diff --git a/extensions/mentions/.editorconfig b/extensions/mentions/.editorconfig
index 87694ddab..dc9d5b361 100644
--- a/extensions/mentions/.editorconfig
+++ b/extensions/mentions/.editorconfig
@@ -15,5 +15,5 @@ indent_size = 2
[*.{diff,md}]
trim_trailing_whitespace = false
-[*.php]
+[*.{php,json}]
indent_size = 4
diff --git a/extensions/mentions/.github/workflows/test.yml b/extensions/mentions/.github/workflows/test.yml
new file mode 100644
index 000000000..d3cfc5a82
--- /dev/null
+++ b/extensions/mentions/.github/workflows/test.yml
@@ -0,0 +1,78 @@
+name: Tests
+
+on: [push, pull_request]
+
+jobs:
+ test:
+ runs-on: ubuntu-latest
+
+ strategy:
+ matrix:
+ php: [7.3, 7.4, '8.0']
+ service: ['mysql:5.7', mariadb]
+ prefix: ['', flarum_]
+
+ include:
+ - service: 'mysql:5.7'
+ db: MySQL
+ - service: mariadb
+ db: MariaDB
+ - prefix: flarum_
+ prefixStr: (prefix)
+
+ exclude:
+ - php: 7.3
+ service: 'mysql:5.7'
+ prefix: flarum_
+ - php: 7.3
+ service: mariadb
+ prefix: flarum_
+ - php: 8.0
+ service: 'mysql:5.7'
+ prefix: flarum_
+ - php: 8.0
+ service: mariadb
+ prefix: flarum_
+
+ services:
+ mysql:
+ image: ${{ matrix.service }}
+ ports:
+ - 13306:3306
+
+ name: 'PHP ${{ matrix.php }} / ${{ matrix.db }} ${{ matrix.prefixStr }}'
+
+ steps:
+ - uses: actions/checkout@master
+
+ - name: Setup PHP
+ uses: shivammathur/setup-php@v2
+ with:
+ php-version: ${{ matrix.php }}
+ coverage: xdebug
+ extensions: curl, dom, gd, json, mbstring, openssl, pdo_mysql, tokenizer, zip
+ tools: phpunit, composer:v2
+
+ # The authentication alter is necessary because newer mysql versions use the `caching_sha2_password` driver,
+ # which isn't supported prior to PHP7.4
+ # When we drop support for PHP7.3, we should remove this from the setup.
+ - name: Create MySQL Database
+ run: |
+ sudo systemctl start mysql
+ mysql -uroot -proot -e 'CREATE DATABASE flarum_test;' --port 13306
+ mysql -uroot -proot -e "ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY 'root';" --port 13306
+
+ - name: Install Composer dependencies
+ run: composer install
+
+ - name: Setup Composer tests
+ run: composer test:setup
+ env:
+ DB_PORT: 13306
+ DB_PASSWORD: root
+ DB_PREFIX: ${{ matrix.prefix }}
+
+ - name: Run Composer tests
+ run: composer test
+ env:
+ COMPOSER_PROCESS_TIMEOUT: 600
diff --git a/extensions/mentions/.gitignore b/extensions/mentions/.gitignore
index 7f43257e7..36f90627a 100644
--- a/extensions/mentions/.gitignore
+++ b/extensions/mentions/.gitignore
@@ -1,6 +1,9 @@
/vendor
+composer.lock
composer.phar
.DS_Store
Thumbs.db
+tests/.phpunit.result.cache
+/tests/integration/tmp
node_modules
js/dist/*
diff --git a/extensions/mentions/composer.json b/extensions/mentions/composer.json
index 625bc5fe1..07db558de 100644
--- a/extensions/mentions/composer.json
+++ b/extensions/mentions/composer.json
@@ -2,7 +2,9 @@
"name": "flarum/mentions",
"description": "Mention and reply to specific posts and users.",
"type": "flarum-extension",
- "keywords": ["discussion"],
+ "keywords": [
+ "discussion"
+ ],
"license": "MIT",
"support": {
"issues": "https://github.com/flarum/core/issues",
@@ -37,5 +39,24 @@
"color": "#fff"
}
}
+ },
+ "scripts": {
+ "test": [
+ "@test:unit",
+ "@test:integration"
+ ],
+ "test:unit": "phpunit -c tests/phpunit.unit.xml",
+ "test:integration": "phpunit -c tests/phpunit.integration.xml",
+ "test:setup": "@php tests/integration/setup.php"
+ },
+ "scripts-descriptions": {
+ "test": "Runs all tests.",
+ "test:unit": "Runs all unit tests.",
+ "test:integration": "Runs all integration tests.",
+ "test:setup": "Sets up a database for use with integration tests. Execute this only once."
+ },
+ "require-dev": {
+ "flarum/core": "0.1.x-dev",
+ "flarum/testing": "^0.1.0-beta.16"
}
}
diff --git a/extensions/mentions/tests/fixtures/.gitkeep b/extensions/mentions/tests/fixtures/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/extensions/mentions/tests/integration/api/PostMentionsTest.php b/extensions/mentions/tests/integration/api/PostMentionsTest.php
new file mode 100644
index 000000000..5d88dc8c2
--- /dev/null
+++ b/extensions/mentions/tests/integration/api/PostMentionsTest.php
@@ -0,0 +1,207 @@
+extension('flarum-mentions');
+
+ $this->prepareDatabase([
+ 'users' => [
+ ['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
+ ['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
+ ],
+ 'discussions' => [
+ ['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
+ ],
+ 'posts' => [
+ ['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '@tobyuuu#5'],
+ ['id' => 5, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '@potato#4'],
+ ],
+ 'post_mentions_post' => [
+ ['post_id' => 4, 'mentions_post_id' => 5],
+ ['post_id' => 5, 'mentions_post_id' => 4]
+ ],
+ 'settings' => [
+ ['key' => 'display_name_driver', 'value' => 'custom_display_name_driver'],
+ ],
+ ]);
+
+ $this->extend(
+ (new Extend\User)
+ ->displayNameDriver('custom_display_name_driver', CustomOtherDisplayNameDriver::class)
+ );
+ }
+
+ /**
+ * Purge the settings cache and reset the new display name driver.
+ */
+ protected function recalculateDisplayNameDriver()
+ {
+ $this->purgeSettingsCache();
+ $container = $this->app()->getContainer();
+ $container->forgetInstance('flarum.user.display_name.driver');
+ User::setDisplayNameDriver($container->make('flarum.user.display_name.driver'));
+ }
+
+ /**
+ * @test
+ */
+ public function mentioning_a_valid_post_works()
+ {
+ $this->app();
+ $this->recalculateDisplayNameDriver();
+
+ $response = $this->send(
+ $this->request('POST', '/api/posts', [
+ 'authenticatedAs' => 1,
+ 'json' => [
+ 'data' => [
+ 'attributes' => [
+ 'content' => '@potato#4',
+ ],
+ 'relationships' => [
+ 'discussion' => ['data' => ['id' => 2]],
+ ],
+ ],
+ ],
+ ])
+ );
+
+ $this->assertEquals(201, $response->getStatusCode());
+
+ $response = json_decode($response->getBody(), true);
+
+ $this->assertStringContainsString('POTATO$', $response['data']['attributes']['contentHtml']);
+ $this->assertStringContainsString('@potato#4', $response['data']['attributes']['content']);
+ $this->assertStringContainsString('PostMention', $response['data']['attributes']['contentHtml']);
+ $this->assertNotNull(CommentPost::find($response['data']['id'])->mentionsPosts->find(4));
+ }
+
+ /**
+ * @test
+ */
+ public function mentioning_an_invalid_post_doesnt_work()
+ {
+ $this->app();
+ $this->recalculateDisplayNameDriver();
+
+ $response = $this->send(
+ $this->request('POST', '/api/posts', [
+ 'authenticatedAs' => 1,
+ 'json' => [
+ 'data' => [
+ 'attributes' => [
+ 'content' => '@franzofflarum#215',
+ ],
+ 'relationships' => [
+ 'discussion' => ['data' => ['id' => 2]],
+ ],
+ ],
+ ],
+ ])
+ );
+
+ $this->assertEquals(201, $response->getStatusCode());
+
+ $response = json_decode($response->getBody(), true);
+
+ $this->assertStringNotContainsString('FRANZOFFLARUM$', $response['data']['attributes']['contentHtml']);
+ $this->assertStringContainsString('@franzofflarum#215', $response['data']['attributes']['content']);
+ $this->assertStringNotContainsString('PostMention', $response['data']['attributes']['contentHtml']);
+ $this->assertCount(0, CommentPost::find($response['data']['id'])->mentionsPosts);
+ }
+
+ /**
+ * @test
+ */
+ public function mentioning_multiple_posts_works()
+ {
+ $this->app();
+ $this->recalculateDisplayNameDriver();
+
+ $response = $this->send(
+ $this->request('POST', '/api/posts', [
+ 'authenticatedAs' => 1,
+ 'json' => [
+ 'data' => [
+ 'attributes' => [
+ 'content' => '@toby#5 @flarum @franzofflarum#220 @potato @potato#4',
+ ],
+ 'relationships' => [
+ 'discussion' => ['data' => ['id' => 2]],
+ ],
+ ],
+ ],
+ ])
+ );
+
+ $this->assertEquals(201, $response->getStatusCode());
+
+ $response = json_decode($response->getBody(), true);
+
+ $this->assertStringContainsString('TOBY$', $response['data']['attributes']['contentHtml']);
+ $this->assertStringNotContainsString('FRANZOFFLARUM$', $response['data']['attributes']['contentHtml']);
+ $this->assertStringContainsString('POTATO$', $response['data']['attributes']['contentHtml']);
+ $this->assertEquals('@toby#5 @flarum @franzofflarum#220 @potato @potato#4', $response['data']['attributes']['content']);
+ $this->assertStringContainsString('PostMention', $response['data']['attributes']['contentHtml']);
+ $this->assertCount(2, CommentPost::find($response['data']['id'])->mentionsPosts);
+ }
+
+ /**
+ * @test
+ */
+ public function post_mentions_render_with_fresh_data()
+ {
+ $this->app();
+ $this->recalculateDisplayNameDriver();
+
+ $response = $this->send(
+ $this->request('GET', '/api/posts/4', [
+ 'authenticatedAs' => 1,
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+
+ $response = json_decode($response->getBody(), true);
+
+ $this->assertStringContainsString('TOBY$', $response['data']['attributes']['contentHtml']);
+ $this->assertStringContainsString('PostMention', $response['data']['attributes']['contentHtml']);
+ $this->assertCount(1, CommentPost::find($response['data']['id'])->mentionsPosts);
+ }
+}
+
+class CustomOtherDisplayNameDriver implements DriverInterface
+{
+ public function displayName(User $user): string
+ {
+ return strtoupper($user->username).'$';
+ }
+}
diff --git a/extensions/mentions/tests/integration/api/UserMentionsTest.php b/extensions/mentions/tests/integration/api/UserMentionsTest.php
new file mode 100644
index 000000000..a23de7498
--- /dev/null
+++ b/extensions/mentions/tests/integration/api/UserMentionsTest.php
@@ -0,0 +1,206 @@
+extension('flarum-mentions');
+
+ $this->prepareDatabase([
+ 'users' => [
+ $this->normalUser(),
+ ['id' => 3, 'username' => 'potato', 'email' => 'potato@machine.local', 'is_email_confirmed' => 1],
+ ['id' => 4, 'username' => 'toby', 'email' => 'toby@machine.local', 'is_email_confirmed' => 1],
+ ],
+ 'discussions' => [
+ ['id' => 2, 'title' => __CLASS__, 'created_at' => Carbon::now(), 'last_posted_at' => Carbon::now(), 'user_id' => 3, 'first_post_id' => 4, 'comment_count' => 2],
+ ],
+ 'posts' => [
+ ['id' => 4, 'number' => 2, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 3, 'type' => 'comment', 'content' => '@tobyuuu'],
+ ],
+ 'post_mentions_user' => [
+ ['post_id' => 4, 'mentions_user_id' => 4]
+ ],
+ 'settings' => [
+ ['key' => 'display_name_driver', 'value' => 'custom_display_name_driver'],
+ ],
+ ]);
+
+ $this->extend(
+ (new Extend\User)
+ ->displayNameDriver('custom_display_name_driver', CustomDisplayNameDriver::class)
+ );
+ }
+
+ /**
+ * Purge the settings cache and reset the new display name driver.
+ */
+ protected function recalculateDisplayNameDriver()
+ {
+ $this->purgeSettingsCache();
+ $container = $this->app()->getContainer();
+ $container->forgetInstance('flarum.user.display_name.driver');
+ User::setDisplayNameDriver($container->make('flarum.user.display_name.driver'));
+ }
+
+ /**
+ * @test
+ */
+ public function mentioning_a_valid_user_works()
+ {
+ $this->app();
+ $this->recalculateDisplayNameDriver();
+
+ $response = $this->send(
+ $this->request('POST', '/api/posts', [
+ 'authenticatedAs' => 2,
+ 'json' => [
+ 'data' => [
+ 'attributes' => [
+ 'content' => '@potato',
+ ],
+ 'relationships' => [
+ 'discussion' => ['data' => ['id' => 2]],
+ ],
+ ],
+ ],
+ ])
+ );
+
+ $this->assertEquals(201, $response->getStatusCode());
+
+ $response = json_decode($response->getBody(), true);
+
+ $this->assertStringContainsString('@POTATO$', $response['data']['attributes']['contentHtml']);
+ $this->assertStringContainsString('@potato', $response['data']['attributes']['content']);
+ $this->assertStringContainsString('UserMention', $response['data']['attributes']['contentHtml']);
+ $this->assertNotNull(CommentPost::find($response['data']['id'])->mentionsUsers->find(3));
+ }
+
+ /**
+ * @test
+ */
+ public function mentioning_an_invalid_user_doesnt_work()
+ {
+ $this->app();
+ $this->recalculateDisplayNameDriver();
+
+ $response = $this->send(
+ $this->request('POST', '/api/posts', [
+ 'authenticatedAs' => 2,
+ 'json' => [
+ 'data' => [
+ 'attributes' => [
+ 'content' => '@franzofflarum',
+ ],
+ 'relationships' => [
+ 'discussion' => ['data' => ['id' => 2]],
+ ],
+ ],
+ ],
+ ])
+ );
+
+ $this->assertEquals(201, $response->getStatusCode());
+
+ $response = json_decode($response->getBody(), true);
+
+ $this->assertStringNotContainsString('@FRANZOFFLARUM$', $response['data']['attributes']['contentHtml']);
+ $this->assertStringContainsString('@franzofflarum', $response['data']['attributes']['content']);
+ $this->assertStringNotContainsString('UserMention', $response['data']['attributes']['contentHtml']);
+ $this->assertCount(0, CommentPost::find($response['data']['id'])->mentionsUsers);
+ }
+
+ /**
+ * @test
+ */
+ public function mentioning_multiple_users_works()
+ {
+ $this->app();
+ $this->recalculateDisplayNameDriver();
+
+ $response = $this->send(
+ $this->request('POST', '/api/posts', [
+ 'authenticatedAs' => 2,
+ 'json' => [
+ 'data' => [
+ 'attributes' => [
+ 'content' => '@toby @potato#4 @franzofflarum @potato',
+ ],
+ 'relationships' => [
+ 'discussion' => ['data' => ['id' => 2]],
+ ],
+ ],
+ ],
+ ])
+ );
+
+ $this->assertEquals(201, $response->getStatusCode());
+
+ $response = json_decode($response->getBody(), true);
+
+ $this->assertStringContainsString('@TOBY$', $response['data']['attributes']['contentHtml']);
+ $this->assertStringNotContainsString('@FRANZOFFLARUM$', $response['data']['attributes']['contentHtml']);
+ $this->assertStringContainsString('@POTATO$', $response['data']['attributes']['contentHtml']);
+ $this->assertEquals('@toby @potato#4 @franzofflarum @potato', $response['data']['attributes']['content']);
+ $this->assertStringContainsString('UserMention', $response['data']['attributes']['contentHtml']);
+ $this->assertCount(2, CommentPost::find($response['data']['id'])->mentionsUsers);
+ }
+
+ /**
+ * @test
+ */
+ public function user_mentions_render_with_fresh_data()
+ {
+ $this->app();
+ $this->recalculateDisplayNameDriver();
+
+ $response = $this->send(
+ $this->request('GET', '/api/posts/4', [
+ 'authenticatedAs' => 1,
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+
+ $response = json_decode($response->getBody(), true);
+
+ $this->assertStringContainsString('@TOBY$', $response['data']['attributes']['contentHtml']);
+ $this->assertStringContainsString('UserMention', $response['data']['attributes']['contentHtml']);
+ $this->assertCount(1, CommentPost::find($response['data']['id'])->mentionsUsers);
+ }
+}
+
+class CustomDisplayNameDriver implements DriverInterface
+{
+ public function displayName(User $user): string
+ {
+ return strtoupper($user->username).'$';
+ }
+}
diff --git a/extensions/mentions/tests/integration/setup.php b/extensions/mentions/tests/integration/setup.php
new file mode 100644
index 000000000..67039c083
--- /dev/null
+++ b/extensions/mentions/tests/integration/setup.php
@@ -0,0 +1,16 @@
+run();
diff --git a/extensions/mentions/tests/phpunit.integration.xml b/extensions/mentions/tests/phpunit.integration.xml
new file mode 100644
index 000000000..23afc237d
--- /dev/null
+++ b/extensions/mentions/tests/phpunit.integration.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ ../src/
+
+
+
+
+ ./integration
+
+
+
diff --git a/extensions/mentions/tests/phpunit.unit.xml b/extensions/mentions/tests/phpunit.unit.xml
new file mode 100644
index 000000000..d3a4a3e3d
--- /dev/null
+++ b/extensions/mentions/tests/phpunit.unit.xml
@@ -0,0 +1,27 @@
+
+
+
+
+ ../src/
+
+
+
+
+ ./unit
+
+
+
+
+
+
diff --git a/extensions/mentions/tests/unit/.gitkeep b/extensions/mentions/tests/unit/.gitkeep
new file mode 100644
index 000000000..e69de29bb