From d2ced8db7075574e36cdc48adb4a73011edd092e Mon Sep 17 00:00:00 2001 From: Kyrch Date: Sun, 15 Feb 2026 16:35:04 -0300 Subject: [PATCH] tests: add mutation tests (#1100) --- app/GraphQL/Middleware/AuthMutation.php | 24 +++ .../Wiki/Anime/AnimeYearsResolver.php | 2 +- app/GraphQL/Schema/Mutations/BaseMutation.php | 2 + .../SyncExternalProfileMutation.php | 18 --- .../List/Playlist/PlaylistTrackPolicy.php | 12 +- phpstan.neon | 3 + .../Playlist/CreatePlaylistMutationTest.php | 78 ++++++++++ .../Playlist/DeletePlaylistMutationTest.php | 95 ++++++++++++ .../Track/CreatePlaylistTrackMutationTest.php | 121 +++++++++++++++ .../Track/DeletePlaylistTrackMutationTest.php | 107 +++++++++++++ .../Track/UpdatePlaylistTrackMutationTest.php | 131 ++++++++++++++++ .../Playlist/UpdatePlaylistMutationTest.php | 109 ++++++++++++++ .../Models/User/ToggleLikeMutationTest.php | 141 ++++++++++++++++++ .../Models/User/WatchMutationTest.php | 114 ++++++++++++++ .../Queries/Wiki/AnimeYearsQueryTest.php | 82 +++++----- .../Wiki/FindAnimeByExternalSiteQueryTest.php | 13 +- 16 files changed, 978 insertions(+), 74 deletions(-) create mode 100644 app/GraphQL/Middleware/AuthMutation.php create mode 100644 tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/CreatePlaylistMutationTest.php create mode 100644 tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/DeletePlaylistMutationTest.php create mode 100644 tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/Track/CreatePlaylistTrackMutationTest.php create mode 100644 tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/Track/DeletePlaylistTrackMutationTest.php create mode 100644 tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/Track/UpdatePlaylistTrackMutationTest.php create mode 100644 tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/UpdatePlaylistMutationTest.php create mode 100644 tests/Feature/GraphQL/Schema/Mutations/Models/User/ToggleLikeMutationTest.php create mode 100644 tests/Feature/GraphQL/Schema/Mutations/Models/User/WatchMutationTest.php diff --git a/app/GraphQL/Middleware/AuthMutation.php b/app/GraphQL/Middleware/AuthMutation.php new file mode 100644 index 000000000..4ad80409a --- /dev/null +++ b/app/GraphQL/Middleware/AuthMutation.php @@ -0,0 +1,24 @@ + $args + */ + public function handle($root, array $args, $context, ResolveInfo $resolveInfo, Closure $next) + { + throw_unless(Auth::check(), AuthorizationError::class, 'Unauthenticated.'); + + return $next($root, $args, $context, $resolveInfo); + } +} diff --git a/app/GraphQL/Resolvers/Wiki/Anime/AnimeYearsResolver.php b/app/GraphQL/Resolvers/Wiki/Anime/AnimeYearsResolver.php index cb91597aa..487ef5a13 100644 --- a/app/GraphQL/Resolvers/Wiki/Anime/AnimeYearsResolver.php +++ b/app/GraphQL/Resolvers/Wiki/Anime/AnimeYearsResolver.php @@ -35,7 +35,7 @@ class AnimeYearsResolver extends BaseResolver $fieldSelection = $resolveInfo->getFieldSelection(1); - Validator::make(['year' => $year], [new AnimeYearRule($fieldSelection)]) + Validator::make(['year' => $year], ['year' => new AnimeYearRule($fieldSelection)]) ->validate(); return Anime::query() diff --git a/app/GraphQL/Schema/Mutations/BaseMutation.php b/app/GraphQL/Schema/Mutations/BaseMutation.php index 5a2bf04ca..9a0b8aa12 100644 --- a/app/GraphQL/Schema/Mutations/BaseMutation.php +++ b/app/GraphQL/Schema/Mutations/BaseMutation.php @@ -6,6 +6,7 @@ namespace App\GraphQL\Schema\Mutations; use App\Concerns\GraphQL\ResolvesArguments; use App\GraphQL\Argument\Argument; +use App\GraphQL\Middleware\AuthMutation; use App\GraphQL\Middleware\ResolveBindableArgs; use App\GraphQL\Schema\Types\BaseType; use App\GraphQL\Schema\Unions\BaseUnion; @@ -27,6 +28,7 @@ abstract class BaseMutation extends Mutation $this->middleware = array_merge( $this->middleware, [ + AuthMutation::class, ResolveBindableArgs::class, ], ); diff --git a/app/GraphQL/Schema/Mutations/Models/List/ExternalProfile/SyncExternalProfileMutation.php b/app/GraphQL/Schema/Mutations/Models/List/ExternalProfile/SyncExternalProfileMutation.php index 53adde65e..0841a37f8 100644 --- a/app/GraphQL/Schema/Mutations/Models/List/ExternalProfile/SyncExternalProfileMutation.php +++ b/app/GraphQL/Schema/Mutations/Models/List/ExternalProfile/SyncExternalProfileMutation.php @@ -5,37 +5,19 @@ declare(strict_types=1); namespace App\GraphQL\Schema\Mutations\Models\List\ExternalProfile; use App\Contracts\GraphQL\Fields\BindableField; -use App\Features\AllowExternalProfileManagement; use App\GraphQL\Argument\Argument; use App\GraphQL\Resolvers\List\SyncExternalProfileResolver; use App\GraphQL\Schema\Fields\Field; use App\GraphQL\Schema\Mutations\BaseMutation; use App\GraphQL\Schema\Types\List\ExternalProfileType; -use App\Http\Middleware\Api\EnabledOnlyOnLocalhost; use GraphQL\Type\Definition\ResolveInfo; use GraphQL\Type\Definition\Type; use Illuminate\Support\Facades\App; -use Illuminate\Support\Str; -use Laravel\Pennant\Middleware\EnsureFeaturesAreActive; class SyncExternalProfileMutation extends BaseMutation { - protected $middleware = [ - EnabledOnlyOnLocalhost::class, - ]; - public function __construct() { - $this->middleware = array_merge( - $this->middleware, - [ - Str::of(EnsureFeaturesAreActive::class) - ->append(':') - ->append(AllowExternalProfileManagement::class) - ->__toString(), - ] - ); - parent::__construct('SyncExternalProfile'); } diff --git a/app/Policies/List/Playlist/PlaylistTrackPolicy.php b/app/Policies/List/Playlist/PlaylistTrackPolicy.php index fa2038697..48ff03b78 100644 --- a/app/Policies/List/Playlist/PlaylistTrackPolicy.php +++ b/app/Policies/List/Playlist/PlaylistTrackPolicy.php @@ -67,7 +67,7 @@ class PlaylistTrackPolicy extends BasePolicy } /** - * @param Playlist $playlist + * @param Playlist|null $playlist */ public function create(User $user, $playlist = null): Response { @@ -87,8 +87,9 @@ class PlaylistTrackPolicy extends BasePolicy /** * @param PlaylistTrack $track + * @param Playlist|null $playlist */ - public function update(User $user, Model $track): Response + public function update(User $user, Model $track, $playlist = null): Response { if (Filament::isServing()) { return $user->hasRole(RoleEnum::ADMIN->value) @@ -97,7 +98,7 @@ class PlaylistTrackPolicy extends BasePolicy } /** @var Playlist|null $playlist */ - $playlist = request()->route('playlist'); + $playlist ??= request()->route('playlist'); return $playlist?->user()->is($user) && parent::update($user, $track)->allowed() ? Response::allow() @@ -106,8 +107,9 @@ class PlaylistTrackPolicy extends BasePolicy /** * @param PlaylistTrack $track + * @param Playlist|null $playlist */ - public function delete(User $user, Model $track): Response + public function delete(User $user, Model $track, $playlist = null): Response { if (Filament::isServing()) { return $user->hasRole(RoleEnum::ADMIN->value) @@ -116,7 +118,7 @@ class PlaylistTrackPolicy extends BasePolicy } /** @var Playlist|null $playlist */ - $playlist = request()->route('playlist'); + $playlist ??= request()->route('playlist'); return $playlist?->user()->is($user) && parent::delete($user, $track)->allowed() ? Response::allow() diff --git a/phpstan.neon b/phpstan.neon index f5ea0752e..46de32a23 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -54,3 +54,6 @@ parameters: - message: '#Call to method assertActionDoesNotExist\(\) on an unknown class static.#' path: tests/Unit/Filament/* + - + message: '#Access to an undefined property PHPUnit\\Framework\\TestCase::\$mutation.#' + path: tests/Feature/GraphQL/* diff --git a/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/CreatePlaylistMutationTest.php b/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/CreatePlaylistMutationTest.php new file mode 100644 index 000000000..f7fc86df9 --- /dev/null +++ b/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/CreatePlaylistMutationTest.php @@ -0,0 +1,78 @@ +mutation = ' + mutation($name: String!, $visibility: PlaylistVisibility!, $description: String) { + CreatePlaylist(name: $name, visibility: $visibility, description: $description) { + name + visibility + description + } + } + '; +}); + +test('protected', function () { + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'name' => fake()->word(), + 'visibility' => Arr::random(PlaylistVisibility::cases())->name, + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +test('forbidden', function () { + actingAs(User::factory()->createOne()); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'name' => fake()->word(), + 'visibility' => Arr::random(PlaylistVisibility::cases())->name, + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +it('creates', function () { + Event::fakeExcept(PlaylistCreated::class); + + $user = User::factory() + ->withPermissions(CrudPermission::CREATE->format(Playlist::class)) + ->createOne(); + + actingAs($user); + + $playlist = Playlist::factory()->makeOne(); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'name' => $playlist->name, + 'visibility' => $playlist->visibility->name, + 'description' => $playlist->description, + ], + ]); + + $this->assertDatabaseCount(Playlist::class, 1); + $response->assertOk(); + $response->assertJsonIsObject('data.CreatePlaylist'); +}); diff --git a/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/DeletePlaylistMutationTest.php b/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/DeletePlaylistMutationTest.php new file mode 100644 index 000000000..916a4d674 --- /dev/null +++ b/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/DeletePlaylistMutationTest.php @@ -0,0 +1,95 @@ +mutation = ' + mutation($id: String!) { + DeletePlaylist(id: $id) { + message + } + } + '; +}); + +test('protected', function () { + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'id' => fake()->word(), + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +test('forbidden', function () { + Event::fakeExcept(PlaylistCreated::class); + + actingAs(User::factory()->createOne()); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + // Needed for the bind resolver. + 'id' => Playlist::factory()->createOne()->hashid, + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +test('forbidden if not owner', function () { + Event::fakeExcept(PlaylistCreated::class); + + $user = User::factory() + ->withPermissions(CrudPermission::DELETE->format(Playlist::class)) + ->createOne(); + + actingAs($user); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'id' => Playlist::factory()->createOne()->hashid, + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +it('deletes', function () { + Event::fakeExcept(PlaylistCreated::class); + + $user = User::factory() + ->withPermissions(CrudPermission::DELETE->format(Playlist::class)) + ->createOne(); + + actingAs($user); + + $playlist = Playlist::factory() + ->for($user) + ->createOne(); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'id' => $playlist->hashid, + ], + ]); + + $this->assertDatabaseCount(Playlist::class, 0); + $response->assertOk(); + $this->assertIsString($response->json('data.DeletePlaylist.message')); +}); diff --git a/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/Track/CreatePlaylistTrackMutationTest.php b/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/Track/CreatePlaylistTrackMutationTest.php new file mode 100644 index 000000000..2d515498f --- /dev/null +++ b/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/Track/CreatePlaylistTrackMutationTest.php @@ -0,0 +1,121 @@ +mutation = ' + mutation($playlist: String!, $entryId: Int!, $videoId: Int!) { + CreatePlaylistTrack(playlist: $playlist, entryId: $entryId, videoId: $videoId) { + id + } + } + '; +}); + +test('protected', function () { + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'playlist' => fake()->word(), + 'entryId' => fake()->randomDigitNotNull(), + 'videoId' => fake()->randomDigitNotNull(), + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +test('forbidden', function () { + Event::fakeExcept(PlaylistCreated::class); + + actingAs(User::factory()->createOne()); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + // Needed for the bind resolver. + 'playlist' => Playlist::factory()->createOne()->hashid, + 'entryId' => AnimeThemeEntry::factory()->createOne()->getKey(), + 'videoId' => Video::factory()->createOne()->getKey(), + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +it('fails if no entry video link', function () { + Event::fakeExcept([PlaylistCreated::class, TrackCreated::class]); + + $user = User::factory() + ->withPermissions(CrudPermission::CREATE->format(PlaylistTrack::class)) + ->createOne(); + + actingAs($user); + + $entry = AnimeThemeEntry::factory()->createOne(); + $video = Video::factory()->createOne(); + + $playlist = Playlist::factory() + ->for($user) + ->createOne(); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'playlist' => $playlist->hashid, + 'entryId' => $entry->getKey(), + 'videoId' => $video->getKey(), + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'validation'); + $this->assertArrayHasKey('entryId', $response->json('errors.0.extensions.validation')); + $this->assertArrayHasKey('videoId', $response->json('errors.0.extensions.validation')); +}); + +it('creates', function () { + Event::fakeExcept([PlaylistCreated::class, TrackCreated::class]); + + $user = User::factory() + ->withPermissions(CrudPermission::CREATE->format(PlaylistTrack::class)) + ->createOne(); + + actingAs($user); + + $entry = AnimeThemeEntry::factory()->createOne(); + $video = Video::factory()->createOne(); + + $entry->videos()->attach($video); + + $playlist = Playlist::factory() + ->for($user) + ->createOne(); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'playlist' => $playlist->hashid, + 'entryId' => $entry->getKey(), + 'videoId' => $video->getKey(), + ], + ]); + + $this->assertDatabaseCount(PlaylistTrack::class, 1); + $response->assertOk(); + $response->assertJsonIsObject('data.CreatePlaylistTrack'); +}); diff --git a/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/Track/DeletePlaylistTrackMutationTest.php b/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/Track/DeletePlaylistTrackMutationTest.php new file mode 100644 index 000000000..b4eb555d5 --- /dev/null +++ b/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/Track/DeletePlaylistTrackMutationTest.php @@ -0,0 +1,107 @@ +mutation = ' + mutation($playlist: String!, $id: String!) { + DeletePlaylistTrack(playlist: $playlist, id: $id) { + message + } + } + '; +}); + +test('protected', function () { + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'playlist' => fake()->word(), + 'id' => fake()->word(), + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +test('forbidden', function () { + Event::fakeExcept([PlaylistCreated::class, TrackCreated::class]); + + actingAs(User::factory()->createOne()); + + $playlist = Playlist::factory()->createOne(); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + // Needed for the bind resolver. + 'playlist' => $playlist->hashid, + 'id' => PlaylistTrack::factory()->for($playlist)->createOne()->hashid, + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +test('forbidden if not owner', function () { + Event::fakeExcept([PlaylistCreated::class, TrackCreated::class]); + + $user = User::factory() + ->withPermissions(CrudPermission::DELETE->format(PlaylistTrack::class)) + ->createOne(); + + actingAs($user); + + $track = PlaylistTrack::factory() + ->for(Playlist::factory()) + ->createOne(); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'playlist' => $track->playlist->hashid, + 'id' => $track->hashid, + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +it('deletes', function () { + Event::fakeExcept([PlaylistCreated::class, TrackCreated::class]); + + $user = User::factory() + ->withPermissions(CrudPermission::DELETE->format(PlaylistTrack::class)) + ->createOne(); + + actingAs($user); + + $track = PlaylistTrack::factory() + ->for(Playlist::factory()->for($user)) + ->createOne(); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'playlist' => $track->playlist->hashid, + 'id' => $track->hashid, + ], + ]); + + $this->assertDatabaseCount(PlaylistTrack::class, 0); + $response->assertOk(); + $this->assertIsString($response->json('data.DeletePlaylistTrack.message')); +}); diff --git a/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/Track/UpdatePlaylistTrackMutationTest.php b/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/Track/UpdatePlaylistTrackMutationTest.php new file mode 100644 index 000000000..f9e611255 --- /dev/null +++ b/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/Track/UpdatePlaylistTrackMutationTest.php @@ -0,0 +1,131 @@ +mutation = ' + mutation($playlist: String!, $id: String!, $entryId: Int, $videoId: Int) { + UpdatePlaylistTrack(playlist: $playlist, id: $id, entryId: $entryId, videoId: $videoId) { + animethemeentry { + id + } + video { + id + } + } + } + '; +}); + +test('protected', function () { + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'playlist' => fake()->word(), + 'id' => fake()->word(), + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +test('forbidden', function () { + Event::fakeExcept([PlaylistCreated::class, TrackCreated::class]); + + actingAs(User::factory()->createOne()); + + $playlist = Playlist::factory()->createOne(); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + // Needed for the bind resolver. + 'playlist' => $playlist->hashid, + 'id' => PlaylistTrack::factory()->for($playlist)->createOne()->hashid, + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +test('forbidden if not owner', function () { + Event::fakeExcept([PlaylistCreated::class, TrackCreated::class]); + + $user = User::factory() + ->withPermissions(CrudPermission::UPDATE->format(PlaylistTrack::class)) + ->createOne(); + + actingAs($user); + + $track = PlaylistTrack::factory() + ->for(Playlist::factory()) + ->createOne(); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'playlist' => $track->playlist->hashid, + 'id' => $track->hashid, + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +it('updates', function () { + Event::fakeExcept([PlaylistCreated::class, TrackCreated::class]); + + $user = User::factory() + ->withPermissions(CrudPermission::UPDATE->format(PlaylistTrack::class)) + ->createOne(); + + actingAs($user); + + $track = PlaylistTrack::factory() + ->for(Playlist::factory()->for($user)) + ->createOne(); + + $entry = AnimeThemeEntry::factory()->createOne(); + $video = Video::factory()->createOne(); + + $entry->videos()->attach($video); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'playlist' => $track->playlist->hashid, + 'id' => $track->hashid, + 'entryId' => $entry->getKey(), + 'videoId' => $video->getKey(), + ], + ]); + + $response->assertOk(); + $response->assertJson([ + 'data' => [ + 'UpdatePlaylistTrack' => [ + 'animethemeentry' => [ + 'id' => $entry->getKey(), + ], + 'video' => [ + 'id' => $video->getKey(), + ], + ], + ], + ]); +}); diff --git a/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/UpdatePlaylistMutationTest.php b/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/UpdatePlaylistMutationTest.php new file mode 100644 index 000000000..7d6bc3b2c --- /dev/null +++ b/tests/Feature/GraphQL/Schema/Mutations/Models/List/Playlist/UpdatePlaylistMutationTest.php @@ -0,0 +1,109 @@ +mutation = ' + mutation($id: String!, $name: String, $visibility: PlaylistVisibility, $description: String) { + UpdatePlaylist(id: $id, name: $name, visibility: $visibility, description: $description) { + name + visibility + description + } + } + '; +}); + +test('protected', function () { + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'id' => fake()->word(), + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +test('forbidden', function () { + Event::fakeExcept(PlaylistCreated::class); + + actingAs(User::factory()->createOne()); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + // Needed for the bind resolver. + 'id' => Playlist::factory()->createOne()->hashid, + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +test('forbidden if not owner', function () { + Event::fakeExcept(PlaylistCreated::class); + + $user = User::factory() + ->withPermissions(CrudPermission::UPDATE->format(Playlist::class)) + ->createOne(); + + actingAs($user); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'id' => Playlist::factory()->createOne()->hashid, + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +it('updates', function () { + Event::fakeExcept(PlaylistCreated::class); + + $user = User::factory() + ->withPermissions(CrudPermission::UPDATE->format(Playlist::class)) + ->createOne(); + + actingAs($user); + + $playlist = Playlist::factory() + ->for($user) + ->createOne(); + + $newPlaylist = Playlist::factory()->makeOne(); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'id' => $playlist->hashid, + 'name' => $newPlaylist->name, + 'visibility' => $newPlaylist->visibility->name, + 'description' => $newPlaylist->description, + ], + ]); + + $response->assertOk(); + $response->assertJson([ + 'data' => [ + 'UpdatePlaylist' => [ + 'name' => $newPlaylist->name, + 'visibility' => $newPlaylist->visibility->name, + 'description' => $newPlaylist->description, + ], + ], + ]); +}); diff --git a/tests/Feature/GraphQL/Schema/Mutations/Models/User/ToggleLikeMutationTest.php b/tests/Feature/GraphQL/Schema/Mutations/Models/User/ToggleLikeMutationTest.php new file mode 100644 index 000000000..ecc1ee960 --- /dev/null +++ b/tests/Feature/GraphQL/Schema/Mutations/Models/User/ToggleLikeMutationTest.php @@ -0,0 +1,141 @@ +mutation = ' + mutation($entryId: Int, $playlistId: String) { + ToggleLike(entry: $entryId, playlist: $playlistId) { + animethemeentry: likeable { + ... on AnimeThemeEntry { + id + } + } + playlist: likeable { + ... on Playlist { + id + } + } + } + } + '; +}); + +test('protected', function () { + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'entryId' => fake()->randomDigitNotNull(), + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +test('forbidden', function () { + actingAs(User::factory()->createOne()); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + // Needed for the bind resolver. + 'entryId' => AnimeThemeEntry::factory()->createOne()->getKey(), + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +it('fails if more than one resource is passed', function () { + $user = User::factory() + ->withPermissions(CrudPermission::CREATE->format(Like::class)) + ->createOne(); + + actingAs($user); + + $entry = AnimeThemeEntry::factory()->createOne(); + $playlist = Playlist::factory()->createOne(); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'entryId' => $entry->getKey(), + 'playlistId' => $playlist->hashid, + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'validation'); + $this->assertArrayHasKey('entry', $response->json('errors.0.extensions.validation')); + $this->assertArrayHasKey('playlist', $response->json('errors.0.extensions.validation')); +}); + +it('likes entry', function () { + $user = User::factory() + ->withPermissions(CrudPermission::CREATE->format(Like::class)) + ->createOne(); + + actingAs($user); + + $entry = AnimeThemeEntry::factory()->createOne(); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'entryId' => $entry->getKey(), + ], + ]); + + $this->assertDatabaseCount(Like::class, 1); + $response->assertOk(); + $response->assertJson([ + 'data' => [ + 'ToggleLike' => [ + 'animethemeentry' => [ + 'id' => $entry->getKey(), + ], + ], + ], + ]); +}); + +it('likes playlist', function () { + $user = User::factory() + ->withPermissions(CrudPermission::CREATE->format(Like::class)) + ->createOne(); + + actingAs($user); + + $playlist = Playlist::factory()->createOne([ + Playlist::ATTRIBUTE_HASHID => fake()->word(), + ]); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'playlistId' => $playlist->hashid, + ], + ]); + + $this->assertDatabaseCount(Like::class, 1); + $response->assertOk(); + $response->assertJson([ + 'data' => [ + 'ToggleLike' => [ + 'playlist' => [ + 'id' => $playlist->hashid, + ], + ], + ], + ]); +}); diff --git a/tests/Feature/GraphQL/Schema/Mutations/Models/User/WatchMutationTest.php b/tests/Feature/GraphQL/Schema/Mutations/Models/User/WatchMutationTest.php new file mode 100644 index 000000000..825240723 --- /dev/null +++ b/tests/Feature/GraphQL/Schema/Mutations/Models/User/WatchMutationTest.php @@ -0,0 +1,114 @@ +mutation = ' + mutation($entryId: Int!, $videoId: Int!) { + Watch(entryId: $entryId, videoId: $videoId) { + animethemeentry { + id + } + video { + id + } + } + } + '; +}); + +test('protected', function () { + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'entryId' => fake()->randomDigitNotNull(), + 'videoId' => fake()->randomDigitNotNull(), + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +test('forbidden', function () { + actingAs(User::factory()->createOne()); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'entryId' => fake()->randomDigitNotNull(), + 'videoId' => fake()->randomDigitNotNull(), + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'authorization'); +}); + +test('invalid entry id or video id', function () { + $user = User::factory() + ->withPermissions(CrudPermission::CREATE->format(WatchHistory::class)) + ->createOne(); + + actingAs($user); + + $entry = AnimeThemeEntry::factory()->createOne(); + $video = Video::factory()->createOne(); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'entryId' => $entry->getKey(), + 'videoId' => $video->getKey(), + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'validation'); + $this->assertArrayHasKey('entryId', $response->json('errors.0.extensions.validation')); + $this->assertArrayHasKey('videoId', $response->json('errors.0.extensions.validation')); +}); + +test('mark as watched', function () { + $user = User::factory() + ->withPermissions(CrudPermission::CREATE->format(WatchHistory::class)) + ->createOne(); + + actingAs($user); + + $entry = AnimeThemeEntry::factory()->createOne(); + $video = Video::factory()->createOne(); + + $entry->videos()->attach($video); + + $response = graphql([ + 'query' => $this->mutation, + 'variables' => [ + 'entryId' => $entry->getKey(), + 'videoId' => $video->getKey(), + ], + ]); + + $this->assertDatabaseCount(WatchHistory::class, 1); + $response->assertOk(); + $response->assertJson([ + 'data' => [ + 'Watch' => [ + 'animethemeentry' => [ + 'id' => $entry->getKey(), + ], + 'video' => [ + 'id' => $video->getKey(), + ], + ], + ], + ]); +}); diff --git a/tests/Feature/GraphQL/Schema/Queries/Wiki/AnimeYearsQueryTest.php b/tests/Feature/GraphQL/Schema/Queries/Wiki/AnimeYearsQueryTest.php index c58a66067..0f584dafa 100644 --- a/tests/Feature/GraphQL/Schema/Queries/Wiki/AnimeYearsQueryTest.php +++ b/tests/Feature/GraphQL/Schema/Queries/Wiki/AnimeYearsQueryTest.php @@ -4,6 +4,45 @@ declare(strict_types=1); use App\Models\Wiki\Anime; +test('fails query season anime field without year', function () { + $animes = Anime::factory() + ->count(fake()->randomDigitNotNull()) + ->create(); + + $response = graphql([ + 'query' => ' + query($season: AnimeSeason!) { + animeyears { + year + season(season: $season) { + season + anime { + data { + id + } + } + } + seasons { + season + anime { + data { + id + } + } + } + } + } + ', + 'variables' => [ + 'season' => $animes->random()->getAttribute(Anime::ATTRIBUTE_SEASON)->name, + ], + ]); + + $response->assertOk(); + $response->assertJsonPath('errors.0.extensions.category', 'validation'); + $this->assertArrayHasKey('year', $response->json('errors.0.extensions.validation')); +}); + test('query season & seasons field', function () { $animes = Anime::factory() ->count(fake()->randomDigitNotNull()) @@ -29,7 +68,6 @@ test('query season & seasons field', function () { ]); $response->assertOk(); - $response->assertJsonStructure([ 'data' => [ 'animeyears' => [[ @@ -42,48 +80,6 @@ test('query season & seasons field', function () { ]); }); -test('fails query season anime field without year', function () { - $animes = Anime::factory() - ->count(fake()->randomDigitNotNull()) - ->create(); - - $response = graphql([ - 'query' => ' - query($season: AnimeSeason!) { - animeyears { - year - season(season: $season) { - season - anime { - data { - id - } - } - } - seasons { - season { - anime { - data { - id - } - } - } - } - } - } - ', - 'variables' => [ - 'season' => $animes->random()->getAttribute(Anime::ATTRIBUTE_SEASON)->name, - ], - ]); - - $response->assertOk(); - $response->assertJsonCount(1, 'errors'); - $response->assertJsonStructure([ - 'errors' => [['message']], - ]); -}); - test('query season anime field with year', function () { $animes = Anime::factory() ->count(fake()->randomDigitNotNull()) diff --git a/tests/Feature/GraphQL/Schema/Queries/Wiki/FindAnimeByExternalSiteQueryTest.php b/tests/Feature/GraphQL/Schema/Queries/Wiki/FindAnimeByExternalSiteQueryTest.php index c80e79e17..3c0600f75 100644 --- a/tests/Feature/GraphQL/Schema/Queries/Wiki/FindAnimeByExternalSiteQueryTest.php +++ b/tests/Feature/GraphQL/Schema/Queries/Wiki/FindAnimeByExternalSiteQueryTest.php @@ -25,9 +25,9 @@ test('fails without id or link', function () { ]); $response->assertOk(); - $response->assertJsonStructure([ - 'errors' => [['message']], - ]); + $response->assertJsonPath('errors.0.extensions.category', 'validation'); + $this->assertArrayHasKey('id', $response->json('errors.0.extensions.validation')); + $this->assertArrayHasKey('link', $response->json('errors.0.extensions.validation')); }); test('fails with for than 100 ids', function () { @@ -35,7 +35,7 @@ test('fails with for than 100 ids', function () { $response = graphql([ 'query' => ' - query($site: ResourceSite!, ids: [Int!]) { + query($site: ResourceSite!, $ids: [Int!]) { findAnimeByExternalSite(site: $site, id: $ids) { id } @@ -48,9 +48,8 @@ test('fails with for than 100 ids', function () { ]); $response->assertOk(); - $response->assertJsonStructure([ - 'errors' => [['message']], - ]); + $response->assertJsonPath('errors.0.extensions.category', 'validation'); + $this->assertArrayHasKey('id', $response->json('errors.0.extensions.validation')); }); test('passes with id', function () {