diff --git a/.github/workflows/analyse.yml b/.github/workflows/analyse.yml new file mode 100644 index 0000000..d1d7f83 --- /dev/null +++ b/.github/workflows/analyse.yml @@ -0,0 +1,31 @@ +name: analyse + +on: + push: + paths: + - '**.php' + - '.github/workflows/analyse.yml' + - 'phpstan.neon.dist' + - 'composer.json' + - 'composer.lock' + +jobs: + analyse: + name: phpstan + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.3' + extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick + coverage: none + + - name: Install dependencies + run: composer install --no-interaction --prefer-dist + + - name: Run PHPStan + run: vendor/bin/phpstan analyse --error-format=github diff --git a/.github/workflows/psalm.yml b/.github/workflows/psalm.yml deleted file mode 100644 index 7fb63ef..0000000 --- a/.github/workflows/psalm.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Psalm - -on: - push: - paths: - - '**.php' - - 'psalm.xml.dist' - -jobs: - psalm: - name: psalm - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup PHP - uses: shivammathur/setup-php@v2 - with: - php-version: '8.0' - extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick - coverage: none - - - name: Cache composer dependencies - uses: actions/cache@v4.3.0 - with: - path: vendor - key: composer-${{ hashFiles('composer.lock') }} - - - name: Run composer install - run: composer install -n --prefer-dist - - - name: Run psalm - run: ./vendor/bin/psalm --output-format=github diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index dd2ada4..a05ee98 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -4,44 +4,42 @@ on: push: branches: - main + paths: + - '**.php' + - '.github/workflows/run-tests.yml' + - 'phpunit.xml.dist' + - 'composer.json' + - 'composer.lock' pull_request: branches: - main + paths: + - '**.php' + - '.github/workflows/run-tests.yml' + - 'phpunit.xml.dist' + - 'composer.json' + - 'composer.lock' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: test: runs-on: ${{ matrix.os }} + timeout-minutes: 5 strategy: fail-fast: true matrix: os: [ubuntu-latest, windows-latest] - php: [8.4, 8.3, 8.2, 8.1, 8.0] - laravel: ['9.*', '10.*', '11.*', '12.*'] + php: [8.5, 8.4, 8.3] + laravel: ['11.*', '12.*'] stability: [prefer-lowest, prefer-stable] include: - laravel: 12.* - testbench: 10.* - carbon: ^3.8.4 - - laravel: 11.* - testbench: 9.* - carbon: ^3.8.4 - - laravel: 10.* - testbench: 8.* - carbon: ^2.63 - - laravel: 9.* - testbench: 7.* - carbon: ^2.63 - exclude: - - laravel: 10.* - php: 8.0 - - laravel: 11.* - php: 8.1 + testbench: ^10.0 - laravel: 11.* - php: 8.0 - - laravel: 12.* - php: 8.1 - - laravel: 12.* - php: 8.0 + testbench: ^9.1.0 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} @@ -63,8 +61,11 @@ jobs: - name: Install dependencies run: | - composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" "nesbot/carbon:${{ matrix.carbon }}" --no-interaction --no-update + composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --no-interaction --no-update composer update --${{ matrix.stability }} --prefer-dist --no-interaction + - name: List Installed Dependencies + run: composer show -D + - name: Execute tests - run: vendor/bin/phpunit + run: vendor/bin/pest --ci diff --git a/.gitignore b/.gitignore index e6987cc..1f73efc 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ vendor node_modules .php-cs-fixer.cache .phpunit.cache/test-results +.claude/ diff --git a/README.md b/README.md index 96b2849..447de28 100644 --- a/README.md +++ b/README.md @@ -23,20 +23,30 @@ You can install the package via composer: composer require maize-tech/laravel-markable ``` -You can publish and run the migrations with: +```bash +php artisan markable:install +``` + +The install command will automatically publish the config file. Then publish only the migrations you need: ```bash -php artisan vendor:publish --tag="markable-migration-bookmark" # publishes bookmark migration -php artisan vendor:publish --tag="markable-migration-favorite" # publishes favorite migration -php artisan vendor:publish --tag="markable-migration-like" # publishes like migration -php artisan vendor:publish --tag="markable-migration-reaction" # publishes reaction migration +php artisan vendor:publish --tag="markable-migration-bookmark" +``` -php artisan migrate +```bash +php artisan vendor:publish --tag="markable-migration-favorite" ``` -You can publish the config file with: ```bash -php artisan vendor:publish --tag="markable-config" +php artisan vendor:publish --tag="markable-migration-like" +``` + +```bash +php artisan vendor:publish --tag="markable-migration-reaction" +``` + +```bash +php artisan migrate ``` This is the content of the published config file: @@ -287,6 +297,53 @@ $user = auth()->user(); Reaction::add($post, $user, 'random_value'); // adds the 'random_value' reaction to the post for the given user ``` +### Using BackedEnum values + +You can use a `BackedEnum` class as the `allowed_values` for a mark. This lets you leverage PHP enums for type-safe mark values instead of plain strings. + +Define your enum: + +``` php +// app/Enums/ReactionType.php +enum ReactionType: string +{ + case Like = 'like'; + case Love = 'love'; + case Wow = 'wow'; +} +``` + +Set the enum class as the allowed values in `config/markable.php`: + +``` php +'allowed_values' => [ + 'reaction' => \App\Enums\ReactionType::class, +], +``` + +All mark methods accept `null|string|\BackedEnum`, so you can pass enum cases directly: + +``` php +use App\Enums\ReactionType; +use App\Models\Post; +use Maize\Markable\Models\Reaction; + +$post = Post::firstOrFail(); +$user = auth()->user(); + +Reaction::add($post, $user, ReactionType::Like); +Reaction::remove($post, $user, ReactionType::Like); +Reaction::toggle($post, $user, ReactionType::Love); +Reaction::has($post, $user, ReactionType::Like); // bool +Reaction::count($post, ReactionType::Wow); // int +``` + +The dynamic Eloquent scopes also accept enum cases: + +``` php +Post::whereHasReaction($user, ReactionType::Like)->get(); +``` + ### Retrieve the list of marks of an entity with eloquent ``` php diff --git a/composer.json b/composer.json index 8d42620..b1f4dac 100644 --- a/composer.json +++ b/composer.json @@ -24,17 +24,17 @@ } ], "require": { - "php": "^8.0", - "illuminate/database": "^9.0|^10.0|^11.0|^12.0", - "illuminate/support": "^9.0|^10.0|^11.0|^12.0", + "php": "^8.3", + "illuminate/database": "^11.0|^12.0", + "illuminate/support": "^11.0|^12.0", "spatie/laravel-package-tools": "^1.92.6" }, "require-dev": { "laravel/pint": "^1.0", - "orchestra/testbench": "^7.0|^8.0|^9.0|^10.0", - "phpunit/phpunit": "^9.5|^10.5|^11.5.3", - "spatie/laravel-ray": "^1.29", - "vimeo/psalm": "^4.22|^5.22|^6.6" + "orchestra/testbench": "^9.1|^10.0", + "pestphp/pest": "^3", + "pestphp/pest-plugin-laravel": "^3", + "larastan/larastan": "^2.9|^3.0" }, "autoload": { "psr-4": { @@ -49,12 +49,15 @@ }, "scripts": { "format": "vendor/bin/pint", - "psalm": "vendor/bin/psalm", - "test": "vendor/bin/phpunit --colors=always", - "test-coverage": "vendor/bin/phpunit --coverage-html coverage" + "analyse": "vendor/bin/phpstan analyse", + "test": "vendor/bin/pest --colors=always", + "test-coverage": "vendor/bin/pest --coverage-html coverage" }, "config": { - "sort-packages": true + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true + } }, "extra": { "laravel": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..6dbf016 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,8 @@ +includes: + - vendor/larastan/larastan/extension.neon + +parameters: + paths: + - src + + level: 5 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 6a26e4c..65838c9 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,5 +1,5 @@ - + tests diff --git a/psalm.xml.dist b/psalm.xml.dist deleted file mode 100644 index c6df33e..0000000 --- a/psalm.xml.dist +++ /dev/null @@ -1,16 +0,0 @@ - - - - - - - - - diff --git a/src/Mark.php b/src/Mark.php index e8cb8cd..04178f8 100644 --- a/src/Mark.php +++ b/src/Mark.php @@ -11,6 +11,7 @@ use Illuminate\Support\Str; use Maize\Markable\Exceptions\InvalidMarkableInstanceException; use Maize\Markable\Exceptions\InvalidMarkValueException; +use Maize\Markable\Support\Config; abstract class Mark extends MorphPivot { @@ -33,7 +34,7 @@ public static function allowedValues(): array|string|null { $className = Str::lower(static::getMarkClassName()); - return config("markable.allowed_values.{$className}"); + return Config::getAllowedValues($className); } public static function getMarkClassName(): string @@ -42,10 +43,12 @@ public static function getMarkClassName(): string ->__toString(); } - public static function add(Model $markable, Model $user, ?string $value = null, array $metadata = []): self + public static function add(Model $markable, Model $user, null|string|\BackedEnum $value = null, array $metadata = []): self { static::validMarkable($markable); + $value = MarkValue::resolve($value); + if (! static::hasAllowedValues($value)) { throw InvalidMarkValueException::create(); } @@ -68,10 +71,12 @@ public static function add(Model $markable, Model $user, ?string $value = null, return static::firstOrCreate($attributes, $values); } - public static function remove(Model $markable, Model $user, ?string $value = null) + public static function remove(Model $markable, Model $user, null|string|\BackedEnum $value = null) { static::validMarkable($markable); + $value = MarkValue::resolve($value); + return static::where([ app(static::class)->getUserIdColumn() => $user->getKey(), 'markable_id' => $markable->getKey(), @@ -80,10 +85,12 @@ public static function remove(Model $markable, Model $user, ?string $value = nul ])->get()->each->delete(); } - public static function count(Model $markable, ?string $value = null): int + public static function count(Model $markable, null|string|\BackedEnum $value = null): int { static::validMarkable($markable); + $value = MarkValue::resolve($value); + return static::where([ 'markable_id' => $markable->getKey(), 'markable_type' => $markable->getMorphClass(), @@ -91,8 +98,10 @@ public static function count(Model $markable, ?string $value = null): int ])->count(); } - public static function has(Model $markable, Model $user, ?string $value = null): bool + public static function has(Model $markable, Model $user, null|string|\BackedEnum $value = null): bool { + $value = MarkValue::resolve($value); + return static::where([ app(static::class)->getUserIdColumn() => $user->getKey(), 'markable_id' => $markable->getKey(), @@ -101,27 +110,34 @@ public static function has(Model $markable, Model $user, ?string $value = null): ])->exists(); } - public static function toggle(Model $markable, Model $user, ?string $value = null, array $metadata = []) + public static function toggle(Model $markable, Model $user, null|string|\BackedEnum $value = null, array $metadata = []) { return static::has($markable, $user, $value) ? static::remove($markable, $user, $value) : static::add($markable, $user, $value, $metadata); } - public static function hasAllowedValues(?string $value): bool + public static function hasAllowedValues(null|string|\BackedEnum $value): bool { + $resolvedValue = MarkValue::resolve($value); $allowedValues = static::allowedValues() ?? [null]; if ($allowedValues === '*') { return true; } - return in_array($value, $allowedValues); + if (is_string($allowedValues) && enum_exists($allowedValues)) { + $allowedValues = $allowedValues::cases(); + } + + $allowedValues = MarkValue::resolveAll($allowedValues); + + return in_array($resolvedValue, $allowedValues); } public function user(): BelongsTo { - return $this->belongsTo(config('markable.user_model')); + return $this->belongsTo(Config::getUserModel()); } public function markable(): MorphTo @@ -143,7 +159,7 @@ public function getTable(): string { if (is_null($this->table)) { $this->setTable( - config('markable.table_prefix', 'markable_'). + Config::getTablePrefix(). Str::snake(Str::pluralStudly(class_basename($this))) ); } diff --git a/src/MarkValue.php b/src/MarkValue.php new file mode 100644 index 0000000..6534b3f --- /dev/null +++ b/src/MarkValue.php @@ -0,0 +1,16 @@ +value : $value; + } + + public static function resolveAll(array $values): array + { + return array_map(fn ($v) => static::resolve($v), $values); + } +} diff --git a/src/Markable.php b/src/Markable.php index 6b91207..fded6bd 100644 --- a/src/Markable.php +++ b/src/Markable.php @@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Model; use Maize\Markable\Exceptions\InvalidMarkInstanceException; use Maize\Markable\Scopes\MarkableScope; +use Maize\Markable\Support\Config; trait Markable { @@ -25,8 +26,10 @@ public static function marks(): array return static::$marks ?? []; } - public function scopeWhereHasMark(Builder $builder, Mark $mark, Model $user, ?string $value = null): Builder + public function scopeWhereHasMark(Builder $builder, Mark $mark, Model $user, null|string|\BackedEnum $value = null): Builder { + $value = MarkValue::resolve($value); + return $builder->whereHas( $mark->markableRelationName(), fn (Builder $b) => $b->where([ @@ -61,7 +64,7 @@ protected static function addMarkableRelation(string $markClass): void static::resolveRelationUsing( $markModel->markableRelationName(), fn ($markable) => $markable - ->morphToMany(config('markable.user_model'), 'markable', $markModel->getTable()) + ->morphToMany(Config::getUserModel(), 'markable', $markModel->getTable()) ->using($markModel->getMorphClass()) ->withPivot('value') ->withTimestamps() diff --git a/src/MarkableServiceProvider.php b/src/MarkableServiceProvider.php index 6999e36..bd0356d 100644 --- a/src/MarkableServiceProvider.php +++ b/src/MarkableServiceProvider.php @@ -2,6 +2,7 @@ namespace Maize\Markable; +use Spatie\LaravelPackageTools\Commands\InstallCommand; use Spatie\LaravelPackageTools\Package; use Spatie\LaravelPackageTools\PackageServiceProvider; @@ -18,7 +19,12 @@ public function configurePackage(Package $package): void { $package ->name('laravel-markable') - ->hasConfigFile(); + ->hasConfigFile() + ->hasInstallCommand(function (InstallCommand $command) { + $command + ->publishConfigFile() + ->askToStarRepoOnGitHub('maize-tech/laravel-markable'); + }); } public function bootingPackage() diff --git a/src/Support/Config.php b/src/Support/Config.php new file mode 100644 index 0000000..b01d594 --- /dev/null +++ b/src/Support/Config.php @@ -0,0 +1,21 @@ +create(); - $posts = Post::factory(2)->create(); - $users = User::factory(2)->create(); +it('can filter models with bookmark', function () { + $articles = Article::factory(5)->create(); + $posts = Post::factory(2)->create(); + $users = User::factory(2)->create(); - $this->assertCount(0, Article::whereHasBookmark($users[0])->get()); - $this->assertCount(0, Article::whereHasBookmark($users[1])->get()); + expect(Article::whereHasBookmark($users[0])->get())->toHaveCount(0); + expect(Article::whereHasBookmark($users[1])->get())->toHaveCount(0); - Bookmark::add($articles[0], $users[0]); - $this->assertCount(1, Article::whereHasBookmark($users[0])->get()); - $this->assertCount(0, Article::whereHasBookmark($users[1])->get()); + Bookmark::add($articles[0], $users[0]); + expect(Article::whereHasBookmark($users[0])->get())->toHaveCount(1); + expect(Article::whereHasBookmark($users[1])->get())->toHaveCount(0); - Bookmark::add($articles[2], $users[0]); - $this->assertCount(2, Article::whereHasBookmark($users[0])->get()); - $this->assertCount(0, Article::whereHasBookmark($users[1])->get()); + Bookmark::add($articles[2], $users[0]); + expect(Article::whereHasBookmark($users[0])->get())->toHaveCount(2); + expect(Article::whereHasBookmark($users[1])->get())->toHaveCount(0); - Bookmark::add($articles[2], $users[1]); - $this->assertCount(2, Article::whereHasBookmark($users[0])->get()); - $this->assertCount(1, Article::whereHasBookmark($users[1])->get()); + Bookmark::add($articles[2], $users[1]); + expect(Article::whereHasBookmark($users[0])->get())->toHaveCount(2); + expect(Article::whereHasBookmark($users[1])->get())->toHaveCount(1); - $this->expectException(InvalidMarkableInstanceException::class); - Bookmark::add($posts[0], $users[0]); - } -} + expect(fn () => Bookmark::add($posts[0], $users[0]))->toThrow(InvalidMarkableInstanceException::class); +}); diff --git a/tests/Enums/ReactionType.php b/tests/Enums/ReactionType.php new file mode 100644 index 0000000..f58c951 --- /dev/null +++ b/tests/Enums/ReactionType.php @@ -0,0 +1,10 @@ +create(); - $posts = Post::factory(2)->create(); - $users = User::factory(2)->create(); +it('can filter models with favorite', function () { + $articles = Article::factory(5)->create(); + $posts = Post::factory(2)->create(); + $users = User::factory(2)->create(); - $this->assertCount(0, Article::whereHasFavorite($users[0])->get()); - $this->assertCount(0, Article::whereHasFavorite($users[1])->get()); + expect(Article::whereHasFavorite($users[0])->get())->toHaveCount(0); + expect(Article::whereHasFavorite($users[1])->get())->toHaveCount(0); - Favorite::add($articles[0], $users[0]); - $this->assertCount(1, Article::whereHasFavorite($users[0])->get()); - $this->assertCount(0, Article::whereHasFavorite($users[1])->get()); + Favorite::add($articles[0], $users[0]); + expect(Article::whereHasFavorite($users[0])->get())->toHaveCount(1); + expect(Article::whereHasFavorite($users[1])->get())->toHaveCount(0); - Favorite::add($articles[2], $users[0]); - $this->assertCount(2, Article::whereHasFavorite($users[0])->get()); - $this->assertCount(0, Article::whereHasFavorite($users[1])->get()); + Favorite::add($articles[2], $users[0]); + expect(Article::whereHasFavorite($users[0])->get())->toHaveCount(2); + expect(Article::whereHasFavorite($users[1])->get())->toHaveCount(0); - Favorite::add($articles[2], $users[1]); - $this->assertCount(2, Article::whereHasFavorite($users[0])->get()); - $this->assertCount(1, Article::whereHasFavorite($users[1])->get()); + Favorite::add($articles[2], $users[1]); + expect(Article::whereHasFavorite($users[0])->get())->toHaveCount(2); + expect(Article::whereHasFavorite($users[1])->get())->toHaveCount(1); - $this->expectException(InvalidMarkableInstanceException::class); - Favorite::add($posts[0], $users[0]); - } -} + expect(fn () => Favorite::add($posts[0], $users[0]))->toThrow(InvalidMarkableInstanceException::class); +}); diff --git a/tests/LikeTest.php b/tests/LikeTest.php index 5ec445a..0664930 100644 --- a/tests/LikeTest.php +++ b/tests/LikeTest.php @@ -1,38 +1,31 @@ create(); - $posts = Post::factory(2)->create(); - $users = User::factory(2)->create(); +it('can filter models with like', function () { + $articles = Article::factory(5)->create(); + $posts = Post::factory(2)->create(); + $users = User::factory(2)->create(); - $this->assertCount(0, Article::whereHasLike($users[0])->get()); - $this->assertCount(0, Article::whereHasLike($users[1])->get()); + expect(Article::whereHasLike($users[0])->get())->toHaveCount(0); + expect(Article::whereHasLike($users[1])->get())->toHaveCount(0); - Like::add($articles[0], $users[0]); - $this->assertCount(1, Article::whereHasLike($users[0])->get()); - $this->assertCount(0, Article::whereHasLike($users[1])->get()); + Like::add($articles[0], $users[0]); + expect(Article::whereHasLike($users[0])->get())->toHaveCount(1); + expect(Article::whereHasLike($users[1])->get())->toHaveCount(0); - Like::add($articles[2], $users[0]); - $this->assertCount(2, Article::whereHasLike($users[0])->get()); - $this->assertCount(0, Article::whereHasLike($users[1])->get()); + Like::add($articles[2], $users[0]); + expect(Article::whereHasLike($users[0])->get())->toHaveCount(2); + expect(Article::whereHasLike($users[1])->get())->toHaveCount(0); - Like::add($articles[2], $users[1]); - $this->assertCount(2, Article::whereHasLike($users[0])->get()); - $this->assertCount(1, Article::whereHasLike($users[1])->get()); + Like::add($articles[2], $users[1]); + expect(Article::whereHasLike($users[0])->get())->toHaveCount(2); + expect(Article::whereHasLike($users[1])->get())->toHaveCount(1); - Like::add($posts[0], $users[0]); - $this->assertCount(2, Article::whereHasLike($users[0])->get()); - $this->assertCount(1, Article::whereHasLike($users[1])->get()); - } -} + Like::add($posts[0], $users[0]); + expect(Article::whereHasLike($users[0])->get())->toHaveCount(2); + expect(Article::whereHasLike($users[1])->get())->toHaveCount(1); +}); diff --git a/tests/MarkTest.php b/tests/MarkTest.php index 4deee35..ac0f391 100644 --- a/tests/MarkTest.php +++ b/tests/MarkTest.php @@ -1,7 +1,5 @@ create(); - $user = User::factory()->create(); - - Like::add($article, $user); - - $this->assertDatabaseHas((new Like)->getTable(), [ - 'user_id' => $user->getKey(), - 'markable_id' => $article->getKey(), - 'markable_type' => $article->getMorphClass(), - 'value' => null, - ]); - } - - /** @test */ - public function can_add_metadata() - { - $article = Article::factory()->create(); - $user = User::factory()->create(); - - Favorite::add($article, $user, null, [ - 'test_data' => true, - ]); - - $this->assertDatabaseHas((new Favorite)->getTable(), [ - 'user_id' => $user->getKey(), - 'markable_id' => $article->getKey(), - 'markable_type' => $article->getMorphClass(), - 'value' => null, - 'metadata' => json_encode(['test_data' => true]), - ]); - - Like::toggle($article, $user, null, [ - 'test_data' => true, - ]); - - $this->assertDatabaseHas((new Like)->getTable(), [ - 'user_id' => $user->getKey(), - 'markable_id' => $article->getKey(), - 'markable_type' => $article->getMorphClass(), - 'value' => null, - 'metadata' => json_encode(['test_data' => true]), - ]); - } - - /** @test */ - public function cannot_remove_with_an_invalid_markable_type_fail() - { - $user = User::factory()->create(); - - $this->expectException(InvalidMarkableInstanceException::class); - Like::remove($user, $user); - } - - /** @test */ - public function cannot_remove_an_unregistered_mark_fail() - { - $user = User::factory()->create(); - $post = Post::factory()->create(); - - $this->expectException(InvalidMarkableInstanceException::class); - Bookmark::remove($post, $user); - } - - /** @test */ - public function can_remove_a_mark() - { - $article = Article::factory()->create(); - $user = User::factory()->create(); - - Like::add($article, $user); - Like::remove($article, $user); - - $this->assertDatabaseMissing((new Like)->getTable(), [ - 'user_id' => $user->getKey(), - 'markable_id' => $article->getKey(), - 'markable_type' => $article->getMorphClass(), - ]); - } - - /** @test */ - public function can_count_marks() - { - $articles = Article::factory(2)->create(); - $post = Post::factory()->create(); - $users = User::factory(2)->create(); - - $this->assertEquals(0, Like::count($articles[0])); - $this->assertEquals(0, Like::count($articles[1])); - $this->assertEquals(0, Like::count($post)); - - Like::add($articles[0], $users[0]); - $this->assertEquals(1, Like::count($articles[0])); - $this->assertEquals(0, Like::count($articles[1])); - $this->assertEquals(0, Like::count($post)); - - Like::add($articles[0], $users[0]); - $this->assertEquals(1, Like::count($articles[0])); - $this->assertEquals(0, Like::count($articles[1])); - $this->assertEquals(0, Like::count($post)); - - Like::add($articles[0], $users[1]); - $this->assertEquals(2, Like::count($articles[0])); - $this->assertEquals(0, Like::count($articles[1])); - $this->assertEquals(0, Like::count($post)); - - Like::add($articles[1], $users[0]); - $this->assertEquals(2, Like::count($articles[0])); - $this->assertEquals(1, Like::count($articles[1])); - $this->assertEquals(0, Like::count($post)); - - Like::add($post, $users[0]); - $this->assertEquals(2, Like::count($articles[0])); - $this->assertEquals(1, Like::count($articles[1])); - $this->assertEquals(1, Like::count($post)); - } - - /** @test */ - public function can_check_if_user_has_mark() - { - $articles = Article::factory(2)->create(); - $post = Post::factory()->create(); - $users = User::factory(2)->create(); - - $this->assertFalse(Like::has($articles[0], $users[0])); - $this->assertFalse(Like::has($articles[0], $users[1])); - $this->assertFalse(Like::has($articles[1], $users[0])); - $this->assertFalse(Like::has($articles[1], $users[1])); - - Like::add($articles[0], $users[0]); - $this->assertTrue(Like::has($articles[0], $users[0])); - $this->assertFalse(Like::has($articles[0], $users[1])); - $this->assertFalse(Like::has($articles[1], $users[0])); - $this->assertFalse(Like::has($articles[1], $users[1])); - } - - /** @test */ - public function can_toggle_a_mark() - { - $article = Article::factory()->create(); - $user = User::factory()->create(); - - Like::toggle($article, $user); - - $this->assertDatabaseHas((new Like)->getTable(), [ - 'user_id' => $user->getKey(), - 'markable_id' => $article->getKey(), - 'markable_type' => $article->getMorphClass(), - 'value' => null, - ]); - - Like::toggle($article, $user); - - $this->assertDatabaseMissing((new Like)->getTable(), [ - 'user_id' => $user->getKey(), - 'markable_id' => $article->getKey(), - 'markable_type' => $article->getMorphClass(), - ]); - } - - /** @test */ - public function can_retrieve_markable_morph() - { - $article = Article::factory()->create(); - $user = User::factory()->create(); - - $like = Like::add($article, $user); - - $this->assertInstanceOf(Article::class, $like->markable); - - $this->assertEquals($article->getKey(), $like->markable->getKey()); - } - - /** @test */ - public function can_retrieve_user_relation() - { - $article = Article::factory()->create(); - $user = User::factory()->create(); - - $like = Like::add($article, $user); - - $this->assertInstanceOf(User::class, $like->user); - - $this->assertEquals($user->getKey(), $like->user->getKey()); - } -} +it('can add a mark', function () { + $article = Article::factory()->create(); + $user = User::factory()->create(); + + Like::add($article, $user); + + $this->assertDatabaseHas((new Like)->getTable(), [ + 'user_id' => $user->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + 'value' => null, + ]); +}); + +it('can add metadata', function () { + $article = Article::factory()->create(); + $user = User::factory()->create(); + + Favorite::add($article, $user, null, [ + 'test_data' => true, + ]); + + $this->assertDatabaseHas((new Favorite)->getTable(), [ + 'user_id' => $user->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + 'value' => null, + 'metadata' => json_encode(['test_data' => true]), + ]); + + Like::toggle($article, $user, null, [ + 'test_data' => true, + ]); + + $this->assertDatabaseHas((new Like)->getTable(), [ + 'user_id' => $user->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + 'value' => null, + 'metadata' => json_encode(['test_data' => true]), + ]); +}); + +it('cannot remove with an invalid markable type', function () { + $user = User::factory()->create(); + + expect(fn () => Like::remove($user, $user))->toThrow(InvalidMarkableInstanceException::class); +}); + +it('cannot remove an unregistered mark', function () { + $user = User::factory()->create(); + $post = Post::factory()->create(); + + expect(fn () => Bookmark::remove($post, $user))->toThrow(InvalidMarkableInstanceException::class); +}); + +it('can remove a mark', function () { + $article = Article::factory()->create(); + $user = User::factory()->create(); + + Like::add($article, $user); + Like::remove($article, $user); + + $this->assertDatabaseMissing((new Like)->getTable(), [ + 'user_id' => $user->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + ]); +}); + +it('can count marks', function () { + $articles = Article::factory(2)->create(); + $post = Post::factory()->create(); + $users = User::factory(2)->create(); + + expect(Like::count($articles[0]))->toEqual(0); + expect(Like::count($articles[1]))->toEqual(0); + expect(Like::count($post))->toEqual(0); + + Like::add($articles[0], $users[0]); + expect(Like::count($articles[0]))->toEqual(1); + expect(Like::count($articles[1]))->toEqual(0); + expect(Like::count($post))->toEqual(0); + + Like::add($articles[0], $users[0]); + expect(Like::count($articles[0]))->toEqual(1); + expect(Like::count($articles[1]))->toEqual(0); + expect(Like::count($post))->toEqual(0); + + Like::add($articles[0], $users[1]); + expect(Like::count($articles[0]))->toEqual(2); + expect(Like::count($articles[1]))->toEqual(0); + expect(Like::count($post))->toEqual(0); + + Like::add($articles[1], $users[0]); + expect(Like::count($articles[0]))->toEqual(2); + expect(Like::count($articles[1]))->toEqual(1); + expect(Like::count($post))->toEqual(0); + + Like::add($post, $users[0]); + expect(Like::count($articles[0]))->toEqual(2); + expect(Like::count($articles[1]))->toEqual(1); + expect(Like::count($post))->toEqual(1); +}); + +it('can check if user has mark', function () { + $articles = Article::factory(2)->create(); + $post = Post::factory()->create(); + $users = User::factory(2)->create(); + + expect(Like::has($articles[0], $users[0]))->toBeFalse(); + expect(Like::has($articles[0], $users[1]))->toBeFalse(); + expect(Like::has($articles[1], $users[0]))->toBeFalse(); + expect(Like::has($articles[1], $users[1]))->toBeFalse(); + + Like::add($articles[0], $users[0]); + expect(Like::has($articles[0], $users[0]))->toBeTrue(); + expect(Like::has($articles[0], $users[1]))->toBeFalse(); + expect(Like::has($articles[1], $users[0]))->toBeFalse(); + expect(Like::has($articles[1], $users[1]))->toBeFalse(); +}); + +it('can toggle a mark', function () { + $article = Article::factory()->create(); + $user = User::factory()->create(); + + Like::toggle($article, $user); + + $this->assertDatabaseHas((new Like)->getTable(), [ + 'user_id' => $user->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + 'value' => null, + ]); + + Like::toggle($article, $user); + + $this->assertDatabaseMissing((new Like)->getTable(), [ + 'user_id' => $user->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + ]); +}); + +it('can retrieve markable morph', function () { + $article = Article::factory()->create(); + $user = User::factory()->create(); + + $like = Like::add($article, $user); + + expect($like->markable)->toBeInstanceOf(Article::class); + expect($like->markable->getKey())->toEqual($article->getKey()); +}); + +it('can retrieve user relation', function () { + $article = Article::factory()->create(); + $user = User::factory()->create(); + + $like = Like::add($article, $user); + + expect($like->user)->toBeInstanceOf(User::class); + expect($like->user->getKey())->toEqual($user->getKey()); +}); diff --git a/tests/MarkableTest.php b/tests/MarkableTest.php index 7ed7103..ae9e4c1 100644 --- a/tests/MarkableTest.php +++ b/tests/MarkableTest.php @@ -1,7 +1,5 @@ expectException(InvalidMarkInstanceException::class); - - $markable = new ModelWithInvalidMark; - } - - /** @test */ - public function can_register_a_mark_in_markable_model() - { - $markable = new Article; - - $this->assertInstanceOf(Article::class, $markable); - - $this->assertContains(Markable::class, trait_uses_recursive($markable)); - } - - /** @test */ - public function can_filter_marked_models() - { - $articles = Article::factory(5)->create(); - $posts = Post::factory(2)->create(); - $users = User::factory(2)->create(); - - $this->assertCount(0, Article::whereHasMark(app(Like::class), $users[0])->get()); - $this->assertCount(0, Article::whereHasMark(app(Like::class), $users[1])->get()); - - Like::add($articles[0], $users[0]); - $this->assertCount(1, Article::whereHasMark(app(Like::class), $users[0])->get()); - $this->assertCount(0, Article::whereHasMark(app(Like::class), $users[1])->get()); - - Like::add($articles[2], $users[0]); - $this->assertCount(2, Article::whereHasMark(app(Like::class), $users[0])->get()); - $this->assertCount(0, Article::whereHasMark(app(Like::class), $users[1])->get()); - - Like::add($articles[2], $users[1]); - $this->assertCount(2, Article::whereHasMark(app(Like::class), $users[0])->get()); - $this->assertCount(1, Article::whereHasMark(app(Like::class), $users[1])->get()); - - Like::add($posts[0], $users[0]); - $this->assertCount(2, Article::whereHasMark(app(Like::class), $users[0])->get()); - $this->assertCount(1, Article::whereHasMark(app(Like::class), $users[1])->get()); - } - - /** @test */ - public function it_should_delete_marks_related_to_deleted_markables() - { - $article = Article::factory()->create(); - $user = User::factory()->create(); - - $mark = Like::add($article, $user); - - $this->assertDatabaseHas($mark->getTable(), [ - 'user_id' => $user->getKey(), - 'markable_id' => $article->getKey(), - 'markable_type' => $article->getMorphClass(), - ]); - - $article->delete(); - - $this->assertDatabaseMissing($mark->getTable(), [ - 'user_id' => $user->getKey(), - 'markable_id' => $article->getKey(), - 'markable_type' => $article->getMorphClass(), - ]); - } - - /** @test */ - public function it_should_retrieve_the_list_of_users_who_marked_an_entity() - { - $article = Article::factory()->create(); - $users = User::factory(2)->create(); - - Like::add($article, $users[0]); - Like::add($article, $users[1]); - - $this->assertEquals([ - $users[0]->getKey(), - $users[1]->getKey(), - ], $article->likers->modelKeys()); - } - - /** @test */ - public function it_should_retrieve_the_list_of_marks_of_an_entity() - { - $article = Article::factory()->create(); - $users = User::factory(2)->create(); - - $like1 = Like::add($article, $users[0]); - $like2 = Like::add($article, $users[1]); - - $this->assertEquals([ - $like1->getKey(), - $like2->getKey(), - ], $article->likes->modelKeys()); - } -} +it('cannot register an invalid mark in markable model', function () { + expect(fn () => new ModelWithInvalidMark)->toThrow(InvalidMarkInstanceException::class); +}); + +it('can register a mark in markable model', function () { + $markable = new Article; + + expect($markable)->toBeInstanceOf(Article::class); + expect(trait_uses_recursive($markable))->toContain(Markable::class); +}); + +it('can filter marked models', function () { + $articles = Article::factory(5)->create(); + $posts = Post::factory(2)->create(); + $users = User::factory(2)->create(); + + expect(Article::whereHasMark(app(Like::class), $users[0])->get())->toHaveCount(0); + expect(Article::whereHasMark(app(Like::class), $users[1])->get())->toHaveCount(0); + + Like::add($articles[0], $users[0]); + expect(Article::whereHasMark(app(Like::class), $users[0])->get())->toHaveCount(1); + expect(Article::whereHasMark(app(Like::class), $users[1])->get())->toHaveCount(0); + + Like::add($articles[2], $users[0]); + expect(Article::whereHasMark(app(Like::class), $users[0])->get())->toHaveCount(2); + expect(Article::whereHasMark(app(Like::class), $users[1])->get())->toHaveCount(0); + + Like::add($articles[2], $users[1]); + expect(Article::whereHasMark(app(Like::class), $users[0])->get())->toHaveCount(2); + expect(Article::whereHasMark(app(Like::class), $users[1])->get())->toHaveCount(1); + + Like::add($posts[0], $users[0]); + expect(Article::whereHasMark(app(Like::class), $users[0])->get())->toHaveCount(2); + expect(Article::whereHasMark(app(Like::class), $users[1])->get())->toHaveCount(1); +}); + +it('should delete marks related to deleted markables', function () { + $article = Article::factory()->create(); + $user = User::factory()->create(); + + $mark = Like::add($article, $user); + + $this->assertDatabaseHas($mark->getTable(), [ + 'user_id' => $user->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + ]); + + $article->delete(); + + $this->assertDatabaseMissing($mark->getTable(), [ + 'user_id' => $user->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + ]); +}); + +it('should retrieve the list of users who marked an entity', function () { + $article = Article::factory()->create(); + $users = User::factory(2)->create(); + + Like::add($article, $users[0]); + Like::add($article, $users[1]); + + expect($article->likers->modelKeys())->toEqual([ + $users[0]->getKey(), + $users[1]->getKey(), + ]); +}); + +it('should retrieve the list of marks of an entity', function () { + $article = Article::factory()->create(); + $users = User::factory(2)->create(); + + $like1 = Like::add($article, $users[0]); + $like2 = Like::add($article, $users[1]); + + expect($article->likes->modelKeys())->toEqual([ + $like1->getKey(), + $like2->getKey(), + ]); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..3dfe5fc --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,5 @@ +in(__DIR__); diff --git a/tests/ReactionTest.php b/tests/ReactionTest.php index 8639157..3e844fc 100644 --- a/tests/ReactionTest.php +++ b/tests/ReactionTest.php @@ -1,203 +1,260 @@ create(); - $user = User::factory()->create(); - - $this->expectException(InvalidMarkValueException::class); - Reaction::add($article, $user); - } - - /** @test */ - public function cannot_add_an_invalid_reaction_value_fails() - { - $article = Article::factory()->create(); - $user = User::factory()->create(); - - $this->expectException(InvalidMarkValueException::class); - Reaction::add($article, $user, 'not_valid_value'); - } - - /** @test */ - public function can_add_any_value_with_wildcard() - { - config()->set('markable.allowed_values.reaction', '*'); - - $article = Article::factory()->create(); - $user = User::factory()->create(); - $table = (new Reaction)->getTable(); - - Reaction::add($article, $user, 'random_value'); - $this->assertDatabaseCount($table, 1); - $this->assertDatabaseHas($table, [ - 'user_id' => $user->getKey(), - 'markable_id' => $article->getKey(), - 'markable_type' => $article->getMorphClass(), - 'value' => 'random_value', - ]); - } - - /** @test */ - public function can_add_a_reaction() - { - $article = Article::factory()->create(); - $users = User::factory(2)->create(); - $table = (new Reaction)->getTable(); - - Reaction::add($article, $users[0], 'reaction_1'); - $this->assertDatabaseCount($table, 1); - $this->assertDatabaseHas($table, [ - 'user_id' => $users[0]->getKey(), - 'markable_id' => $article->getKey(), - 'markable_type' => $article->getMorphClass(), - 'value' => 'reaction_1', - ]); - - Reaction::add($article, $users[0], 'reaction_2'); - $this->assertDatabaseCount($table, 2); - $this->assertDatabaseHas($table, [ - 'user_id' => $users[0]->getKey(), - 'markable_id' => $article->getKey(), - 'markable_type' => $article->getMorphClass(), - 'value' => 'reaction_2', - ]); - - Reaction::add($article, $users[0], 'reaction_2'); - $this->assertDatabaseCount($table, 2); - - Reaction::add($article, $users[1], 'reaction_2'); - $this->assertDatabaseCount($table, 3); - $this->assertDatabaseHas($table, [ - 'user_id' => $users[1]->getKey(), - 'markable_id' => $article->getKey(), - 'markable_type' => $article->getMorphClass(), - 'value' => 'reaction_2', - ]); - } - - /** @test */ - public function can_remove_a_reaction() - { - $article = Article::factory()->create(); - $user = User::factory()->create(); - $table = (new Reaction)->getTable(); - - Reaction::add($article, $user, 'reaction_1'); - $this->assertDatabaseCount($table, 1); - - Reaction::remove($article, $user, 'not_valid_value'); - $this->assertDatabaseCount($table, 1); - - Reaction::remove($article, $user, 'reaction_1'); - $this->assertDatabaseCount($table, 0); - } - - /** @test */ - public function can_count_reactions() - { - $article = Article::factory()->create(); - $users = User::factory(2)->create(); - $table = (new Reaction)->getTable(); - - $this->assertEquals(0, Reaction::count($article, 'reaction_1')); - - Reaction::add($article, $users[0], 'reaction_1'); - $this->assertEquals(1, Reaction::count($article, 'reaction_1')); - $this->assertEquals(0, Reaction::count($article, 'reaction_2')); - - Reaction::add($article, $users[0], 'reaction_1'); - Reaction::add($article, $users[1], 'reaction_1'); - $this->assertEquals(2, Reaction::count($article, 'reaction_1')); - $this->assertEquals(0, Reaction::count($article, 'reaction_2')); - } - - /** @test */ - public function can_check_if_user_has_reaction() - { - $article = Article::factory()->create(); - $users = User::factory(2)->create(); - $table = (new Reaction)->getTable(); - - $this->assertFalse(Reaction::has($article, $users[0], 'reaction_1')); - $this->assertFalse(Reaction::has($article, $users[1], 'reaction_1')); - - Reaction::add($article, $users[0], 'reaction_1'); - $this->assertTrue(Reaction::has($article, $users[0], 'reaction_1')); - $this->assertFalse(Reaction::has($article, $users[1], 'reaction_1')); - } - - /** @test */ - public function cannot_toggle_an_invalid_reaction_value_null_fails() - { - $article = Article::factory()->create(); - $user = User::factory()->create(); - - $this->expectException(InvalidMarkValueException::class); - Reaction::toggle($article, $user); - } - - /** @test */ - public function cannot_toggle_an_invalid_reaction_value_fails() - { - $article = Article::factory()->create(); - $user = User::factory()->create(); - - $this->expectException(InvalidMarkValueException::class); - Reaction::toggle($article, $user, 'not_valid_value'); - } - - /** @test */ - public function can_toggle_a_reaction() - { - $article = Article::factory()->create(); - $users = User::factory(2)->create(); - $table = (new Reaction)->getTable(); - - Reaction::toggle($article, $users[0], 'reaction_1'); - $this->assertDatabaseCount($table, 1); - $this->assertDatabaseHas($table, [ - 'user_id' => $users[0]->getKey(), - 'markable_id' => $article->getKey(), - 'markable_type' => $article->getMorphClass(), - 'value' => 'reaction_1', - ]); - - Reaction::toggle($article, $users[0], 'reaction_2'); - $this->assertDatabaseCount($table, 2); - $this->assertDatabaseHas($table, [ - 'user_id' => $users[0]->getKey(), - 'markable_id' => $article->getKey(), - 'markable_type' => $article->getMorphClass(), - 'value' => 'reaction_2', - ]); - - Reaction::toggle($article, $users[0], 'reaction_2'); - $this->assertDatabaseCount($table, 1); - $this->assertDatabaseMissing($table, [ - 'user_id' => $users[0]->getKey(), - 'markable_id' => $article->getKey(), - 'markable_type' => $article->getMorphClass(), - 'value' => 'reaction_2', - ]); - - Reaction::toggle($article, $users[1], 'reaction_3'); - $this->assertDatabaseCount($table, 2); - $this->assertDatabaseHas($table, [ - 'user_id' => $users[1]->getKey(), - 'markable_id' => $article->getKey(), - 'markable_type' => $article->getMorphClass(), - 'value' => 'reaction_3', - ]); - } -} +it('cannot add an invalid reaction value null', function () { + $article = Article::factory()->create(); + $user = User::factory()->create(); + + expect(fn () => Reaction::add($article, $user))->toThrow(InvalidMarkValueException::class); +}); + +it('cannot add an invalid reaction value', function () { + $article = Article::factory()->create(); + $user = User::factory()->create(); + + expect(fn () => Reaction::add($article, $user, 'not_valid_value'))->toThrow(InvalidMarkValueException::class); +}); + +it('can add any value with wildcard', function () { + config()->set('markable.allowed_values.reaction', '*'); + + $article = Article::factory()->create(); + $user = User::factory()->create(); + $table = (new Reaction)->getTable(); + + Reaction::add($article, $user, 'random_value'); + $this->assertDatabaseCount($table, 1); + $this->assertDatabaseHas($table, [ + 'user_id' => $user->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + 'value' => 'random_value', + ]); +}); + +it('can add a reaction', function () { + $article = Article::factory()->create(); + $users = User::factory(2)->create(); + $table = (new Reaction)->getTable(); + + Reaction::add($article, $users[0], 'reaction_1'); + $this->assertDatabaseCount($table, 1); + $this->assertDatabaseHas($table, [ + 'user_id' => $users[0]->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + 'value' => 'reaction_1', + ]); + + Reaction::add($article, $users[0], 'reaction_2'); + $this->assertDatabaseCount($table, 2); + $this->assertDatabaseHas($table, [ + 'user_id' => $users[0]->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + 'value' => 'reaction_2', + ]); + + Reaction::add($article, $users[0], 'reaction_2'); + $this->assertDatabaseCount($table, 2); + + Reaction::add($article, $users[1], 'reaction_2'); + $this->assertDatabaseCount($table, 3); + $this->assertDatabaseHas($table, [ + 'user_id' => $users[1]->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + 'value' => 'reaction_2', + ]); +}); + +it('can remove a reaction', function () { + $article = Article::factory()->create(); + $user = User::factory()->create(); + $table = (new Reaction)->getTable(); + + Reaction::add($article, $user, 'reaction_1'); + $this->assertDatabaseCount($table, 1); + + Reaction::remove($article, $user, 'not_valid_value'); + $this->assertDatabaseCount($table, 1); + + Reaction::remove($article, $user, 'reaction_1'); + $this->assertDatabaseCount($table, 0); +}); + +it('can count reactions', function () { + $article = Article::factory()->create(); + $users = User::factory(2)->create(); + + expect(Reaction::count($article, 'reaction_1'))->toEqual(0); + + Reaction::add($article, $users[0], 'reaction_1'); + expect(Reaction::count($article, 'reaction_1'))->toEqual(1); + expect(Reaction::count($article, 'reaction_2'))->toEqual(0); + + Reaction::add($article, $users[0], 'reaction_1'); + Reaction::add($article, $users[1], 'reaction_1'); + expect(Reaction::count($article, 'reaction_1'))->toEqual(2); + expect(Reaction::count($article, 'reaction_2'))->toEqual(0); +}); + +it('can check if user has reaction', function () { + $article = Article::factory()->create(); + $users = User::factory(2)->create(); + + expect(Reaction::has($article, $users[0], 'reaction_1'))->toBeFalse(); + expect(Reaction::has($article, $users[1], 'reaction_1'))->toBeFalse(); + + Reaction::add($article, $users[0], 'reaction_1'); + expect(Reaction::has($article, $users[0], 'reaction_1'))->toBeTrue(); + expect(Reaction::has($article, $users[1], 'reaction_1'))->toBeFalse(); +}); + +it('cannot toggle an invalid reaction value null', function () { + $article = Article::factory()->create(); + $user = User::factory()->create(); + + expect(fn () => Reaction::toggle($article, $user))->toThrow(InvalidMarkValueException::class); +}); + +it('cannot toggle an invalid reaction value', function () { + $article = Article::factory()->create(); + $user = User::factory()->create(); + + expect(fn () => Reaction::toggle($article, $user, 'not_valid_value'))->toThrow(InvalidMarkValueException::class); +}); + +it('can add a reaction with backed enum', function () { + $article = Article::factory()->create(); + $user = User::factory()->create(); + $table = (new Reaction)->getTable(); + + Reaction::add($article, $user, ReactionType::Like); + $this->assertDatabaseCount($table, 1); + $this->assertDatabaseHas($table, [ + 'user_id' => $user->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + 'value' => ReactionType::Like->value, + ]); +}); + +it('can has a reaction with backed enum', function () { + $article = Article::factory()->create(); + $user = User::factory()->create(); + + expect(Reaction::has($article, $user, ReactionType::Like))->toBeFalse(); + + Reaction::add($article, $user, ReactionType::Like); + expect(Reaction::has($article, $user, ReactionType::Like))->toBeTrue(); + expect(Reaction::has($article, $user, ReactionType::Love))->toBeFalse(); +}); + +it('can remove a reaction with backed enum', function () { + $article = Article::factory()->create(); + $user = User::factory()->create(); + $table = (new Reaction)->getTable(); + + Reaction::add($article, $user, ReactionType::Like); + $this->assertDatabaseCount($table, 1); + + Reaction::remove($article, $user, ReactionType::Like); + $this->assertDatabaseCount($table, 0); +}); + +it('can toggle a reaction with backed enum', function () { + $article = Article::factory()->create(); + $user = User::factory()->create(); + $table = (new Reaction)->getTable(); + + Reaction::toggle($article, $user, ReactionType::Like); + $this->assertDatabaseCount($table, 1); + expect(Reaction::has($article, $user, ReactionType::Like))->toBeTrue(); + + Reaction::toggle($article, $user, ReactionType::Like); + $this->assertDatabaseCount($table, 0); + expect(Reaction::has($article, $user, ReactionType::Like))->toBeFalse(); +}); + +it('can count reactions with backed enum', function () { + $article = Article::factory()->create(); + $users = User::factory(2)->create(); + + expect(Reaction::count($article, ReactionType::Like))->toEqual(0); + + Reaction::add($article, $users[0], ReactionType::Like); + Reaction::add($article, $users[1], ReactionType::Like); + expect(Reaction::count($article, ReactionType::Like))->toEqual(2); + expect(Reaction::count($article, ReactionType::Love))->toEqual(0); +}); + +it('can use enum class as allowed values', function () { + config()->set('markable.allowed_values.reaction', ReactionType::class); + + $article = Article::factory()->create(); + $user = User::factory()->create(); + $table = (new Reaction)->getTable(); + + Reaction::add($article, $user, ReactionType::Like); + $this->assertDatabaseCount($table, 1); + $this->assertDatabaseHas($table, [ + 'value' => ReactionType::Like->value, + ]); +}); + +it('cannot add invalid value when allowed values is enum class', function () { + config()->set('markable.allowed_values.reaction', ReactionType::class); + + $article = Article::factory()->create(); + $user = User::factory()->create(); + + expect(fn () => Reaction::add($article, $user, 'not_a_valid_enum_value'))->toThrow(InvalidMarkValueException::class); +}); + +it('can toggle a reaction', function () { + $article = Article::factory()->create(); + $users = User::factory(2)->create(); + $table = (new Reaction)->getTable(); + + Reaction::toggle($article, $users[0], 'reaction_1'); + $this->assertDatabaseCount($table, 1); + $this->assertDatabaseHas($table, [ + 'user_id' => $users[0]->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + 'value' => 'reaction_1', + ]); + + Reaction::toggle($article, $users[0], 'reaction_2'); + $this->assertDatabaseCount($table, 2); + $this->assertDatabaseHas($table, [ + 'user_id' => $users[0]->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + 'value' => 'reaction_2', + ]); + + Reaction::toggle($article, $users[0], 'reaction_2'); + $this->assertDatabaseCount($table, 1); + $this->assertDatabaseMissing($table, [ + 'user_id' => $users[0]->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + 'value' => 'reaction_2', + ]); + + Reaction::toggle($article, $users[1], 'reaction_3'); + $this->assertDatabaseCount($table, 2); + $this->assertDatabaseHas($table, [ + 'user_id' => $users[1]->getKey(), + 'markable_id' => $article->getKey(), + 'markable_type' => $article->getMorphClass(), + 'value' => 'reaction_3', + ]); +});