diff --git a/extensions/flags/.github/workflows/test.yml b/extensions/flags/.github/workflows/test.yml
new file mode 100644
index 000000000..d3cfc5a82
--- /dev/null
+++ b/extensions/flags/.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/flags/.vscode/launch.json b/extensions/flags/.vscode/launch.json
new file mode 100644
index 000000000..bad1e468b
--- /dev/null
+++ b/extensions/flags/.vscode/launch.json
@@ -0,0 +1,22 @@
+{
+ // Use IntelliSense to learn about possible attributes.
+ // Hover to view descriptions of existing attributes.
+ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
+ "version": "0.2.0",
+ "configurations": [
+ {
+ "name": "Listen for Xdebug",
+ "type": "php",
+ "request": "launch",
+ "port": 9000
+ },
+ {
+ "name": "Launch currently open script",
+ "type": "php",
+ "request": "launch",
+ "program": "${file}",
+ "cwd": "${fileDirname}",
+ "port": 9000
+ }
+ ]
+}
\ No newline at end of file
diff --git a/extensions/flags/composer.json b/extensions/flags/composer.json
index 8c7ea5a55..615976baf 100644
--- a/extensions/flags/composer.json
+++ b/extensions/flags/composer.json
@@ -1,41 +1,63 @@
{
- "name": "flarum/flags",
- "description": "Allow users to flag posts for moderator review.",
- "type": "flarum-extension",
- "keywords": ["moderation"],
- "license": "MIT",
- "support": {
- "issues": "https://github.com/flarum/core/issues",
- "source": "https://github.com/flarum/flags",
- "forum": "https://discuss.flarum.org"
- },
- "homepage": "https://flarum.org",
- "funding": [
- {
- "type": "website",
- "url": "https://flarum.org/donate/"
- }
- ],
- "require": {
- "flarum/core": "^0.1.0-beta.16"
- },
- "autoload": {
- "psr-4": {
- "Flarum\\Flags\\": "src/"
- }
- },
- "extra": {
- "branch-alias": {
- "dev-master": "0.1.x-dev"
- },
- "flarum-extension": {
- "title": "Flags",
- "category": "feature",
- "icon": {
- "name": "fas fa-flag",
- "backgroundColor": "#D659B5",
- "color": "#fff"
- }
- }
+ "name": "flarum/flags",
+ "description": "Allow users to flag posts for moderator review.",
+ "type": "flarum-extension",
+ "keywords": [
+ "moderation"
+ ],
+ "license": "MIT",
+ "support": {
+ "issues": "https://github.com/flarum/core/issues",
+ "source": "https://github.com/flarum/flags",
+ "forum": "https://discuss.flarum.org"
+ },
+ "homepage": "https://flarum.org",
+ "funding": [
+ {
+ "type": "website",
+ "url": "https://flarum.org/donate/"
}
+ ],
+ "require": {
+ "flarum/core": "^0.1.0-beta.16"
+ },
+ "autoload": {
+ "psr-4": {
+ "Flarum\\Flags\\": "src/"
+ }
+ },
+ "extra": {
+ "branch-alias": {
+ "dev-master": "0.1.x-dev"
+ },
+ "flarum-extension": {
+ "title": "Flags",
+ "category": "feature",
+ "icon": {
+ "name": "fas fa-flag",
+ "backgroundColor": "#D659B5",
+ "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/tags": "0.1.x-dev",
+ "flarum/testing": "*@dev"
+ }
}
diff --git a/extensions/flags/extend.php b/extensions/flags/extend.php
index 2fd82a934..3bb468454 100644
--- a/extensions/flags/extend.php
+++ b/extensions/flags/extend.php
@@ -15,6 +15,7 @@ use Flarum\Api\Serializer\CurrentUserSerializer;
use Flarum\Api\Serializer\ForumSerializer;
use Flarum\Api\Serializer\PostSerializer;
use Flarum\Extend;
+use Flarum\Flags\Access\ScopeFlagVisibility;
use Flarum\Flags\AddCanFlagAttribute;
use Flarum\Flags\AddFlagsApiAttributes;
use Flarum\Flags\AddNewFlagCountAttribute;
@@ -78,5 +79,8 @@ return [
(new Extend\Event())
->listen(Deleted::class, Listener\DeleteFlags::class),
+ (new Extend\ModelVisibility(Flag::class))
+ ->scope(ScopeFlagVisibility::class),
+
new Extend\Locales(__DIR__.'/locale'),
];
diff --git a/extensions/flags/src/Access/ScopeFlagVisibility.php b/extensions/flags/src/Access/ScopeFlagVisibility.php
new file mode 100644
index 000000000..89edda5ba
--- /dev/null
+++ b/extensions/flags/src/Access/ScopeFlagVisibility.php
@@ -0,0 +1,59 @@
+extensions = $extensions;
+ }
+
+ public function __invoke(User $actor, Builder $query)
+ {
+ if ($this->extensions->isEnabled('flarum-tags')) {
+ $query
+ ->select('flags.*')
+ ->leftJoin('posts', 'posts.id', '=', 'flags.post_id')
+ ->leftJoin('discussions', 'discussions.id', '=', 'posts.discussion_id')
+ ->whereNotExists(function ($query) use ($actor) {
+ return $query->selectRaw('1')
+ ->from('discussion_tag')
+ ->whereNotIn('tag_id', function ($query) use ($actor) {
+ Tag::query()->setQuery($query->from('tags'))->whereHasPermission($actor, 'discussion.viewFlags')->select('tags.id');
+ })
+ ->whereColumn('discussions.id', 'discussion_id');
+ });
+
+ if (!$actor->hasPermission('discussion.viewFlags')) {
+ $query->whereExists(function ($query) use ($actor) {
+ return $query->selectRaw('1')
+ ->from('discussion_tag')
+ ->whereColumn('discussions.id', 'discussion_id');
+ });
+ }
+ }
+
+
+ if (!$actor->hasPermission('discussion.viewFlags')) {
+ $query->orWhere('flags.user_id', $actor->id);
+ }
+ }
+}
diff --git a/extensions/flags/tests/.phpunit.result.cache b/extensions/flags/tests/.phpunit.result.cache
new file mode 100644
index 000000000..5b659f337
--- /dev/null
+++ b/extensions/flags/tests/.phpunit.result.cache
@@ -0,0 +1 @@
+C:37:"PHPUnit\Runner\DefaultTestResultCache":2499:{a:2:{s:7:"defects";a:13:{s:81:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::admin_can_see_all_flag";i:3;s:86:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::regular_user_sees_own_flags";i:3;s:77:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::mod_sees_all_flags";i:3;s:79:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::guest_cant_see_flags";i:4;s:73:"Flarum\Flags\Tests\integration\api\flags\ListTest::admin_can_see_all_flag";i:3;s:78:"Flarum\Flags\Tests\integration\api\flags\ListTest::regular_user_sees_own_flags";i:3;s:92:"Flarum\Flags\Tests\integration\api\flags\ListTest::mod_sees_own_flags_in_visible_discussions";i:4;s:71:"Flarum\Flags\Tests\integration\api\flags\ListTest::guest_cant_see_flags";i:3;s:69:"Flarum\Flags\Tests\integration\api\flags\ListTest::mod_sees_all_flags";i:3;s:82:"Flarum\Flags\Tests\integration\api\flags\ListTest::admin_can_see_one_flag_per_post";i:4;s:80:"Flarum\Flags\Tests\integration\api\flags\ListTest::mod_can_see_one_flag_per_post";i:3;s:90:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::admin_can_see_one_flag_per_post";i:4;s:88:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::mod_can_see_one_flag_per_post";i:3;}s:5:"times";a:13:{s:81:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::admin_can_see_all_flag";d:0.346;s:86:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::regular_user_sees_own_flags";d:0.2;s:77:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::mod_sees_all_flags";d:0.245;s:79:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::guest_cant_see_flags";d:0.134;s:73:"Flarum\Flags\Tests\integration\api\flags\ListTest::admin_can_see_all_flag";d:2.598;s:78:"Flarum\Flags\Tests\integration\api\flags\ListTest::regular_user_sees_own_flags";d:0.183;s:92:"Flarum\Flags\Tests\integration\api\flags\ListTest::mod_sees_own_flags_in_visible_discussions";d:0.139;s:71:"Flarum\Flags\Tests\integration\api\flags\ListTest::guest_cant_see_flags";d:0.112;s:69:"Flarum\Flags\Tests\integration\api\flags\ListTest::mod_sees_all_flags";d:0.364;s:82:"Flarum\Flags\Tests\integration\api\flags\ListTest::admin_can_see_one_flag_per_post";d:0.194;s:80:"Flarum\Flags\Tests\integration\api\flags\ListTest::mod_can_see_one_flag_per_post";d:0.177;s:90:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::admin_can_see_one_flag_per_post";d:0.24;s:88:"Flarum\Flags\Tests\integration\api\flags\ListWithTagsTest::mod_can_see_one_flag_per_post";d:0.235;}}}
\ No newline at end of file
diff --git a/extensions/flags/tests/fixtures/.gitkeep b/extensions/flags/tests/fixtures/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/extensions/flags/tests/integration/api/flags/ListTest.php b/extensions/flags/tests/integration/api/flags/ListTest.php
new file mode 100644
index 000000000..7ab74e466
--- /dev/null
+++ b/extensions/flags/tests/integration/api/flags/ListTest.php
@@ -0,0 +1,133 @@
+extension('flarum-flags');
+
+ $this->prepareDatabase([
+ 'users' => [
+ $this->normalUser(),
+ [
+ 'id' => 3,
+ 'username' => 'mod',
+ 'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
+ 'email' => 'normal2@machine.local',
+ 'is_email_confirmed' => 1,
+ ]
+ ],
+ 'group_user' => [
+ ['group_id' => Group::MODERATOR_ID, 'user_id' => 3]
+ ],
+ 'group_permission' => [
+ ['group_id' => Group::MODERATOR_ID, 'permission' => 'discussion.viewFlags'],
+ ],
+ 'discussions' => [
+ ['id' => 1, 'title' => '', 'user_id' => 1, 'comment_count' => 1],
+ ],
+ 'posts' => [
+ ['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => ''],
+ ['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => ''],
+ ['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => ''],
+ ],
+ 'flags' => [
+ ['id' => 1, 'post_id' => 1, 'user_id' => 1],
+ ['id' => 2, 'post_id' => 1, 'user_id' => 2],
+ ['id' => 3, 'post_id' => 1, 'user_id' => 3],
+ ['id' => 4, 'post_id' => 2, 'user_id' => 2],
+ ['id' => 5, 'post_id' => 3, 'user_id' => 1],
+ ]
+ ]);
+ }
+
+ /**
+ * @test
+ */
+ public function admin_can_see_one_flag_per_post()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/flags', [
+ 'authenticatedAs' => 1
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ $ids = Arr::pluck($data, 'id');
+ $this->assertEqualsCanonicalizing(['1', '4', '5'], $ids);
+ }
+
+ /**
+ * @test
+ */
+ public function regular_user_sees_own_flags()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/flags', [
+ 'authenticatedAs' => 2
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ $ids = Arr::pluck($data, 'id');
+ $this->assertEqualsCanonicalizing(['2', '4'], $ids);
+ }
+
+ /**
+ * @test
+ */
+ public function mod_can_see_one_flag_per_post()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/flags', [
+ 'authenticatedAs' => 3
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ $ids = Arr::pluck($data, 'id');
+ $this->assertEqualsCanonicalizing(['1', '4', '5'], $ids);
+ }
+
+ /**
+ * @test
+ */
+ public function guest_cant_see_flags()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/flags')
+ );
+
+ $this->assertEquals(401, $response->getStatusCode());
+ }
+}
diff --git a/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php b/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php
new file mode 100644
index 000000000..6a86e0f0e
--- /dev/null
+++ b/extensions/flags/tests/integration/api/flags/ListWithTagsTest.php
@@ -0,0 +1,168 @@
+extension('flarum-flags');
+ $this->extension('flarum-tags');
+
+ $this->prepareDatabase([
+ 'tags' => [
+ ['id' => 1, 'name' => 'Unrestricted', 'slug' => '1', 'position' => 0, 'parent_id' => null],
+ ['id' => 2, 'name' => 'Mods can view discussions', 'slug' => '2', 'position' => 0, 'parent_id' => null, 'is_restricted' => true],
+ ['id' => 3, 'name' => 'Mods can view flags', 'slug' => '3', 'position' => 0, 'parent_id' => null, 'is_restricted' => true],
+ ['id' => 4, 'name' => 'Mods can view discussions and flags', 'slug' => '4', 'position' => 0, 'parent_id' => null, 'is_restricted' => true],
+ ],
+ 'users' => [
+ $this->normalUser(),
+ [
+ 'id' => 3,
+ 'username' => 'mod',
+ 'password' => '$2y$10$LO59tiT7uggl6Oe23o/O6.utnF6ipngYjvMvaxo1TciKqBttDNKim', // BCrypt hash for "too-obscure"
+ 'email' => 'normal2@machine.local',
+ 'is_email_confirmed' => 1,
+ ]
+ ],
+ 'group_user' => [
+ ['group_id' => Group::MODERATOR_ID, 'user_id' => 3]
+ ],
+ 'group_permission' => [
+ ['group_id' => Group::MODERATOR_ID, 'permission' => 'discussion.viewFlags'],
+ ['group_id' => Group::MODERATOR_ID, 'permission' => 'tag2.viewDiscussions'],
+ ['group_id' => Group::MODERATOR_ID, 'permission' => 'tag3.discussion.viewFlags'],
+ ['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.viewDiscussions'],
+ ['group_id' => Group::MODERATOR_ID, 'permission' => 'tag4.discussion.viewFlags'],
+ ],
+ 'discussions' => [
+ ['id' => 1, 'title' => 'no tags', 'user_id' => 1, 'comment_count' => 1],
+ ['id' => 2, 'title' => 'has tags where mods can view discussions but not flags', 'user_id' => 1, 'comment_count' => 1],
+ ['id' => 3, 'title' => 'has tags where mods can view flags but not discussions', 'user_id' => 1, 'comment_count' => 1],
+ ['id' => 4, 'title' => 'has tags where mods can view discussions and flags', 'user_id' => 1, 'comment_count' => 1],
+ ['id' => 5, 'title' => 'has unrestricted tag', 'user_id' => 1, 'comment_count' => 1],
+ ],
+ 'discussion_tag' => [
+ ['discussion_id' => 2, 'tag_id' => 2],
+ ['discussion_id' => 3, 'tag_id' => 3],
+ ['discussion_id' => 4, 'tag_id' => 4],
+ ['discussion_id' => 5, 'tag_id' => 1],
+ ],
+ 'posts' => [
+ // From regular ListTest
+ ['id' => 1, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => ''],
+ ['id' => 2, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => ''],
+ ['id' => 3, 'discussion_id' => 1, 'user_id' => 1, 'type' => 'comment', 'content' => ''],
+ // In tags
+ ['id' => 4, 'discussion_id' => 2, 'user_id' => 1, 'type' => 'comment', 'content' => ''],
+ ['id' => 5, 'discussion_id' => 3, 'user_id' => 1, 'type' => 'comment', 'content' => ''],
+ ['id' => 6, 'discussion_id' => 4, 'user_id' => 1, 'type' => 'comment', 'content' => ''],
+ ['id' => 7, 'discussion_id' => 5, 'user_id' => 1, 'type' => 'comment', 'content' => ''],
+ ],
+ 'flags' => [
+ // From regular ListTest
+ ['id' => 1, 'post_id' => 1, 'user_id' => 1],
+ ['id' => 2, 'post_id' => 1, 'user_id' => 2],
+ ['id' => 3, 'post_id' => 1, 'user_id' => 3],
+ ['id' => 4, 'post_id' => 2, 'user_id' => 2],
+ ['id' => 5, 'post_id' => 3, 'user_id' => 1],
+ // In tags
+ ['id' => 6, 'post_id' => 4, 'user_id' => 1],
+ ['id' => 7, 'post_id' => 5, 'user_id' => 1],
+ ['id' => 8, 'post_id' => 6, 'user_id' => 1],
+ ['id' => 9, 'post_id' => 7, 'user_id' => 1],
+ ]
+ ]);
+ }
+
+ /**
+ * @test
+ */
+ public function admin_can_see_one_flag_per_post()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/flags', [
+ 'authenticatedAs' => 1
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ $ids = Arr::pluck($data, 'id');
+ $this->assertEqualsCanonicalizing(['1', '4', '5', '6', '7', '8', '9'], $ids);
+ }
+
+ /**
+ * @test
+ */
+ public function regular_user_sees_own_flags()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/flags', [
+ 'authenticatedAs' => 2
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ $ids = Arr::pluck($data, 'id');
+ $this->assertEqualsCanonicalizing(['2', '4'], $ids);
+ }
+
+ /**
+ * @test
+ */
+ public function mod_can_see_one_flag_per_post()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/flags', [
+ 'authenticatedAs' => 3
+ ])
+ );
+
+ $this->assertEquals(200, $response->getStatusCode());
+
+ $data = json_decode($response->getBody()->getContents(), true)['data'];
+
+ $ids = Arr::pluck($data, 'id');
+ // 7 is included, even though mods can't view discussions.
+ // This is because the UI doesnt allow discussions.viewFlags without viewDiscussions.
+ $this->assertEqualsCanonicalizing(['1', '4', '5', '7', '8', '9'], $ids);
+ }
+
+ /**
+ * @test
+ */
+ public function guest_cant_see_flags()
+ {
+ $response = $this->send(
+ $this->request('GET', '/api/flags')
+ );
+
+ $this->assertEquals(401, $response->getStatusCode());
+ }
+}
diff --git a/extensions/flags/tests/integration/setup.php b/extensions/flags/tests/integration/setup.php
new file mode 100644
index 000000000..67039c083
--- /dev/null
+++ b/extensions/flags/tests/integration/setup.php
@@ -0,0 +1,16 @@
+run();
diff --git a/extensions/flags/tests/phpunit.integration.xml b/extensions/flags/tests/phpunit.integration.xml
new file mode 100644
index 000000000..23afc237d
--- /dev/null
+++ b/extensions/flags/tests/phpunit.integration.xml
@@ -0,0 +1,24 @@
+
+
+
+
+ ../src/
+
+
+
+
+ ./integration
+
+
+
diff --git a/extensions/flags/tests/phpunit.unit.xml b/extensions/flags/tests/phpunit.unit.xml
new file mode 100644
index 000000000..d3a4a3e3d
--- /dev/null
+++ b/extensions/flags/tests/phpunit.unit.xml
@@ -0,0 +1,27 @@
+
+
+
+
+ ../src/
+
+
+
+
+ ./unit
+
+
+
+
+
+
diff --git a/extensions/flags/tests/unit/.gitkeep b/extensions/flags/tests/unit/.gitkeep
new file mode 100644
index 000000000..e69de29bb