From 4a6739b8cdb5de89fd903590292b7359d4cb5e49 Mon Sep 17 00:00:00 2001 From: Kyrch Date: Fri, 14 Nov 2025 14:55:50 -0300 Subject: [PATCH] tests(graphql): added non-eloquent queries tests (#1004) --- .../GraphQL/ClientForbiddenException.php | 5 +- .../GraphQL/ClientValidationException.php | 5 +- app/GraphQL/Controllers/BaseController.php | 13 +- .../List/SyncExternalProfileController.php | 6 +- .../Wiki/Anime/AnimeYearsController.php | 7 +- .../Wiki/FindAnimeByExternalSiteQuery.php | 2 +- .../Admin/CurrentFeaturedThemeQueryTest.php | 38 +++++ .../Schema/Queries/Auth/MeQueryTest.php | 52 +++++++ .../Schema/Queries/SearchQueryTest.php | 62 ++++++++ .../Queries/Wiki/AnimeYearsQueryTest.php | 146 ++++++++++++++++++ .../Wiki/FindAnimeByExternalSiteQueryTest.php | 130 ++++++++++++++++ 11 files changed, 453 insertions(+), 13 deletions(-) create mode 100644 tests/Feature/GraphQL/Schema/Queries/Admin/CurrentFeaturedThemeQueryTest.php create mode 100644 tests/Feature/GraphQL/Schema/Queries/Auth/MeQueryTest.php create mode 100644 tests/Feature/GraphQL/Schema/Queries/SearchQueryTest.php create mode 100644 tests/Feature/GraphQL/Schema/Queries/Wiki/AnimeYearsQueryTest.php create mode 100644 tests/Feature/GraphQL/Schema/Queries/Wiki/FindAnimeByExternalSiteQueryTest.php diff --git a/app/Exceptions/GraphQL/ClientForbiddenException.php b/app/Exceptions/GraphQL/ClientForbiddenException.php index a4ff23fff..edbeb501b 100644 --- a/app/Exceptions/GraphQL/ClientForbiddenException.php +++ b/app/Exceptions/GraphQL/ClientForbiddenException.php @@ -4,13 +4,12 @@ declare(strict_types=1); namespace App\Exceptions\GraphQL; -use Exception; -use GraphQL\Error\ClientAware; +use GraphQL\Error\Error; /** * Thrown when query is not allowed. */ -class ClientForbiddenException extends Exception implements ClientAware +class ClientForbiddenException extends Error { public function isClientSafe(): bool { diff --git a/app/Exceptions/GraphQL/ClientValidationException.php b/app/Exceptions/GraphQL/ClientValidationException.php index 8457f34d5..4e7efe674 100644 --- a/app/Exceptions/GraphQL/ClientValidationException.php +++ b/app/Exceptions/GraphQL/ClientValidationException.php @@ -4,13 +4,12 @@ declare(strict_types=1); namespace App\Exceptions\GraphQL; -use Exception; -use GraphQL\Error\ClientAware; +use GraphQL\Error\Error; /** * Thrown when client arguments are missing or wrong. */ -class ClientValidationException extends Exception implements ClientAware +class ClientValidationException extends Error { public function isClientSafe(): bool { diff --git a/app/GraphQL/Controllers/BaseController.php b/app/GraphQL/Controllers/BaseController.php index 9b459d7d7..cf6255829 100644 --- a/app/GraphQL/Controllers/BaseController.php +++ b/app/GraphQL/Controllers/BaseController.php @@ -8,10 +8,11 @@ use App\Actions\Http\Api\DestroyAction; use App\Actions\Http\Api\StoreAction; use App\Actions\Http\Api\UpdateAction; use App\GraphQL\Schema\Mutations\BaseMutation; -use Dotenv\Exception\ValidationException; use Illuminate\Support\Arr; use Illuminate\Support\Facades\App; use Illuminate\Support\Facades\Validator; +use Illuminate\Validation\ValidationException; +use Rebing\GraphQL\Error\ValidationError; /** * @template TModel of \Illuminate\Database\Eloquent\Model @@ -37,13 +38,19 @@ abstract class BaseController * @param class-string $mutation * @return array * - * @throws ValidationException + * @throws ValidationError */ public function validated(array $args, string $mutation): array { $mutationInstance = App::make($mutation); - $validated = Validator::make($args, $mutationInstance->rulesForValidation($args))->validated(); + $validator = Validator::make($args, $mutationInstance->rulesForValidation($args)); + + try { + $validated = $validator->validated(); + } catch (ValidationException $e) { + throw new ValidationError($e->getMessage(), $validator); + } return [ ...$validated, diff --git a/app/GraphQL/Controllers/List/SyncExternalProfileController.php b/app/GraphQL/Controllers/List/SyncExternalProfileController.php index 556a171fe..bd107ae05 100644 --- a/app/GraphQL/Controllers/List/SyncExternalProfileController.php +++ b/app/GraphQL/Controllers/List/SyncExternalProfileController.php @@ -21,7 +21,11 @@ class SyncExternalProfileController extends BaseController /** @var ExternalProfile $profile */ $profile = Arr::pull($args, self::MODEL); - throw_unless($profile->canBeSynced(), ClientForbiddenException::class, 'This external profile cannot be synced at the moment.'); + throw_unless( + $profile->canBeSynced(), + ClientForbiddenException::class, + 'This external profile cannot be synced at the moment.' + ); $profile->dispatchSyncJob(); diff --git a/app/GraphQL/Controllers/Wiki/Anime/AnimeYearsController.php b/app/GraphQL/Controllers/Wiki/Anime/AnimeYearsController.php index e3abbf620..d09e5d1c8 100644 --- a/app/GraphQL/Controllers/Wiki/Anime/AnimeYearsController.php +++ b/app/GraphQL/Controllers/Wiki/Anime/AnimeYearsController.php @@ -35,8 +35,11 @@ class AnimeYearsController extends BaseController $fieldSelection = $resolveInfo->getFieldSelection(1); // Restrict 'animes' field to a unique year. - throw_if(($year === null || count($year) > 1) - && (Arr::get($fieldSelection, 'season.anime') || Arr::get($fieldSelection, 'seasons.anime')), ClientValidationException::class, "Please provide a unique 'year' argument to query the animes field."); + throw_if( + ($year === null || count($year) > 1) && (Arr::get($fieldSelection, 'season.anime') || Arr::get($fieldSelection, 'seasons.anime')), + ClientValidationException::class, + "Please provide a unique 'year' argument to query the animes field." + ); return Anime::query() ->distinct([Anime::ATTRIBUTE_YEAR, Anime::ATTRIBUTE_SEASON]) diff --git a/app/GraphQL/Schema/Queries/Wiki/FindAnimeByExternalSiteQuery.php b/app/GraphQL/Schema/Queries/Wiki/FindAnimeByExternalSiteQuery.php index 923e3b605..0ca2f5441 100644 --- a/app/GraphQL/Schema/Queries/Wiki/FindAnimeByExternalSiteQuery.php +++ b/app/GraphQL/Schema/Queries/Wiki/FindAnimeByExternalSiteQuery.php @@ -79,7 +79,7 @@ class FindAnimeByExternalSiteQuery extends BaseQuery throw_if(is_null($externalId) && is_null($link), ClientValidationException::class, 'At least "id" or "link" is required.'); - throw_if(count($externalId) > 100, ClientValidationException::class, 'The "Id" parameter cannot contain more than 100 integer values.'); + throw_if($externalId !== null && count($externalId) > 100, ClientValidationException::class, 'The "Id" parameter cannot contain more than 100 integer values.'); $builder->whereRelation(Anime::RELATION_RESOURCES, function (Builder $query) use ($site, $externalId, $link): void { $query->where(ExternalResource::ATTRIBUTE_SITE, $site); diff --git a/tests/Feature/GraphQL/Schema/Queries/Admin/CurrentFeaturedThemeQueryTest.php b/tests/Feature/GraphQL/Schema/Queries/Admin/CurrentFeaturedThemeQueryTest.php new file mode 100644 index 000000000..746bf12b6 --- /dev/null +++ b/tests/Feature/GraphQL/Schema/Queries/Admin/CurrentFeaturedThemeQueryTest.php @@ -0,0 +1,38 @@ +sequence(fn () => [ + FeaturedTheme::ATTRIBUTE_START_AT => now()->addDays(fake()->numberBetween(1, 10)), + FeaturedTheme::ATTRIBUTE_END_AT => now()->addDays(fake()->numberBetween(11, 20)), + ]) + ->count(fake()->randomDigitNotNull()) + ->create(); + + $featuredTheme = FeaturedTheme::factory()->create(); + + $response = post(route('graphql'), [ + 'query' => ' + query { + currentfeaturedtheme { + id + } + } + ', + ]); + + $response->assertOk(); + $response->assertJson([ + 'data' => [ + 'currentfeaturedtheme' => [ + 'id' => $featuredTheme->getKey(), + ], + ], + ]); +}); diff --git a/tests/Feature/GraphQL/Schema/Queries/Auth/MeQueryTest.php b/tests/Feature/GraphQL/Schema/Queries/Auth/MeQueryTest.php new file mode 100644 index 000000000..0a86c6a0c --- /dev/null +++ b/tests/Feature/GraphQL/Schema/Queries/Auth/MeQueryTest.php @@ -0,0 +1,52 @@ + ' + query { + me { + id + } + } + ', + ]); + + $response->assertOk(); + $response->assertJson([ + 'data' => [ + 'me' => null, + ], + ]); +}); + +test('authenticated returns user', function () { + $user = User::factory()->createOne(); + + actingAs($user); + + $response = post(route('graphql'), [ + 'query' => ' + query { + me { + id + } + } + ', + ]); + + $response->assertOk(); + $response->assertJson([ + 'data' => [ + 'me' => [ + 'id' => $user->getKey(), + ], + ], + ]); +}); diff --git a/tests/Feature/GraphQL/Schema/Queries/SearchQueryTest.php b/tests/Feature/GraphQL/Schema/Queries/SearchQueryTest.php new file mode 100644 index 000000000..0f3647697 --- /dev/null +++ b/tests/Feature/GraphQL/Schema/Queries/SearchQueryTest.php @@ -0,0 +1,62 @@ + ' + query($search: String!) { + search(search: $search) { + anime { + id + } + artists { + id + } + animethemes { + id + } + playlists { + id + } + series { + id + } + songs { + id + } + studios { + id + } + videos { + id + } + } + } + ', + 'variables' => [ + 'search' => fake()->word(), + ], + ]); + + $response->assertJsonStructure([ + 'data' => [ + 'search' => [ + 'anime', + 'artists', + 'animethemes', + 'playlists', + 'series', + 'songs', + 'studios', + 'videos', + ], + ], + ]); +}); diff --git a/tests/Feature/GraphQL/Schema/Queries/Wiki/AnimeYearsQueryTest.php b/tests/Feature/GraphQL/Schema/Queries/Wiki/AnimeYearsQueryTest.php new file mode 100644 index 000000000..2040c8c63 --- /dev/null +++ b/tests/Feature/GraphQL/Schema/Queries/Wiki/AnimeYearsQueryTest.php @@ -0,0 +1,146 @@ +count(fake()->randomDigitNotNull()) + ->create(); + + $response = post(route('graphql'), [ + 'query' => ' + query($season: AnimeSeason!) { + animeyears { + year + season(season: $season) { + season + } + seasons { + season + } + } + } + ', + 'variables' => [ + 'season' => $animes->random()->getAttribute(Anime::ATTRIBUTE_SEASON)->name, + ], + ]); + + $response->assertOk(); + + $response->assertJsonStructure([ + 'data' => [ + 'animeyears' => [[ + 'year', + 'season' => [ + 'season', + ], + ]], + ], + ]); +}); + +test('fails query season anime field without year', function () { + $animes = Anime::factory() + ->count(fake()->randomDigitNotNull()) + ->create(); + + $response = post(route('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()) + ->create(); + + $random = $animes->random(); + + $response = post(route('graphql'), [ + 'query' => ' + query($season: AnimeSeason!, $year: [Int!]) { + animeyears(year: $year) { + year + season(season: $season) { + season + anime { + data { + id + } + } + } + seasons { + season + anime { + data { + id + } + } + } + } + } + ', + 'variables' => [ + 'season' => $random->getAttribute(Anime::ATTRIBUTE_SEASON)->name, + 'year' => $random->getAttribute(Anime::ATTRIBUTE_YEAR), + ], + ]); + + $response->assertOk(); + $response->assertJsonStructure([ + 'data' => [ + 'animeyears' => [[ + 'year', + 'season' => [ + 'season', + 'anime' => [ + 'data' => [['id']], + ], + ], + 'seasons' => [[ + 'season', + 'anime' => [ + 'data' => [['id']], + ], + ]], + ]], + ], + ]); +}); diff --git a/tests/Feature/GraphQL/Schema/Queries/Wiki/FindAnimeByExternalSiteQueryTest.php b/tests/Feature/GraphQL/Schema/Queries/Wiki/FindAnimeByExternalSiteQueryTest.php new file mode 100644 index 000000000..0f9d18b4f --- /dev/null +++ b/tests/Feature/GraphQL/Schema/Queries/Wiki/FindAnimeByExternalSiteQueryTest.php @@ -0,0 +1,130 @@ + ' + query($site: ResourceSite!) { + findAnimeByExternalSite(site: $site) { + id + } + } + ', + 'variables' => [ + 'site' => $resourceSite->name, + ], + ]); + + $response->assertOk(); + $response->assertJsonStructure([ + 'errors' => [['message']], + ]); +}); + +test('fails with for than 100 ids', function () { + $resourceSite = Arr::random(ResourceSite::cases()); + + $response = post(route('graphql'), [ + 'query' => ' + query($site: ResourceSite!, ids: [Int!]) { + findAnimeByExternalSite(site: $site, id: $ids) { + id + } + } + ', + 'variables' => [ + 'site' => $resourceSite->name, + 'ids' => Collection::times(101, fn (int $int) => $int + 1)->toArray(), + ], + ]); + + $response->assertOk(); + $response->assertJsonStructure([ + 'errors' => [['message']], + ]); +}); + +test('passes with id', function () { + Resourceable::factory() + ->for( + ExternalResource::factory()->create([ + ExternalResource::ATTRIBUTE_EXTERNAL_ID => $externalId = fake()->randomDigitNotNull(), + ]), + Resourceable::RELATION_RESOURCE + ) + ->forAnime() + ->create(); + + $resourceSite = ExternalResource::query() + ->first() + ->getAttribute(ExternalResource::ATTRIBUTE_SITE); + + $response = post(route('graphql'), [ + 'query' => ' + query($site: ResourceSite!, $externalId: [Int!]) { + findAnimeByExternalSite(site: $site, id: $externalId) { + id + } + } + ', + 'variables' => [ + 'site' => $resourceSite->name, + 'externalId' => $externalId, + ], + ]); + + $response->assertOk(); + $response->assertExactJsonStructure([ + 'data' => [ + 'findAnimeByExternalSite' => [['id']], + ], + ]); +}); + +test('passes with link', function () { + Resourceable::factory() + ->for( + ExternalResource::factory()->create([ + ExternalResource::ATTRIBUTE_LINK => $link = fake()->url(), + ]), + Resourceable::RELATION_RESOURCE + ) + ->forAnime() + ->create(); + + $resourceSite = ExternalResource::query() + ->first() + ->getAttribute(ExternalResource::ATTRIBUTE_SITE); + + $response = post(route('graphql'), [ + 'query' => ' + query($site: ResourceSite!, $link: String) { + findAnimeByExternalSite(site: $site, link: $link) { + id + } + } + ', + 'variables' => [ + 'site' => $resourceSite->name, + 'link' => (string) $link, + ], + ]); + + $response->assertOk(); + $response->assertJsonStructure([ + 'data' => [ + 'findAnimeByExternalSite' =>[['id']], + ], + ]); +});