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
This commit is contained in:
pushrbx 2023-02-02 23:37:37 +00:00
parent 0211fc4128
commit a530e9f5d6
47 changed files with 687 additions and 687 deletions

26
.codecov.yml Normal file
View File

@ -0,0 +1,26 @@
# Docs: <https://docs.codecov.io/docs/commit-status>
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

6
.gitignore vendored
View File

@ -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

View File

@ -19,8 +19,11 @@ RUN set -ex \
# enable opcache for CLI and JIT, docs: <https://www.php.net/manual/en/opcache.configuration.php#ini.opcache.jit>
&& 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 \

View File

@ -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 */

View File

@ -40,7 +40,7 @@ abstract class RequestHandlerWithScraperCache implements RequestHandler
protected function resource(Collection $results): JsonResource
{
return new ResultsResource(
$results->first()
$results->first() ?? ["results" => []]
);
}

View File

@ -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)
);

View File

@ -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;
}

View File

@ -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"];
/**

View File

@ -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
];
}
}

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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]);
}
}
}
}

View File

@ -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;

View File

@ -1,5 +1,8 @@
<?php
use App\JikanApiModel;
use PackageVersions\Versions;
use HaydenPierce\ClassFinder\ClassFinder;
require_once __DIR__.'/../vendor/autoload.php';
@ -7,3 +10,13 @@ require_once __DIR__.'/../vendor/autoload.php';
Defines
*/
defined('JIKAN_PARSER_VERSION') or define('JIKAN_PARSER_VERSION', Versions::getVersion('jikan-me/jikan'));
$classNamesCachePath = __DIR__ . "/../storage/app";
// this line only works if dev dependencies are installed
$classes = ClassFinder::getClassesInNamespace("App");
$jikanModels = array_values(
array_filter($classes, fn($class) => is_subclass_of($class, JikanApiModel::class))
);
file_put_contents($classNamesCachePath . "/jikan_model_classes.json", json_encode($jikanModels));

View File

@ -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",

52
composer.lock generated
View File

@ -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",

View File

@ -1,12 +1,21 @@
<?php
$db_username = env('DB_USERNAME', 'admin');
$dsn = "mongodb://";
if (empty($db_username)) {
$dsn .= env('DB_HOST', 'localhost').":".env('DB_PORT', 27017)."/".env('DB_ADMIN', 'admin');
}
else {
$dsn .= env('DB_USERNAME', 'admin').":".env('DB_PASSWORD', '')."@".env('DB_HOST', 'localhost').":".env('DB_PORT', 27017)."/".env('DB_ADMIN', 'admin');
}
return [
'default' => 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'),
]
],

View File

@ -1,6 +1,7 @@
<?php
return [
'micro_caching_enabled' => env('MICROCACHING', false),
'default_cache_expire' => env('CACHE_DEFAULT_EXPIRE', 86400),
'per_endpoint_cache_ttl' => [
/**

View File

@ -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
],

View File

@ -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"
]
]
];
}
}

View File

@ -0,0 +1,80 @@
<?php
namespace Database\Factories;
use App\Club;
use App\Testing\JikanDataGenerator;
use Illuminate\Support\Carbon;
use MongoDB\BSON\UTCDateTime;
final class ClubFactory extends JikanModelFactory
{
use JikanDataGenerator;
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Club::class;
protected function definitionInternal(): array
{
$mal_id = $this->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"])
];
}
}

View File

@ -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;

View File

@ -0,0 +1,33 @@
<?php
namespace Database\Factories;
use App\Magazine;
use App\Testing\JikanDataGenerator;
final class MagazineFactory extends JikanModelFactory
{
use JikanDataGenerator;
/**
* The name of the factory's corresponding model.
*
* @var string
*/
protected $model = Magazine::class;
protected function definitionInternal(): array
{
$mal_id = $this->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
];
}
}

View File

@ -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

View File

@ -10,16 +10,33 @@
<directory>./tests/HttpV4/</directory>
</testsuite>
<testsuite name="integration">
<directory>./tests/integration/</directory>
<directory>./tests/Integration/</directory>
</testsuite>
<testsuite name="unit">
<directory>./tests/unit/</directory>
<directory>./tests/Unit/</directory>
</testsuite>
</testsuites>
<coverage>
<coverage includeUncoveredFiles="false" processUncoveredFiles="true">
<include>
<directory suffix=".php">./app</directory>
</include>
<exclude>
<directory>./vendor</directory>
<directory>./tests</directory>
<directory>./storage</directory>
<directory>./resources</directory>
<directory>./docker</directory>
<directory>./bootstrap</directory>
<directory>./config</directory>
<directory>./routes</directory>
<directory>./.github</directory>
</exclude>
<report>
<html outputDirectory="./coverage/html"/>
<xml outputDirectory="./coverage/xml"/>
<clover outputFile="./coverage/clover.xml"/>
<text outputFile="php://stdout" showUncoveredFiles="false"/>
</report>
</coverage>
<listeners>
<listener class="Tests\IntegrationTestListener" />

View File

@ -1,3 +1,4 @@
!.gitignore
failovers.json
source_failover.lock
jikan_model_classes.json

View File

@ -1,12 +1,93 @@
<?php /** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\HttpV4\Controllers;
<?php
/** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\Integration;
use App\Anime;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Illuminate\Support\Facades\DB;
use MongoDB\BSON\UTCDateTime;
use Tests\TestCase;
class AnimeControllerTest extends TestCase
{
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function __construct($name = null, array $data = [], $dataName = '')
{
parent::__construct($name, $data, $dataName);
$this->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);
}
}

View File

@ -1,12 +1,21 @@
<?php /** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\HttpV4\Controllers;
use App\Character;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Tests\TestCase;
class CharacterControllerTest extends TestCase
{
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function testMain()
{
Character::factory()->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);
}

View File

@ -1,13 +1,23 @@
<?php /** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\HttpV4\Controllers;
use tests\TestCase;
use App\Club;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Illuminate\Support\Facades\DB;
use Tests\TestCase;
class ClubControllerTest extends TestCase
{
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function testMain()
{
$this->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);
}

View File

@ -1,9 +1,14 @@
<?php /** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\HttpV4\Controllers;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Tests\TestCase;
class GenreControllerTest extends TestCase
{
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function testAnimeGenre()
{
$this->get('/v4/genres/anime')
@ -34,6 +39,7 @@ class GenreControllerTest extends TestCase
public function test404()
{
$this->mockJikanParserWith404RespondingUpstream();
$this->get('/v4/genres')
->seeStatusCode(404);
}

View File

@ -1,14 +1,21 @@
<?php /** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\HttpV4\Controllers;
use App\Magazine;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Tests\TestCase;
class MagazineControllerTest extends TestCase
{
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function testMagazinesListing()
{
$this->get('/v4/magazines')
->seeStatusCode(200)
->seeJsonStructure(['data'=>[
Magazine::factory(1)->create();
$test = $this->get('/v4/magazines');
$test->seeStatusCode(200)
->seeJsonStructure(['data' => [
[
'mal_id',
'name',

View File

@ -1,9 +1,14 @@
<?php /** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\HttpV4\Controllers;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Tests\TestCase;
class MangaControllerTest extends TestCase
{
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function testMain()
{
$this->get('/v4/manga/1')

View File

@ -1,9 +1,14 @@
<?php /** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\HttpV4\Controllers;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Tests\TestCase;
class PersonControllerTest extends TestCase
{
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function testMain()
{
$this->get('/v4/people/1')

View File

@ -1,9 +1,13 @@
<?php /** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\HttpV4\Controllers;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Tests\TestCase;
class ProducerControllerTest extends TestCase
{
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function testProducersListing()
{

View File

@ -1,10 +1,14 @@
<?php /** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\HttpV4\Controllers;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Tests\TestCase;
class RecommendationsControllerTest extends TestCase
{
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function testAnimeRecommendations()
{

View File

@ -1,9 +1,13 @@
<?php /** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\HttpV4\Controllers;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Tests\TestCase;
class ReviewsControllerTest extends TestCase
{
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function testAnimeReviews()
{

View File

@ -1,9 +1,14 @@
<?php /** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\HttpV4\Controllers;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Tests\TestCase;
class ScheduleControllerTest extends TestCase
{
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function testSchedule()
{
$this->get('/v4/schedules')

View File

@ -1,9 +1,14 @@
<?php /** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\HttpV4\Controllers;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Tests\TestCase;
class SearchControllerTest extends TestCase
{
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function testAnimeSearch()
{
$this->get('/v4/anime?order_by=id&sort=asc')

View File

@ -1,9 +1,14 @@
<?php /** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\HttpV4\Controllers;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Tests\TestCase;
class TopControllerTest extends TestCase
{
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function testTopAnime()
{
$this->get('/v4/top/anime')

View File

@ -1,9 +1,14 @@
<?php /** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\HttpV4\Controllers;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Tests\TestCase;
class UserControllerTest extends TestCase
{
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function testUserProfile()
{
$this->get('/v4/users/nekomata1037')

View File

@ -1,9 +1,13 @@
<?php /** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\HttpV4\Controllers;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Tests\TestCase;
class WatchControllerTest extends TestCase
{
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function testWatchEpisodes()
{

View File

@ -1,261 +0,0 @@
<?php
/** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests\Integration;
use App\Anime;
use App\Testing\ScoutFlush;
use App\Testing\SyntheticMongoDbTransaction;
use Illuminate\Support\Facades\DB;
use MongoDB\BSON\UTCDateTime;
use Tests\TestCase;
class AnimeControllerTest extends TestCase
{
use SyntheticMongoDbTransaction;
use ScoutFlush;
public function __construct($name = null, array $data = [], $dataName = '')
{
parent::__construct($name, $data, $dataName);
$this->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'
]
]]);
}
}

View File

@ -1,19 +1,21 @@
<?php /** @noinspection PhpIllegalPsrClassPathInspection */
namespace Tests;
use Illuminate\Support\Str;
use \Throwable;
use PHPUnit\Framework\TestListener;
// fixme: with phpunit 10, this should be replaced with the new event system
class IntegrationTestListener implements TestListener
{
private $app;
private \Laravel\Lumen\Application $app;
public function __construct()
{
$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);
$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.
}
}

View File

@ -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));
}
}

View File

View File

@ -16,7 +16,6 @@ final class DefaultCachedScraperServiceTest extends TestCase
{
public function tearDown(): void
{
Mockery::close();
// reset time pinning
Carbon::setTestNow();
}