From a530e9f5d6cb7b11f942ca4853a2c6d6e0208339 Mon Sep 17 00:00:00 2001 From: pushrbx Date: Thu, 2 Feb 2023 23:37:37 +0000 Subject: [PATCH] multiple changes - wip -> http tests should use model factories - test runner bootstrap: jikan models are cached in a temporary file - fixed various bugs - improved test execution time with typesense - added new dev dependency: ClassFinder - updated composer scripts to include coverage generation - added coverage reports in phpunit - improved roadrunner integration - updated docker image - added xdebug in disabled state --- .codecov.yml | 26 + .gitignore | 6 + Dockerfile | 3 + app/Club.php | 7 +- .../RequestHandlerWithScraperCache.php | 2 +- app/Http/Middleware/MicroCaching.php | 14 +- app/JikanSearchable.php | 9 +- app/Magazine.php | 3 +- app/Providers/AppServiceProvider.php | 13 +- app/Providers/SerializerFactory.php | 14 + app/Support/CachedData.php | 25 +- app/Support/JikanConfig.php | 8 + app/Testing/ScoutFlush.php | 36 +- app/Testing/SyntheticMongoDbTransaction.php | 8 +- bootstrap/tests.php | 13 + composer.json | 8 +- composer.lock | 52 +- config/database.php | 11 +- config/jikan.php | 1 + config/roadrunner.php | 2 + database/factories/CharacterFactory.php | 25 +- database/factories/ClubFactory.php | 80 +++ database/factories/JikanModelFactory.php | 13 +- database/factories/MagazineFactory.php | 33 ++ docker/base_image/php-8.1/Dockerfile | 13 +- phpunit.xml | 23 +- storage/app/.gitignore | 3 +- .../Controllers/AnimeControllerTest.php | 470 ++++-------------- .../Controllers/CharacterControllerTest.php | 44 ++ .../HttpV4/Controllers/ClubControllerTest.php | 43 +- .../Controllers/GenreControllerTest.php | 6 + .../Controllers/MagazineControllerTest.php | 13 +- .../Controllers/MangaControllerTest.php | 5 + .../Controllers/PersonControllerTest.php | 5 + .../Controllers/ProducerControllerTest.php | 4 + .../RecommendationsControllerTest.php | 4 + .../Controllers/ReviewsControllerTest.php | 4 + .../Controllers/ScheduleControllerTest.php | 5 + .../Controllers/SearchControllerTest.php | 5 + .../HttpV4/Controllers/TopControllerTest.php | 5 + .../HttpV4/Controllers/UserControllerTest.php | 5 + .../Controllers/WatchControllerTest.php | 4 + tests/Integration/AnimeControllerTest.php | 261 ---------- tests/IntegrationTestListener.php | 21 +- tests/TestCase.php | 18 + tests/Unit/.gitkeep | 0 .../Unit/DefaultCachedScraperServiceTest.php | 1 - 47 files changed, 687 insertions(+), 687 deletions(-) create mode 100644 .codecov.yml create mode 100644 database/factories/ClubFactory.php create mode 100644 database/factories/MagazineFactory.php delete mode 100644 tests/Integration/AnimeControllerTest.php delete mode 100644 tests/Unit/.gitkeep diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..0e9b4e1 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,26 @@ +# Docs: + +coverage: + # coverage lower than 50 is red, higher than 90 green + range: 30..80 + + status: + project: + default: + # Choose a minimum coverage ratio that the commit must meet to be considered a success. + # + # `auto` will use the coverage from the base commit (pull request base or parent commit) coverage to compare + # against. + target: auto + + # Allow the coverage to drop by X%, and posting a success status. + threshold: 5% + + # Resulting status will pass no matter what the coverage is or what other settings are specified. + informational: true + + patch: + default: + target: auto + threshold: 5% + informational: true diff --git a/.gitignore b/.gitignore index 46ca520..4e30353 100644 --- a/.gitignore +++ b/.gitignore @@ -11,3 +11,9 @@ composer.phar /storage/app/failovers.json /storage/app/source_failover_last_downtime /storage/app/source_failover.lock +# Temp dirs & trash +/temp +/tmp +/coverage +.DS_Store +*.cache diff --git a/Dockerfile b/Dockerfile index 8e2bc8d..a9eb275 100644 --- a/Dockerfile +++ b/Dockerfile @@ -19,8 +19,11 @@ RUN set -ex \ # enable opcache for CLI and JIT, docs: && echo -e "\nopcache.enable=1\nopcache.enable_cli=1\nopcache.jit_buffer_size=32M\nopcache.jit=1235\n" >> \ ${PHP_INI_DIR}/conf.d/docker-php-ext-opcache.ini \ + # show php version + && php -v \ # show installed modules && php -m \ + && composer --version \ # create unpriviliged user && adduser --disabled-password --shell "/sbin/nologin" --home "/nonexistent" --no-create-home --uid "10001" --gecos "" "jikanapi" \ && mkdir /app /var/run/rr \ diff --git a/app/Club.php b/app/Club.php index dfbc388..05c8cf7 100644 --- a/app/Club.php +++ b/app/Club.php @@ -5,11 +5,12 @@ namespace App; use App\Concerns\FilteredByLetter; use App\Enums\ClubCategoryEnum; use App\Enums\ClubTypeEnum; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Jikan\Request\Club\ClubRequest; class Club extends JikanApiSearchableModel { - use FilteredByLetter; + use FilteredByLetter, HasFactory; protected array $filters = ["order_by", "sort", "letter", "category", "type"]; @@ -19,7 +20,7 @@ class Club extends JikanApiSearchableModel * @var array */ protected $fillable = [ - 'mal_id', 'url', 'images', 'name', 'members', 'category', 'created', 'access', 'anime', 'manga' + 'mal_id', 'url', 'images', 'name', 'members', 'category', 'created', 'access', 'anime', 'manga', 'created_at', 'updated_at', 'characters', 'staff' ]; /** @@ -48,7 +49,7 @@ class Club extends JikanApiSearchableModel public function __construct(array $attributes = []) { parent::__construct($attributes); - $this->displayNameFieldName = "title"; + $this->displayNameFieldName = "name"; } /** @noinspection PhpUnused */ diff --git a/app/Features/RequestHandlerWithScraperCache.php b/app/Features/RequestHandlerWithScraperCache.php index ceb825a..4650cdd 100644 --- a/app/Features/RequestHandlerWithScraperCache.php +++ b/app/Features/RequestHandlerWithScraperCache.php @@ -40,7 +40,7 @@ abstract class RequestHandlerWithScraperCache implements RequestHandler protected function resource(Collection $results): JsonResource { return new ResultsResource( - $results->first() + $results->first() ?? ["results" => []] ); } diff --git a/app/Http/Middleware/MicroCaching.php b/app/Http/Middleware/MicroCaching.php index f5c8cf0..115b116 100644 --- a/app/Http/Middleware/MicroCaching.php +++ b/app/Http/Middleware/MicroCaching.php @@ -3,7 +3,9 @@ namespace App\Http\Middleware; use App\Http\HttpHelper; +use App\Support\JikanConfig; use Closure; +use Illuminate\Support\Env; use Illuminate\Support\Facades\Cache; use Jikan\Exception\BadResponseException; @@ -18,6 +20,13 @@ class MicroCaching 'InsightsController@main' ]; + private readonly bool $isEnabled; + + public function __construct(JikanConfig $jikanConfig) + { + $this->isEnabled = $jikanConfig->isMicroCachingEnabled(); + } + /** * Handle an incoming request. * @@ -27,6 +36,9 @@ class MicroCaching */ public function handle($request, Closure $next) { + if (!$this->isEnabled) { + return $next($request); + } if (isset($request->route()[1]['uses'])) { $route = explode('\\', $request->route()[1]['uses']); $route = end($route); @@ -65,7 +77,7 @@ class MicroCaching Cache::add( $fingerprint, json_encode( - $next($request)->getData() + $response->getData() ), env('MICROCACHING_EXPIRE', 60) ); diff --git a/app/JikanSearchable.php b/app/JikanSearchable.php index bc114f3..3970266 100644 --- a/app/JikanSearchable.php +++ b/app/JikanSearchable.php @@ -4,6 +4,7 @@ namespace App; use Jikan\Helper\Parser; use Laravel\Scout\Builder; use Laravel\Scout\Searchable; +use MongoDB\BSON\UTCDateTime; trait JikanSearchable { @@ -30,8 +31,14 @@ trait JikanSearchable }, $field); } - protected function convertToTimestamp(?string $datetime): int + protected function convertToTimestamp(mixed $datetime): int { + if ($datetime instanceof \DateTimeInterface) { + return $datetime->getTimestamp(); + } + if ($datetime instanceof UTCDateTime) { + return $datetime->toDateTime()->getTimestamp(); + } return $datetime ? Parser::parseDate($datetime)->getTimestamp() : 0; } diff --git a/app/Magazine.php b/app/Magazine.php index 6641953..a37b924 100644 --- a/app/Magazine.php +++ b/app/Magazine.php @@ -3,6 +3,7 @@ namespace App; use App\Concerns\FilteredByLetter; +use Illuminate\Database\Eloquent\Factories\HasFactory; use Jikan\Jikan; use Jikan\Request\Magazine\MagazinesRequest; @@ -12,7 +13,7 @@ use Jikan\Request\Magazine\MagazinesRequest; */ class Magazine extends JikanApiSearchableModel { - use FilteredByLetter; + use FilteredByLetter, HasFactory; protected array $filters = ["order_by", "sort", "letter"]; /** diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index d4bfed8..7c06831 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -222,6 +222,7 @@ class AppServiceProvider extends ServiceProvider Features\AnimeExternalLookupHandler::class => $unitOfWorkInstance->anime(), Features\AnimeStreamingLookupHandler::class => $unitOfWorkInstance->anime(), Features\AnimeThemesLookupHandler::class => $unitOfWorkInstance->anime(), + Features\AnimeUserUpdatesLookupHandler::class => $unitOfWorkInstance->documents("anime_userupdates"), Features\CharacterLookupHandler::class => $unitOfWorkInstance->characters(), Features\CharacterFullLookupHandler::class => $unitOfWorkInstance->characters(), Features\CharacterAnimeLookupHandler::class => $unitOfWorkInstance->characters(), @@ -355,8 +356,7 @@ class AppServiceProvider extends ServiceProvider public static function servicesToWarm(): array { $services = [ - ScoutSearchService::class, - UnitOfWork::class + ScoutSearchService::class ]; if (Env::get("SCOUT_DRIVER") === "typesense") { @@ -375,4 +375,13 @@ class AppServiceProvider extends ServiceProvider return $services; } + + public static function servicesToClear(): array + { + return [ + // in RoadRunner we want to reset the repositories after each request, because we cache the query builders in them. + // todo: refactor repositories to avoid caching query builders, so this step is not necessary + JikanUnitOfWork::class + ]; + } } diff --git a/app/Providers/SerializerFactory.php b/app/Providers/SerializerFactory.php index 558eaa1..6ea6c3f 100644 --- a/app/Providers/SerializerFactory.php +++ b/app/Providers/SerializerFactory.php @@ -7,8 +7,10 @@ use Jikan\Model\Common\DateRange; use Jikan\Model\Common\MalUrl; use JMS\Serializer\GraphNavigatorInterface; use JMS\Serializer\Handler\HandlerRegistry; +use JMS\Serializer\SerializationContext; use JMS\Serializer\Serializer; use JMS\Serializer\SerializerBuilder; +use MongoDB\BSON\UTCDateTime; class SerializerFactory { @@ -45,6 +47,13 @@ class SerializerFactory 'json', self::convertCarbonDateRange(...) ); + + $registry->registerHandler( + GraphNavigatorInterface::DIRECTION_SERIALIZATION, + UTCDateTime::class, + 'json', + self::convertBsonDateTime(...) + ); } ) ->setSerializationContextFactory(new SerializationContextFactory()) @@ -129,4 +138,9 @@ class SerializerFactory { return $obj ? $obj->format(DATE_ATOM) : null; } + + private static function convertBsonDateTime($visitor, UTCDateTime $obj, array $type, SerializationContext $context): string + { + return $obj->toDateTime()->format(DATE_ATOM); + } } diff --git a/app/Support/CachedData.php b/app/Support/CachedData.php index e148cf6..9fe6e6a 100644 --- a/app/Support/CachedData.php +++ b/app/Support/CachedData.php @@ -4,8 +4,10 @@ namespace App\Support; use App\Concerns\ScraperCacheTtl; use App\JikanApiModel; +use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Illuminate\Support\Env; +use MongoDB\BSON\UTCDateTime; final class CachedData { @@ -82,16 +84,31 @@ final class CachedData $result = $this->scraperResult->first(); - if ($result instanceof JikanApiModel && !is_null($result->getAttributeValue("modifiedAt"))) { - return (int) $result["modifiedAt"]->toDateTime()->format("U"); + if ($result instanceof JikanApiModel && null != $modifiedAt = $result->getAttributeValue("modifiedAt")) { + return $this->mixedToTimestamp($modifiedAt); } if (is_array($result) && array_key_exists("modifiedAt", $result)) { - return (int) $result["modifiedAt"]->toDateTime()->format("U"); + return $this->mixedToTimestamp($result["modifiedAt"]); } if (is_object($result) && property_exists($result, "modifiedAt")) { - return $result->modifiedAt->toDateTime()->format("U"); + return $this->mixedToTimestamp($result->modifiedAt); + } + + return null; + } + + private function mixedToTimestamp(mixed $modifiedAt): ?int + { + if ($modifiedAt instanceof UTCDateTime) { + return (int) $modifiedAt->toDateTime()->format("U"); + } + if ($modifiedAt instanceof \DateTimeInterface) { + return (int) $modifiedAt->format("U"); + } + if (is_string($modifiedAt)) { + return Carbon::createFromTimeString($modifiedAt)->format("U"); } return null; diff --git a/app/Support/JikanConfig.php b/app/Support/JikanConfig.php index d8d8ced..4c881e2 100644 --- a/app/Support/JikanConfig.php +++ b/app/Support/JikanConfig.php @@ -16,11 +16,14 @@ final class JikanConfig private int $defaultCacheExpire; + private bool $microCachingEnabled; + public function __construct(array $config) { $config = collect($config); $this->perEndpointCacheTtl = $config->get("per_endpoint_cache_ttl", []); $this->defaultCacheExpire = $config->get("default_cache_expire", 0); + $this->microCachingEnabled = in_array($config->get("micro_caching_enabled", false), [true, 1, "1", "true"]); } public function cacheTtlForEndpoint(string $endpoint): ?int @@ -32,4 +35,9 @@ final class JikanConfig { return $this->defaultCacheExpire; } + + public function isMicroCachingEnabled(): bool + { + return $this->microCachingEnabled; + } } diff --git a/app/Testing/ScoutFlush.php b/app/Testing/ScoutFlush.php index ab1d84c..731d310 100644 --- a/app/Testing/ScoutFlush.php +++ b/app/Testing/ScoutFlush.php @@ -2,16 +2,46 @@ namespace App\Testing; +use Typesense\LaravelTypesense\Typesense; + trait ScoutFlush { protected array $searchIndexModelCleanupList = [ - "App\\Anime", "App\\Manga", "App\\Character", "App\\GenreAnime", "App\\GenreManga", "App\\Person" + "App\\Anime", + "App\\Manga", + "App\\Character", + "App\\GenreAnime", + "App\\GenreManga", + "App\\Person", + "App\\Club", + "App\\Magazine" ]; public function runScoutFlush(): void { - foreach ($this->searchIndexModelCleanupList as $model) { - $this->artisan("scout:flush", ["model" => $model]); + if (config("scout.driver") === "typesense") { + /** + * @var Typesense $typeSenseClient + */ + $typeSenseClient = app(Typesense::class); + // more optimized approach for quicker tests. + foreach ($this->searchIndexModelCleanupList as $model) { + $modelInstance = new $model; + $collection = $typeSenseClient->getCollectionIndex($modelInstance); + // we count items by exporting + $items = $collection->documents->export(); + if (strlen($items) > 1) { + $typeSenseClient->deleteDocuments($collection, [ + "filter_by" => "mal_id:>0", + "batch_size" => 500 + ]); + } + } + } + else { + foreach ($this->searchIndexModelCleanupList as $model) { + $this->artisan("scout:flush", ["model" => $model]); + } } } } diff --git a/app/Testing/SyntheticMongoDbTransaction.php b/app/Testing/SyntheticMongoDbTransaction.php index 4ff3531..3b09480 100644 --- a/app/Testing/SyntheticMongoDbTransaction.php +++ b/app/Testing/SyntheticMongoDbTransaction.php @@ -3,8 +3,12 @@ namespace App\Testing; use App\JikanApiModel; +use HaydenPierce\ClassFinder\ClassFinder; use Illuminate\Support\Facades\DB; +/** + * A trait for test cases which want to clear the database after each test + */ trait SyntheticMongoDbTransaction { private static array $jikanModels = []; @@ -13,7 +17,9 @@ trait SyntheticMongoDbTransaction { if (count(self::$jikanModels) === 0) { - self::$jikanModels = array_filter(get_declared_classes(), fn($class) => is_subclass_of($class, JikanApiModel::class)); + self::$jikanModels = json_decode( + file_get_contents(base_path("storage/app") . "/jikan_model_classes.json") + ); } return self::$jikanModels; diff --git a/bootstrap/tests.php b/bootstrap/tests.php index 94fe199..339223a 100644 --- a/bootstrap/tests.php +++ b/bootstrap/tests.php @@ -1,5 +1,8 @@ is_subclass_of($class, JikanApiModel::class)) +); +file_put_contents($classNamesCachePath . "/jikan_model_classes.json", json_encode($jikanModels)); + diff --git a/composer.json b/composer.json index 3fce07a..370083d 100644 --- a/composer.json +++ b/composer.json @@ -43,6 +43,7 @@ }, "require-dev": { "fakerphp/faker": "^1.21", + "haydenpierce/class-finder": "^0.4.4", "mockery/mockery": "^1.5.1", "phpunit/phpunit": "^9.5.28" }, @@ -64,8 +65,13 @@ "post-root-package-install": [ "php -r \"copy('.env.dist', '.env');\"" ], + "phpunit": "@php ./vendor/bin/phpunit --no-coverage", + "phpunit-cover": "@php ./vendor/bin/phpunit", "test": [ - "php ./vendor/phpunit/phpunit/phpunit" + "@phpunit" + ], + "test-cover": [ + "@phpunit-cover" ] }, "minimum-stability": "dev", diff --git a/composer.lock b/composer.lock index 5b75321..174f5dd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "64ddaa662c7d0df9a7127336259b22ff", + "content-hash": "9ffd521140d58bc63cf640fedd62a978", "packages": [ { "name": "amphp/amp", @@ -7238,12 +7238,12 @@ "source": { "type": "git", "url": "https://github.com/pushrbx/lumen-roadrunner.git", - "reference": "835e99ba6854f31236a7039f29e730c785800a88" + "reference": "52ab151734d13861759415f1985c8e01e96885dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pushrbx/lumen-roadrunner/zipball/835e99ba6854f31236a7039f29e730c785800a88", - "reference": "835e99ba6854f31236a7039f29e730c785800a88", + "url": "https://api.github.com/repos/pushrbx/lumen-roadrunner/zipball/52ab151734d13861759415f1985c8e01e96885dd", + "reference": "52ab151734d13861759415f1985c8e01e96885dd", "shasum": "" }, "require": { @@ -7337,7 +7337,7 @@ "issues": "https://github.com/spiral/roadrunner-laravel/issues", "source": "https://github.com/spiral/roadrunner-laravel" }, - "time": "2022-08-16T16:49:18+00:00" + "time": "2023-02-01T00:49:15+00:00" }, { "name": "ralouphie/getallheaders", @@ -11552,6 +11552,48 @@ }, "time": "2020-07-09T08:09:16+00:00" }, + { + "name": "haydenpierce/class-finder", + "version": "0.4.4", + "source": { + "type": "git", + "url": "git@gitlab.com:hpierce1102/ClassFinder.git", + "reference": "94c602870ddf8d4fa2d67fb9bae637d88f9bd76e" + }, + "dist": { + "type": "zip", + "url": "https://gitlab.com/api/v4/projects/hpierce1102%2FClassFinder/repository/archive.zip?sha=94c602870ddf8d4fa2d67fb9bae637d88f9bd76e", + "reference": "94c602870ddf8d4fa2d67fb9bae637d88f9bd76e", + "shasum": "" + }, + "require": { + "ext-json": "*", + "php": ">=5.3" + }, + "require-dev": { + "mikey179/vfsstream": "^1.6", + "phpunit/phpunit": "~9.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "HaydenPierce\\ClassFinder\\": "src/", + "HaydenPierce\\ClassFinder\\UnitTest\\": "test/unit" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Hayden Pierce", + "email": "hayden@haydenpierce.com" + } + ], + "description": "A library that can provide of a list of classes in a given namespace", + "time": "2022-09-26T22:42:59+00:00" + }, { "name": "mockery/mockery", "version": "1.5.1", diff --git a/config/database.php b/config/database.php index e3b278c..1e38df4 100644 --- a/config/database.php +++ b/config/database.php @@ -1,12 +1,21 @@ env('DB_CONNECTION', 'mongodb'), 'connections' => [ 'mongodb' => [ 'driver' => 'mongodb', - 'dsn'=> "mongodb://".env('DB_USERNAME', 'admin').":".env('DB_PASSWORD', '')."@".env('DB_HOST', 'localhost').":".env('DB_PORT', 27017)."/".env('DB_ADMIN', 'admin'), + 'dsn'=> $dsn, 'database' => env('DB_DATABASE', 'jikan'), ] ], diff --git a/config/jikan.php b/config/jikan.php index 870df5c..f785ee8 100644 --- a/config/jikan.php +++ b/config/jikan.php @@ -1,6 +1,7 @@ env('MICROCACHING', false), 'default_cache_expire' => env('CACHE_DEFAULT_EXPIRE', 86400), 'per_endpoint_cache_ttl' => [ /** diff --git a/config/roadrunner.php b/config/roadrunner.php index 2873bac..f54cc31 100644 --- a/config/roadrunner.php +++ b/config/roadrunner.php @@ -33,6 +33,7 @@ return [ 'listeners' => [ Events\BeforeLoopStartedEvent::class => [ ...Defaults::beforeLoopStarted(), + Listeners\ResetLaravelScoutListener::class ], Events\BeforeLoopIterationEvent::class => [ @@ -86,6 +87,7 @@ return [ 'clear' => [ ...Defaults::servicesToClear(), + ...\App\Providers\AppServiceProvider::servicesToClear(), 'auth', // is not required for Laravel >= v8.35 ], diff --git a/database/factories/CharacterFactory.php b/database/factories/CharacterFactory.php index 11b47a4..b6c883d 100644 --- a/database/factories/CharacterFactory.php +++ b/database/factories/CharacterFactory.php @@ -21,7 +21,15 @@ class CharacterFactory extends JikanModelFactory return [ "mal_id" => $mal_id, "url" => $url, - "images" => [], + "images" => [ + "jpg" => [ + "image_url" => "https://cdn.myanimelist.net/images/characters/4/50197.jpg" + ], + "webp" => [ + "image_url" => "https://cdn.myanimelist.net/images/characters/4/50197.webp", + "small_image_url" => "https://cdn.myanimelist.net/images/characters/4/50197t.webp" + ] + ], "name" => $this->faker->name(), "name_kanji" => "岡", "nicknames" => [], @@ -29,7 +37,20 @@ class CharacterFactory extends JikanModelFactory "about" => "test", "createdAt" => new UTCDateTime(), "modifiedAt" => new UTCDateTime(), - "request_hash" => sprintf("request:%s:%s", "v4", $this->getItemTestUrl("character", $mal_id)) + "request_hash" => sprintf("request:%s:%s", "v4", $this->getItemTestUrl("character", $mal_id)), + "animeography" => [], + "mangaography" => [], + "voice_actors" => [ + [ + "person" => [ + "mal_id" => 11, + "url" => "https://myanimelist.net/people/11/Kouichi_Yamadera", + "images" => [], + "name" => "Yamadera, Kouichi" + ], + "language" => "Japanese" + ] + ] ]; } } diff --git a/database/factories/ClubFactory.php b/database/factories/ClubFactory.php new file mode 100644 index 0000000..3ebe59c --- /dev/null +++ b/database/factories/ClubFactory.php @@ -0,0 +1,80 @@ +createMalId(); + $url = "https://myanimelist.net/clubs.php?cid=".$mal_id; + $createdAt = Carbon::createFromTimestamp( + $this->faker->dateTime()->getTimestamp())->toAtomString(); + $modifiedAt = Carbon::createFromTimestamp( + $this->faker->dateTime()->getTimestamp())->toDateTimeString(); + + return [ + "mal_id" => $mal_id, + "url" => $url, + "images" => [ + "jpg" => [ + "image_url" => "https://cdn.myanimelist.net/images/clubs/16/222057.jpg" + ] + ], + "category" => $this->faker->randomElement(["anime", "manga", "characters"]), + "created" => $createdAt, + "createdAt" => new UTCDateTime(), + "modifiedAt" => new UTCDateTime(), + "created_at" => $createdAt, + "updated_at" => $modifiedAt, + "name" => $this->faker->name(), + "request_hash" => sprintf("request:%s:%s", "v4", $this->getItemTestUrl("club", $mal_id)), + "anime" => [ + [ + "mal_id" => $this->createMalId(), + "type" => "anime", + "name" => $this->faker->name(), + "url" => "https://myanimelist.net/anime/1/x" + ] + ], + "characters" => [ + [ + "mal_id" => $this->createMalId(), + "type" => "character", + "name" => $this->faker->name(), + "url" => "https://myanimelist.net/character/1234" + ] + ], + "manga" => [ + [ + "mal_id" => $this->createMalId(), + "type" => "manga", + "name" => $this->faker->name(), + "url" => "https://myanimelist.net/manga/1/x" + ] + ], + "staff" => [ + [ + "url" => "https://myanimelist.net/profile/cyruz", + "username" => "cryuz" + ] + ], + "members" => $this->faker->numberBetween(1, 9999), + "access" => $this->faker->randomElement(["public", "private"]) + ]; + } +} diff --git a/database/factories/JikanModelFactory.php b/database/factories/JikanModelFactory.php index 5657809..2abd261 100644 --- a/database/factories/JikanModelFactory.php +++ b/database/factories/JikanModelFactory.php @@ -2,6 +2,8 @@ namespace Database\Factories; +use App\CarbonDateRange; +use Jikan\Model\Common\DateRange; use JMS\Serializer\Serializer; use \Illuminate\Database\Eloquent\Factories\Factory; use Spatie\Enum\Laravel\Faker\FakerEnumProvider; @@ -28,7 +30,16 @@ abstract class JikanModelFactory extends Factory * @var Serializer $serializer */ $serializer = app("SerializerV4"); - return $serializer->toArray($stateDefinition); + $translated = array_merge(array(), $stateDefinition); + foreach ($stateDefinition as $k => $v) + { + if ($v instanceof DateRange || $v instanceof CarbonDateRange) + { + $converted = $serializer->toArray([$k => $v]); + $translated[$k] = $converted[$k]; + } + } + return $translated; } protected abstract function definitionInternal(): array; diff --git a/database/factories/MagazineFactory.php b/database/factories/MagazineFactory.php new file mode 100644 index 0000000..5ab55fe --- /dev/null +++ b/database/factories/MagazineFactory.php @@ -0,0 +1,33 @@ +createMalId(); + $name = $this->createTitle(); + $url = $this->createMalUrl($mal_id, "manga/magazine"); + $count = $this->faker->numberBetween(1, 999); + + return [ + "mal_id" => $mal_id, + "name" => $name, + "url" => $url, + "count" => $count + ]; + } +} diff --git a/docker/base_image/php-8.1/Dockerfile b/docker/base_image/php-8.1/Dockerfile index a136d7d..34f81fa 100644 --- a/docker/base_image/php-8.1/Dockerfile +++ b/docker/base_image/php-8.1/Dockerfile @@ -1,13 +1,16 @@ -FROM spiralscout/roadrunner:2.10.6 as roadrunner -FROM composer:2.3.9 as composer -FROM mlocati/php-extension-installer:1.5.29 as php-ext-installer -FROM php:8.1-bullseye as runtime +FROM spiralscout/roadrunner:2.12.2 as roadrunner +FROM composer:2.5.1 as composer +FROM mlocati/php-extension-installer:1.5.52 as php-ext-installer +FROM php:8.1.13-bullseye as runtime ARG GITHUB_PERSONAL_TOKEN LABEL org.opencontainers.image.source=https://github.com/jikan-me/jikan-rest/docker/base_image/php-8.1 COPY --from=composer /usr/bin/composer /usr/bin/composer COPY --from=php-ext-installer /usr/bin/install-php-extensions /usr/local/bin/ ENV COMPOSER_HOME="/tmp/composer" -RUN install-php-extensions gd exif intl bz2 gettext mongodb-stable redis opcache sockets pcntl +RUN set -x \ + && install-php-extensions gd exif intl bz2 gettext mongodb-stable redis opcache sockets pcntl \ + # install xdebug (for testing with code coverage), but do not enable it + && IPE_DONT_ENABLE=1 install-php-extensions xdebug-3.2.0 # install roadrunner COPY --from=roadrunner /usr/bin/rr /usr/bin/rr diff --git a/phpunit.xml b/phpunit.xml index d3029c2..664b0f9 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -10,16 +10,33 @@ ./tests/HttpV4/ - ./tests/integration/ + ./tests/Integration/ - ./tests/unit/ + ./tests/Unit/ - + ./app + + ./vendor + ./tests + ./storage + ./resources + ./docker + ./bootstrap + ./config + ./routes + ./.github + + + + + + + diff --git a/storage/app/.gitignore b/storage/app/.gitignore index 0407052..ce9fcc1 100644 --- a/storage/app/.gitignore +++ b/storage/app/.gitignore @@ -1,3 +1,4 @@ !.gitignore failovers.json -source_failover.lock \ No newline at end of file +source_failover.lock +jikan_model_classes.json diff --git a/tests/HttpV4/Controllers/AnimeControllerTest.php b/tests/HttpV4/Controllers/AnimeControllerTest.php index bb2ca7b..867c7cb 100644 --- a/tests/HttpV4/Controllers/AnimeControllerTest.php +++ b/tests/HttpV4/Controllers/AnimeControllerTest.php @@ -1,12 +1,93 @@ -searchIndexModelCleanupList = ["App\\Anime"]; + } + + private function givenDummyCharactersStaffData($uri) + { + DB::table("anime_characters_staff")->insert([ + "createdAt" => new UTCDateTime(), + "modifiedAt" => new UTCDateTime(), + "request_hash" => "request:anime:" . sha1($uri), + "characters" => [ + [ + "character" => [ + "mal_id" => 3, + "url" => "https://myanimelist.net/character/3/Jet_Black", + "images" => [ + "jpg" => [ + "image_url" => "https://cdn.myanimelist.net/images/characters/11/253723.jpg?s=6c8a19a79a88c46ae15f30e3ef5fd839", + "small_image_url" => "https://cdn.myanimelist.net/images/characters/11/253723t.jpg?s=6c8a19a79a88c46ae15f30e3ef5fd839" + ], + "webp" => [ + "image_url" => "https://cdn.myanimelist.net/images/characters/11/253723.webp?s=6c8a19a79a88c46ae15f30e3ef5fd839", + "small_image_url" => "https://cdn.myanimelist.net/images/characters/11/253723t.webp?s=6c8a19a79a88c46ae15f30e3ef5fd839" + ] + ], + "name" => "Black, Jet" + ], + "role" => "Main", + "favorites" => 1, + "voice_actors" => [ + [ + "person" => [ + "mal_id" => 357, + "url" => "https://myanimelist.net/people/357/Unshou_Ishizuk", + "images" => [ + "jpg" => [ + "image_url" => "https://cdn.myanimelist.net/images/voiceactors/2/17135.jpg?s=5925123b8a7cf9b51a445c225442f0ef" + ] + ], + "name" => "Ishizuka, Unshou" + ], + "language" => "Japanese" + ] + ] + ] + ], + "staff" => [ + [ + "person" => [ + "mal_id" => 40009, + "url" => "https://myanimelist.net/people/40009/Yutaka_Maseba", + "images" => [ + "jpg" => [ + "image_url" => "https://cdn.myanimelist.net/images/voiceactors/3/40216.jpg?s=d9fb7a625868ec7d9cd3804fa0da3fd6" + ] + ], + "name" => "Maseba, Yutaka" + ], + "positions" => [ + "Producer" + ] + ] + ] + ]); + } + public function testMain() { - $this->get('/v4/anime/1') + Anime::factory(1)->create([ + "mal_id" => 1 + ]); + $this->getJson('/v4/anime/1') ->seeStatusCode(200) ->seeJsonStructure(['data' => [ 'mal_id', @@ -116,16 +197,20 @@ class AnimeControllerTest extends TestCase public function testCharacters() { - $this->get('/v4/anime/1/characters') + // let's avoid sending request to MAL in tests + $this->givenDummyCharactersStaffData("/v4/anime/1/characters"); + + $this->getJson('/v4/anime/1/characters') ->seeStatusCode(200) - ->seeJsonStructure(['data'=>[ + ->seeJsonStructure(['data' => [ [ 'character' => [ 'mal_id', 'url', 'images' => [ 'jpg' => [ - 'image_url' + 'image_url', + 'small_image_url', ], 'webp' => [ 'image_url', @@ -155,7 +240,8 @@ class AnimeControllerTest extends TestCase public function testStaff() { - $this->get('/v4/anime/1/staff') + $this->givenDummyCharactersStaffData('/v4/anime/1/staff'); + $this->getJson('/v4/anime/1/staff') ->seeStatusCode(200) ->seeJsonStructure(['data'=>[ [ @@ -172,374 +258,4 @@ class AnimeControllerTest extends TestCase ] ]]); } - - public function testEpisodes() - { - $this->get('/v4/anime/1/episodes') - ->seeStatusCode(200) - ->seeJsonStructure([ - 'pagination' => [ - 'last_visible_page', - 'has_next_page', - ], - 'data' => [ - [ - 'mal_id', - 'url', - 'title', - 'title_japanese', - 'title_romanji', - 'aired', - 'score', - 'filler', - 'recap', - 'forum_url' - ] - ] - ]); - - $this->get('/v4/anime/21/episodes?page=2') - ->seeStatusCode(200) - ->seeJson([ - 'pagination' => [ - 'last_visible_page', - 'has_next_page', - ], - 'data' => [ - [ - 'mal_id', - 'url', - 'title', - 'title_japanese', - 'title_romanji', - 'aired', - 'score', - 'filler', - 'recap', - 'forum_url' - ] - ] - ]); - } - - public function testEpisode() - { - $this->get('/v4/anime/21/episodes/1') - ->seeStatusCode(200) - ->seeJsonStructure([ - 'data' => [ - 'mal_id', - 'url', - 'title', - 'title_japanese', - 'title_romanji', - 'duration', - 'aired', - 'aired', - 'filler', - 'recap', - 'synopsis', - ] - ]); - } - - public function testNews() - { - $this->get('/v4/anime/1/news') - ->seeStatusCode(200) - ->seeJsonStructure([ - 'pagination' => [ - 'last_visible_page', - 'has_next_page', - ], - 'data' => [ - [ - 'mal_id', - 'url', - 'title', - 'date', - 'author_username', - 'author_url', - 'forum_url', - 'images' => [ - 'jpg' => [ - 'image_url', - ], - ], - 'comments', - 'excerpt' - ] - ] - ]); - } - - public function testPictures() - { - $this->get('/v4/anime/1/pictures') - ->seeStatusCode(200) - ->seeJsonStructure([ - 'data' => [ - [ - 'jpg' => [ - 'image_url', - 'large_image_url', - 'small_image_url', - ], - 'webp' => [ - 'image_url', - 'large_image_url', - 'small_image_url', - ] - ] - ] - ]); - } - - public function testVideos() - { - $this->get('/v4/anime/1/videos') - ->seeStatusCode(200) - ->seeJsonStructure(['data'=>[ - 'promo' => [ - [ - 'title', - 'trailer' => [ - 'youtube_id', - 'url', - 'embed_url', - 'images' => [ - 'image_url', - 'small_image_url', - 'medium_image_url', - 'large_image_url', - 'maximum_image_url', - ] - ], - ] - ], - 'episodes' => [ - [ - 'mal_id', - 'title', - 'episode', - 'url', - 'images' => [ - 'jpg' => [ - 'image_url', - ], - ], - ] - ] - ]]); - } - - public function testStats() - { - $this->get('/v4/anime/21/statistics') - ->seeStatusCode(200) - ->seeJsonStructure(['data'=>[ - 'watching', - 'completed', - 'on_hold', - 'dropped', - 'plan_to_watch', - 'total', - 'scores' => [ - [ - 'score', - 'votes', - 'percentage' - ] - ] - ]]); - } - - public function testForum() - { - $this->get('/v4/anime/1/forum') - ->seeStatusCode(200) - ->seeJsonStructure([ - 'data' => [ - [ - 'mal_id', - 'url', - 'title', - 'date', - 'author_username', - 'author_url', - 'comments', - 'last_comment' => [ - 'url', - 'author_username', - 'author_url', - 'date' - ] - ] - ] - ]); - } - - public function testMoreInfo() - { - $this->get('/v4/anime/1/moreinfo') - ->seeStatusCode(200) - ->seeJsonStructure(['data'=>[ - 'moreinfo' - ]]); - } - - public function testReviews() - { - $this->get('/v4/anime/1/reviews') - ->seeStatusCode(200) - ->seeJsonStructure([ - 'pagination' => [ - 'last_visible_page', - 'has_next_page', - ], - 'data' => [ - [ - 'mal_id', - 'url', - 'type', - 'reactions', - 'date', - 'review', - 'score', - 'tags', - 'is_spoiler', - 'is_preliminary', - 'episodes_watched', - 'user' => [ - 'url', - 'username', - 'images' => [ - 'jpg' => [ - 'image_url', - ], - 'webp' => [ - 'image_url', - ], - ], - ] - ] - ] - ]); - - $this->get('/v4/anime/1/reviews?page=100') - ->seeStatusCode(404) - ->seeJsonStructure([ - 'pagination' => [ - 'last_visible_page', - 'has_next_page', - ], - 'data' => [] - ]); - } - - public function testRecommendations() - { - $this->get('/v4/anime/1/recommendations') - ->seeStatusCode(200) - ->seeJsonStructure([ - 'data' => [ - [ - 'entry' => [ - 'mal_id', - 'url', - 'images' => [ - 'jpg' => [ - 'image_url', - 'small_image_url', - 'large_image_url' - ], - 'webp' => [ - 'image_url', - 'small_image_url', - 'large_image_url' - ], - ], - 'title' - ], - 'url', - 'votes', - ] - ] - ]); - } - - public function testAnimeUserUpdates() - { - $this->get('/v4/anime/1/userupdates') - ->seeStatusCode(200) - ->seeJsonStructure([ - 'pagination' => [ - 'last_visible_page', - 'has_next_page', - ], - 'data' => [ - [ - 'user' => [ - 'username', - 'url', - 'images' => [ - 'jpg' => [ - 'image_url', - ], - 'webp' => [ - 'image_url', - ], - ], - ], - 'score', - 'status', - 'episodes_seen', - 'episodes_total', - 'date' - ] - ] - ]); - - $this->get('/v4/anime/1/userupdates?page=200') - ->seeStatusCode(404); - } - - public function testAnimeRelations() - { - $this->get('/v4/anime/1/relations') - ->seeStatusCode(200) - ->seeJsonStructure([ - 'data' => [ - [ - 'relation', - 'entry' => [ - [ - 'mal_id', - 'type', - 'name', - 'url' - ] - ], - ] - ] - ]); - } - - public function testAnimeThemes() - { - $this->get('/v4/anime/1/themes') - ->seeStatusCode(200) - ->seeJsonStructure([ - 'data' => [ - 'openings', - 'endings', - ] - ]); - } - - public function test404() - { - $this->get('/v4/anime/2') - ->seeStatusCode(404); - } } diff --git a/tests/HttpV4/Controllers/CharacterControllerTest.php b/tests/HttpV4/Controllers/CharacterControllerTest.php index 1a05751..add966d 100644 --- a/tests/HttpV4/Controllers/CharacterControllerTest.php +++ b/tests/HttpV4/Controllers/CharacterControllerTest.php @@ -1,12 +1,21 @@ createOne([ + "mal_id" => 1 + ]); $this->get('/v4/characters/1') ->seeStatusCode(200) ->seeJsonStructure(['data'=>[ @@ -29,6 +38,20 @@ class CharacterControllerTest extends TestCase public function testAnimeography() { + Character::factory()->createOne([ + "mal_id" => 1, + "animeography" => [ + [ + "role" => "Main", + "anime" => [ + "mal_id" => 1, + "url" => "https://myanimelist.net/anime/1/Cowboy_Bebop", + "images" => [], + "title" => "Cowboy Bebop" + ] + ] + ] + ]); $this->get('/v4/characters/1/anime') ->seeStatusCode(200) ->seeJsonStructure(['data'=>[ @@ -57,6 +80,20 @@ class CharacterControllerTest extends TestCase public function testMangaography() { + Character::factory()->createOne([ + "mal_id" => 1, + "mangaography" => [ + [ + "role" => "Main", + "manga" => [ + "mal_id" => 1, + "url" => "https://myanimelist.net/anime/1/Cowboy_Bebop", + "images" => [], + "title" => "Cowboy Bebop" + ] + ] + ] + ]); $this->get('/v4/characters/1/manga') ->seeStatusCode(200) ->seeJsonStructure(['data'=>[ @@ -85,6 +122,9 @@ class CharacterControllerTest extends TestCase public function testVoices() { + Character::factory()->createOne([ + "mal_id" => 1 + ]); $this->get('/v4/characters/1/voices') ->seeStatusCode(200) ->seeJsonStructure(['data'=>[ @@ -106,6 +146,9 @@ class CharacterControllerTest extends TestCase public function testPictures() { + Character::factory()->createOne([ + "mal_id" => 1 + ]); $this->get('/v4/characters/1/pictures') ->seeStatusCode(200) ->seeJsonStructure([ @@ -121,6 +164,7 @@ class CharacterControllerTest extends TestCase public function test404() { + $this->mockJikanParserWith404RespondingUpstream(); $this->get('/v4/characters/1000000') ->seeStatusCode(404); } diff --git a/tests/HttpV4/Controllers/ClubControllerTest.php b/tests/HttpV4/Controllers/ClubControllerTest.php index b042fb7..41ed78a 100644 --- a/tests/HttpV4/Controllers/ClubControllerTest.php +++ b/tests/HttpV4/Controllers/ClubControllerTest.php @@ -1,13 +1,23 @@ get('/v4/clubs/1') - ->seeStatusCode(200) + Club::factory()->createOne([ + "mal_id" => 1 + ]); + $t = $this->get('/v4/clubs/1'); + $t->seeStatusCode(200) ->seeJsonStructure(['data'=>[ 'mal_id', 'name', @@ -26,6 +36,32 @@ class ClubControllerTest extends TestCase public function testMembers() { + $m = Club::factory()->createOne([ + "mal_id" => 1 + ]); + $dummyUsername = $this->faker->userName(); + DB::table("clubs_members")->insert([ + // we are just copying the data from the manufactured model out of convenience + "createdAt" => $m->createdAt, + "modifiedAt" => $m->modifiedAt, + "has_next_page" => false, + "last_visible_page" => 1, + "request_hash" => "request:clubs:".sha1("/v4/clubs/1/members"), + "results" => [ + [ + "username" => $this->faker->userName(), + "url" => "https://myanimelist.net/profile/".$dummyUsername, + "images" => [ + "jpg" => [ + "image_url" => "http://httpbin.org/get" + ], + "webp" => [ + "image_url" => "http://httpbin.org/get" + ] + ] + ] + ] + ]); $this->get('/v4/clubs/1/members') ->seeStatusCode(200) ->seeJsonStructure([ @@ -55,6 +91,7 @@ class ClubControllerTest extends TestCase public function test404() { + $this->mockJikanParserWith404RespondingUpstream(); $this->get('/v4/clubs/1000000') ->seeStatusCode(404); } diff --git a/tests/HttpV4/Controllers/GenreControllerTest.php b/tests/HttpV4/Controllers/GenreControllerTest.php index d1b41b4..28b4bc4 100644 --- a/tests/HttpV4/Controllers/GenreControllerTest.php +++ b/tests/HttpV4/Controllers/GenreControllerTest.php @@ -1,9 +1,14 @@ get('/v4/genres/anime') @@ -34,6 +39,7 @@ class GenreControllerTest extends TestCase public function test404() { + $this->mockJikanParserWith404RespondingUpstream(); $this->get('/v4/genres') ->seeStatusCode(404); } diff --git a/tests/HttpV4/Controllers/MagazineControllerTest.php b/tests/HttpV4/Controllers/MagazineControllerTest.php index 166f0f6..ec5cc8c 100644 --- a/tests/HttpV4/Controllers/MagazineControllerTest.php +++ b/tests/HttpV4/Controllers/MagazineControllerTest.php @@ -1,14 +1,21 @@ get('/v4/magazines') - ->seeStatusCode(200) - ->seeJsonStructure(['data'=>[ + Magazine::factory(1)->create(); + $test = $this->get('/v4/magazines'); + $test->seeStatusCode(200) + ->seeJsonStructure(['data' => [ [ 'mal_id', 'name', diff --git a/tests/HttpV4/Controllers/MangaControllerTest.php b/tests/HttpV4/Controllers/MangaControllerTest.php index c735f3d..2895485 100644 --- a/tests/HttpV4/Controllers/MangaControllerTest.php +++ b/tests/HttpV4/Controllers/MangaControllerTest.php @@ -1,9 +1,14 @@ get('/v4/manga/1') diff --git a/tests/HttpV4/Controllers/PersonControllerTest.php b/tests/HttpV4/Controllers/PersonControllerTest.php index 96a6c89..0a60bb7 100644 --- a/tests/HttpV4/Controllers/PersonControllerTest.php +++ b/tests/HttpV4/Controllers/PersonControllerTest.php @@ -1,9 +1,14 @@ get('/v4/people/1') diff --git a/tests/HttpV4/Controllers/ProducerControllerTest.php b/tests/HttpV4/Controllers/ProducerControllerTest.php index c204c8e..ce5bb09 100644 --- a/tests/HttpV4/Controllers/ProducerControllerTest.php +++ b/tests/HttpV4/Controllers/ProducerControllerTest.php @@ -1,9 +1,13 @@ get('/v4/schedules') diff --git a/tests/HttpV4/Controllers/SearchControllerTest.php b/tests/HttpV4/Controllers/SearchControllerTest.php index 37600ff..1ff6e9c 100644 --- a/tests/HttpV4/Controllers/SearchControllerTest.php +++ b/tests/HttpV4/Controllers/SearchControllerTest.php @@ -1,9 +1,14 @@ get('/v4/anime?order_by=id&sort=asc') diff --git a/tests/HttpV4/Controllers/TopControllerTest.php b/tests/HttpV4/Controllers/TopControllerTest.php index 7f441ec..60a756a 100644 --- a/tests/HttpV4/Controllers/TopControllerTest.php +++ b/tests/HttpV4/Controllers/TopControllerTest.php @@ -1,9 +1,14 @@ get('/v4/top/anime') diff --git a/tests/HttpV4/Controllers/UserControllerTest.php b/tests/HttpV4/Controllers/UserControllerTest.php index e6a29a0..ece9541 100644 --- a/tests/HttpV4/Controllers/UserControllerTest.php +++ b/tests/HttpV4/Controllers/UserControllerTest.php @@ -1,9 +1,14 @@ get('/v4/users/nekomata1037') diff --git a/tests/HttpV4/Controllers/WatchControllerTest.php b/tests/HttpV4/Controllers/WatchControllerTest.php index bb186ae..cb6ad02 100644 --- a/tests/HttpV4/Controllers/WatchControllerTest.php +++ b/tests/HttpV4/Controllers/WatchControllerTest.php @@ -1,9 +1,13 @@ searchIndexModelCleanupList = ["App\\Anime"]; - } - - private function givenDummyCharactersStaffData($uri) - { - DB::table("anime_characters_staff")->insert([ - "createdAt" => new UTCDateTime(), - "modifiedAt" => new UTCDateTime(), - "request_hash" => "request:anime:" . sha1($uri), - "characters" => [ - [ - "character" => [ - "mal_id" => 3, - "url" => "https://myanimelist.net/character/3/Jet_Black", - "images" => [ - "jpg" => [ - "image_url" => "https://cdn.myanimelist.net/images/characters/11/253723.jpg?s=6c8a19a79a88c46ae15f30e3ef5fd839", - "small_image_url" => "https://cdn.myanimelist.net/images/characters/11/253723t.jpg?s=6c8a19a79a88c46ae15f30e3ef5fd839" - ], - "webp" => [ - "image_url" => "https://cdn.myanimelist.net/images/characters/11/253723.webp?s=6c8a19a79a88c46ae15f30e3ef5fd839", - "small_image_url" => "https://cdn.myanimelist.net/images/characters/11/253723t.webp?s=6c8a19a79a88c46ae15f30e3ef5fd839" - ] - ], - "name" => "Black, Jet" - ], - "role" => "Main", - "favorites" => 1, - "voice_actors" => [ - [ - "person" => [ - "mal_id" => 357, - "url" => "https://myanimelist.net/people/357/Unshou_Ishizuk", - "images" => [ - "jpg" => [ - "image_url" => "https://cdn.myanimelist.net/images/voiceactors/2/17135.jpg?s=5925123b8a7cf9b51a445c225442f0ef" - ] - ], - "name" => "Ishizuka, Unshou" - ], - "language" => "Japanese" - ] - ] - ] - ], - "staff" => [ - [ - "person" => [ - "mal_id" => 40009, - "url" => "https://myanimelist.net/people/40009/Yutaka_Maseba", - "images" => [ - "jpg" => [ - "image_url" => "https://cdn.myanimelist.net/images/voiceactors/3/40216.jpg?s=d9fb7a625868ec7d9cd3804fa0da3fd6" - ] - ], - "name" => "Maseba, Yutaka" - ], - "positions" => [ - "Producer" - ] - ] - ] - ]); - } - - public function testMain() - { - Anime::factory(1)->create([ - "mal_id" => 1 - ]); - $this->getJson('/v4/anime/1') - ->seeStatusCode(200) - ->seeJsonStructure(['data' => [ - 'mal_id', - 'url', - 'images' => [ - 'jpg' => [ - 'image_url', - 'small_image_url', - 'large_image_url' - ], - 'webp' => [ - 'image_url', - 'small_image_url', - 'large_image_url' - ], - ], - 'trailer' => [ - 'youtube_id', - 'url', - 'embed_url', - 'images' => [ - 'image_url', - 'small_image_url', - 'medium_image_url', - 'large_image_url', - 'maximum_image_url', - ] - ], - 'title', - 'title_english', - 'title_japanese', - 'title_synonyms', - 'type', - 'source', - 'episodes', - 'status', - 'airing', - 'aired' => [ - 'from', - 'to', - 'prop' => [ - 'from' => [ - 'day', - 'month', - 'year' - ], - 'to' => [ - 'day', - 'month', - 'year' - ] - ], - 'string' - ], - 'duration', - 'rating', - 'score', - 'scored_by', - 'rank', - 'popularity', - 'members', - 'favorites', - 'synopsis', - 'background', - 'season', - 'year', - 'broadcast' => [ - 'day', - 'time', - 'timezone', - 'string' - ], - 'producers' => [ - [ - 'mal_id', - 'type', - 'name', - 'url' - ] - ], - 'licensors' => [ - [ - 'mal_id', - 'type', - 'name', - 'url' - ] - ], - 'studios' => [ - [ - 'mal_id', - 'type', - 'name', - 'url' - ] - ], - 'genres' => [ - [ - 'mal_id', - 'type', - 'name', - 'url' - ] - ], - ]]); - } - - public function testCharacters() - { - // let's avoid sending request to MAL in tests - $this->givenDummyCharactersStaffData("/v4/anime/1/characters"); - - $this->getJson('/v4/anime/1/characters') - ->seeStatusCode(200) - ->seeJsonStructure(['data' => [ - [ - 'character' => [ - 'mal_id', - 'url', - 'images' => [ - 'jpg' => [ - 'image_url', - 'small_image_url', - ], - 'webp' => [ - 'image_url', - 'small_image_url', - ], - ], - 'name', - ], - 'role', - 'voice_actors' => [ - [ - 'person' => [ - 'mal_id', - 'images' => [ - 'jpg' => [ - 'image_url', - ], - ], - 'name' - ], - 'language' - ] - ] - ] - ]]); - } - - public function testStaff() - { - $this->givenDummyCharactersStaffData('/v4/anime/1/staff'); - $this->getJson('/v4/anime/1/staff') - ->seeStatusCode(200) - ->seeJsonStructure(['data'=>[ - [ - 'person' => [ - 'mal_id', - 'images' => [ - 'jpg' => [ - 'image_url', - ], - ], - 'name' - ], - 'positions' - ] - ]]); - } -} diff --git a/tests/IntegrationTestListener.php b/tests/IntegrationTestListener.php index 651a499..3264c81 100644 --- a/tests/IntegrationTestListener.php +++ b/tests/IntegrationTestListener.php @@ -1,19 +1,21 @@ set('database.connections.mongodb.database', $database === 'jikan' ? 'jikan_tests' : $database); + $app['config']->set('jikan.micro_caching_enabled', false); $this->app = $app; } @@ -41,10 +43,18 @@ class IntegrationTestListener implements TestListener { } + private function isIntegrationTest(\PHPUnit\Framework\TestSuite $suite): bool + { + $suiteName = $suite->getName(); + return in_array($suiteName, [ + "integration", "http-integration", "Tests\HttpV4\Controllers", "Tests\Integration", + "Integration" + ]); + } + public function startTestSuite(\PHPUnit\Framework\TestSuite $suite): void { - echo $suite->getName(); - if ($suite->getName() == "integration") { + if ($this->isIntegrationTest($suite)) { $app = $this->app; $kernel = $app->make( 'Illuminate\Contracts\Console\Kernel' @@ -53,13 +63,14 @@ class IntegrationTestListener implements TestListener $kernel->call('migrate:fresh', []); } catch (\Exception $ex) { print_r($ex->getMessage()); + throw $ex; } } } public function endTestSuite(\PHPUnit\Framework\TestSuite $suite): void { - if ($suite->getName() == "integration") { + if ($this->isIntegrationTest($suite)) { $app = $this->app; $kernel = $app->make( 'Illuminate\Contracts\Console\Kernel' @@ -70,11 +81,9 @@ class IntegrationTestListener implements TestListener public function startTest(\PHPUnit\Framework\Test $test): void { - // TODO: Implement startTest() method. } public function endTest(\PHPUnit\Framework\Test $test, float $time): void { - // TODO: Implement endTest() method. } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 3d389e1..6069039 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -7,8 +7,11 @@ use Faker\Factory as FakerFactory; use Faker\Generator; use Illuminate\Support\Collection; use Illuminate\Testing\TestResponse; +use Jikan\MyAnimeList\MalClient; use Laravel\Lumen\Testing\TestCase as LumenTestCase; use Spatie\Enum\Faker\FakerEnumProvider; +use Symfony\Contracts\HttpClient\HttpClientInterface; +use Symfony\Contracts\HttpClient\ResponseInterface; abstract class TestCase extends LumenTestCase { @@ -34,6 +37,7 @@ abstract class TestCase extends LumenTestCase $app = require __DIR__.'/../bootstrap/app.php'; $database = env('DB_DATABASE', 'jikan_tests'); $app['config']->set('database.connections.mongodb.database', $database === 'jikan' ? 'jikan_tests' : $database); + $app['config']->set('jikan.micro_caching_enabled', false); $app->register(TestServiceProvider::class); return $app; @@ -78,4 +82,18 @@ abstract class TestCase extends LumenTestCase $this->assertEquals(0, $expectedItems->diff($actualItems)->count()); $this->assertEquals($expectedItems->toArray(), $actualItems->toArray()); } + + protected function mockJikanParserWith404RespondingUpstream() + { + $httpClient = \Mockery::mock(HttpClientInterface::class); + $response = \Mockery::mock(ResponseInterface::class); + /** @noinspection PhpParamsInspection */ + $httpClient->allows()->request(\Mockery::any(), \Mockery::any(), \Mockery::any())->andReturn($response); + $response->allows([ + "getStatusCode" => 404, + "getHeaders" => [], + "getContent" => "" + ]); + $this->app->instance("JikanParser", new MalClient($httpClient)); + } } diff --git a/tests/Unit/.gitkeep b/tests/Unit/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/Unit/DefaultCachedScraperServiceTest.php b/tests/Unit/DefaultCachedScraperServiceTest.php index 2e64d21..c5b1484 100644 --- a/tests/Unit/DefaultCachedScraperServiceTest.php +++ b/tests/Unit/DefaultCachedScraperServiceTest.php @@ -16,7 +16,6 @@ final class DefaultCachedScraperServiceTest extends TestCase { public function tearDown(): void { - Mockery::close(); // reset time pinning Carbon::setTestNow(); }