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 @@ +<?php + +/* + * This file is part of Flarum. + * + * For detailed copyright and license information, please view the + * LICENSE file that was distributed with this source code. + */ + +namespace Flarum\Mentions\Tests\integration\api; + +use Carbon\Carbon; +use Flarum\Extend; +use Flarum\Post\CommentPost; +use Flarum\Testing\integration\RetrievesAuthorizedUsers; +use Flarum\Testing\integration\TestCase; +use Flarum\Testing\integration\UsesSettings; +use Flarum\User\DisplayName\DriverInterface; +use Flarum\User\User; + +class PostMentionsTest extends TestCase +{ + use RetrievesAuthorizedUsers; + use UsesSettings; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->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' => '<r><POSTMENTION displayname="TobyFlarum___" id="5" number="2" discussionid="2" username="toby">@tobyuuu#5</POSTMENTION></r>'], + ['id' => 5, 'number' => 3, 'discussion_id' => 2, 'created_at' => Carbon::now(), 'user_id' => 4, 'type' => 'comment', 'content' => '<r><POSTMENTION displayname="potato" id="4" number="3" discussionid="2" username="potato">@potato#4</POSTMENTION></r>'], + ], + '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 @@ +<?php + +/* + * This file is part of Flarum. + * + * For detailed copyright and license information, please view the + * LICENSE file that was distributed with this source code. + */ + +namespace Flarum\Mentions\Tests\integration\api; + +use Carbon\Carbon; +use Flarum\Extend; +use Flarum\Post\CommentPost; +use Flarum\Testing\integration\RetrievesAuthorizedUsers; +use Flarum\Testing\integration\TestCase; +use Flarum\Testing\integration\UsesSettings; +use Flarum\User\DisplayName\DriverInterface; +use Flarum\User\User; + +class UserMentionsTest extends TestCase +{ + use RetrievesAuthorizedUsers; + use UsesSettings; + + /** + * @inheritDoc + */ + protected function setUp(): void + { + parent::setUp(); + + $this->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' => '<r><USERMENTION displayname="TobyFlarum___" id="4" username="toby">@tobyuuu</USERMENTION></r>'], + ], + '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 @@ +<?php + +/* + * This file is part of Flarum. + * + * For detailed copyright and license information, please view the + * LICENSE file that was distributed with this source code. + */ + +use Flarum\Testing\integration\Setup\SetupScript; + +require __DIR__.'/../../vendor/autoload.php'; + +$setup = new SetupScript(); + +$setup->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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<phpunit + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" + backupGlobals="false" + backupStaticAttributes="false" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + processIsolation="true" + stopOnFailure="false" +> + <coverage processUncoveredFiles="true"> + <include> + <directory suffix=".php">../src/</directory> + </include> + </coverage> + <testsuites> + <testsuite name="Flarum Integration Tests"> + <directory suffix="Test.php">./integration</directory> + </testsuite> + </testsuites> +</phpunit> 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 @@ +<?xml version="1.0" encoding="UTF-8"?> +<phpunit + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd" + backupGlobals="false" + backupStaticAttributes="false" + colors="true" + convertErrorsToExceptions="true" + convertNoticesToExceptions="true" + convertWarningsToExceptions="true" + processIsolation="false" + stopOnFailure="false" +> + <coverage processUncoveredFiles="true"> + <include> + <directory suffix=".php">../src/</directory> + </include> + </coverage> + <testsuites> + <testsuite name="Flarum Unit Tests"> + <directory suffix="Test.php">./unit</directory> + </testsuite> + </testsuites> + <listeners> + <listener class="\Mockery\Adapter\Phpunit\TestListener" /> + </listeners> +</phpunit> diff --git a/extensions/mentions/tests/unit/.gitkeep b/extensions/mentions/tests/unit/.gitkeep new file mode 100644 index 000000000..e69de29bb