diff --git a/README.md b/README.md index 87e8d08fc..554fc64a3 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,7 @@ BuildKit(または Docker の対応するバージョン)あるいは Builda - `docker build` を利用する場合: Docker 18.09 以上 - `docker buildx` を利用する場合: Docker 19.03 以上 -nginx + PHP-FPM + PostgreSQL + Redis で構成されています。 +nginx + PHP-FPM + PostgreSQL で構成されています。 ### 設定 @@ -105,8 +105,6 @@ $ export HOMOCHECKER_DB_SSLCERT=/path/to/sslcert $ export HOMOCHECKER_DB_SSLKEY=/path/to/sslkey $ export HOMOCHECKER_DB_SSLROOTCERT=/path/to/sslrootcert $ export HOMOCHECKER_LOG_LEVEL=debug -$ export HOMOCHECKER_REDIS_HOST=redis -$ export HOMOCHECKER_REDIS_PORT=6379 $ export HOMOCHECKER_TWITTER_CONSUMER_KEY= $ export HOMOCHECKER_TWITTER_CONSUMER_SECRET= $ export HOMOCHECKER_TWITTER_TOKEN= diff --git a/api/Dockerfile b/api/Dockerfile index 3c0adec48..6373cabed 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -98,11 +98,9 @@ RUN --mount=type=cache,id=api:/var/cache/apt,target=/var/cache/apt \ make clean && \ ln -s /etc/ssl /usr/local/ssl && \ pecl install \ - apcu \ - redis && \ + apcu && \ docker-php-ext-enable \ - apcu \ - redis && \ + apcu && \ docker-php-ext-install \ opcache \ pdo_pgsql && \ diff --git a/api/composer.json b/api/composer.json index 13f0700de..3da1bbb22 100644 --- a/api/composer.json +++ b/api/composer.json @@ -4,12 +4,10 @@ "require": { "guzzlehttp/guzzle": "^7.5", "guzzlehttp/oauth-subscriber": "^0.6.0", - "illuminate/cache": "^9.43", "illuminate/config": "^9.43", "illuminate/database": "^9.43", "illuminate/events": "^9.43", "illuminate/log": "^9.43", - "illuminate/redis": "^9.43", "middlewares/access-log": "^2.1", "phpoption/phpoption": "^1.9", "promphp/prometheus_client_php": "^2.6", diff --git a/api/composer.lock b/api/composer.lock index 5fd282407..7e5ebe176 100644 --- a/api/composer.lock +++ b/api/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": "e307145ec5b7f55206796e2eb2a4de0a", + "content-hash": "075388069cc0cc87879a8cf25e2dbd6f", "packages": [ { "name": "doctrine/inflector", @@ -658,66 +658,6 @@ }, "time": "2022-11-25T07:56:47+00:00" }, - { - "name": "illuminate/cache", - "version": "v9.43.0", - "source": { - "type": "git", - "url": "https://github.com/illuminate/cache.git", - "reference": "8ba188bcfad7d2b5ed13f8cd1ccbb67db22cfa04" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/cache/zipball/8ba188bcfad7d2b5ed13f8cd1ccbb67db22cfa04", - "reference": "8ba188bcfad7d2b5ed13f8cd1ccbb67db22cfa04", - "shasum": "" - }, - "require": { - "illuminate/collections": "^9.0", - "illuminate/contracts": "^9.0", - "illuminate/macroable": "^9.0", - "illuminate/support": "^9.0", - "php": "^8.0.2" - }, - "provide": { - "psr/simple-cache-implementation": "1.0|2.0|3.0" - }, - "suggest": { - "ext-memcached": "Required to use the memcache cache driver.", - "illuminate/database": "Required to use the database cache driver (^9.0).", - "illuminate/filesystem": "Required to use the file cache driver (^9.0).", - "illuminate/redis": "Required to use the redis cache driver (^9.0).", - "symfony/cache": "Required to use PSR-6 cache bridge (^6.0)." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "9.x-dev" - } - }, - "autoload": { - "psr-4": { - "Illuminate\\Cache\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "The Illuminate Cache package.", - "homepage": "https://laravel.com", - "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" - }, - "time": "2022-08-24T13:50:51+00:00" - }, { "name": "illuminate/collections", "version": "v9.43.0", @@ -1232,60 +1172,6 @@ }, "time": "2022-06-09T14:13:53+00:00" }, - { - "name": "illuminate/redis", - "version": "v9.43.0", - "source": { - "type": "git", - "url": "https://github.com/illuminate/redis.git", - "reference": "6e36ee3846112c82032562d7f525fedf5ad93169" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/illuminate/redis/zipball/6e36ee3846112c82032562d7f525fedf5ad93169", - "reference": "6e36ee3846112c82032562d7f525fedf5ad93169", - "shasum": "" - }, - "require": { - "illuminate/collections": "^9.0", - "illuminate/contracts": "^9.0", - "illuminate/macroable": "^9.0", - "illuminate/support": "^9.0", - "php": "^8.0.2" - }, - "suggest": { - "ext-redis": "Required to use the phpredis connector (^4.0|^5.0).", - "predis/predis": "Required to use the predis connector (^1.1.9|^2.0.2)." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "9.x-dev" - } - }, - "autoload": { - "psr-4": { - "Illuminate\\Redis\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Taylor Otwell", - "email": "taylor@laravel.com" - } - ], - "description": "The Illuminate Redis package.", - "homepage": "https://laravel.com", - "support": { - "issues": "https://github.com/laravel/framework/issues", - "source": "https://github.com/laravel/framework" - }, - "time": "2022-10-04T13:30:33+00:00" - }, { "name": "illuminate/support", "version": "v9.43.0", diff --git a/api/src/Contracts/Repository/AltsvcRepository.php b/api/src/Contracts/Repository/AltsvcRepository.php new file mode 100644 index 000000000..bbcca7e96 --- /dev/null +++ b/api/src/Contracts/Repository/AltsvcRepository.php @@ -0,0 +1,21 @@ +setScreenName($homo->screen_name ?? null); $this->setService($homo->service ?? null); $this->setUrl($homo->url ?? null); + + if (isset($homo->icon_url)) { + $this->setProfile(new Profile(['icon_url' => $homo->icon_url])); + } } /** @@ -109,4 +118,22 @@ public function setUrl(?string $url): void { $this->url = $url; } + + /** + * Get the profile. + * @return ?Profile The profile. + */ + public function getProfile(): ?Profile + { + return $this->profile; + } + + /** + * Set the profile. + * @param ?Profile $profile The profile. + */ + public function setProfile(?Profile $profile): void + { + $this->profile = $profile; + } } diff --git a/api/src/Domain/Profile.php b/api/src/Domain/Profile.php new file mode 100644 index 000000000..049d6e092 --- /dev/null +++ b/api/src/Domain/Profile.php @@ -0,0 +1,39 @@ +setIconUrl($profile->icon_url ?? null); + } + + /** + * Get the icon URL. + * @return ?string The icon URL. + */ + public function getIconUrl(): ?string + { + return $this->iconUrl; + } + + /** + * Set the icon URL. + */ + public function setIconUrl(?string $iconUrl): void + { + $this->iconUrl = $iconUrl; + } +} diff --git a/api/src/Providers/HomoAppProvider.php b/api/src/Providers/HomoAppProvider.php index b1e0d8f2c..97c054aa2 100644 --- a/api/src/Providers/HomoAppProvider.php +++ b/api/src/Providers/HomoAppProvider.php @@ -8,12 +8,16 @@ use HomoChecker\Action\HealthCheckAction; use HomoChecker\Action\ListAction; use HomoChecker\Action\MetricsAction; +use HomoChecker\Contracts\Repository\AltsvcRepository as AltsvcRepositoryContract; use HomoChecker\Contracts\Repository\HomoRepository as HomoRepositoryContract; +use HomoChecker\Contracts\Repository\ProfileRepository as ProfileRepositoryContract; use HomoChecker\Http\NonBufferedBody; use HomoChecker\Middleware\AccessLogMiddleware; use HomoChecker\Middleware\ErrorMiddleware; use HomoChecker\Middleware\MetricsMiddleware; +use HomoChecker\Repository\AltsvcRepository; use HomoChecker\Repository\HomoRepository; +use HomoChecker\Repository\ProfileRepository; use Illuminate\Config\Repository; use Illuminate\Contracts\Container\Container; use Illuminate\Support\ServiceProvider; @@ -35,7 +39,9 @@ public function register() $this->app->singleton(ListAction::class); $this->app->singleton(BadgeAction::class); + $this->app->singleton(AltsvcRepositoryContract::class, AltsvcRepository::class); $this->app->singleton(HomoRepositoryContract::class, HomoRepository::class); + $this->app->singleton(ProfileRepositoryContract::class, ProfileRepository::class); $this->app->singleton('app', function (Container $app) { AppFactory::setContainer($app); diff --git a/api/src/Providers/HomoProvider.php b/api/src/Providers/HomoProvider.php index e8f69b97a..ddc3309fa 100644 --- a/api/src/Providers/HomoProvider.php +++ b/api/src/Providers/HomoProvider.php @@ -5,7 +5,6 @@ use GuzzleHttp\Client; use GuzzleHttp\ClientInterface; -use HomoChecker\Contracts\Service\CacheService as CacheServiceContract; use HomoChecker\Contracts\Service\CheckService as CheckServiceContract; use HomoChecker\Contracts\Service\ClientService as ClientServiceContract; use HomoChecker\Contracts\Service\HomoService as HomoServiceContract; @@ -13,17 +12,14 @@ use HomoChecker\Middleware\ErrorMiddleware; use HomoChecker\Middleware\MetricsMiddleware; use HomoChecker\Providers\Support\LogServiceProvider; -use HomoChecker\Service\CacheService; use HomoChecker\Service\CheckService; use HomoChecker\Service\ClientService; use HomoChecker\Service\HomoService; -use Illuminate\Cache\CacheServiceProvider; use Illuminate\Contracts\Container\Container; use Illuminate\Database\Connectors\ConnectionFactory; use Illuminate\Database\DatabaseManager; use Illuminate\Database\DatabaseServiceProvider; use Illuminate\Events\EventServiceProvider; -use Illuminate\Redis\RedisServiceProvider; use Illuminate\Support\Facades\Log; use Illuminate\Support\ServiceProvider; use Middlewares\AccessLog; @@ -77,8 +73,6 @@ public function register() ->needs('$config') ->giveConfig('client'); - $this->app->singleton(CacheServiceContract::class, CacheService::class); - $this->app->singleton(CheckServiceContract::class, fn (Container $app) => new CheckService( $app->make(ClientServiceContract::class), $app->make(HomoServiceContract::class), @@ -131,8 +125,6 @@ public function __toString() (new EventServiceProvider($this->app))->register(); (new LogServiceProvider($this->app))->register(); (new DatabaseServiceProvider($this->app))->register(); - (new CacheServiceProvider($this->app))->register(); - (new RedisServiceProvider($this->app))->register(); } public function provides() @@ -142,7 +134,6 @@ public function provides() CallableResolverInterface::class, MetricsMiddleware::class, ClientInterface::class, - CacheServiceContract::class, CheckServiceContract::class, ClientServiceContract::class, HomoServiceContract::class, diff --git a/api/src/Repository/AltsvcRepository.php b/api/src/Repository/AltsvcRepository.php new file mode 100644 index 000000000..2b06a561e --- /dev/null +++ b/api/src/Repository/AltsvcRepository.php @@ -0,0 +1,31 @@ +whereRaw('altsvcs.expires_at >= CURRENT_TIMESTAMP')->get()->all(); + } + + public function save(string $url, string $protocol, string $expiresAt): void + { + DB::table('altsvcs') + ->upsert( + [ + [ + 'url' => $url, + 'protocol' => $protocol, + 'expires_at' => $expiresAt, + ], + ], + ['url'], + ['protocol', 'expires_at'], + ); + } +} diff --git a/api/src/Repository/HomoRepository.php b/api/src/Repository/HomoRepository.php index 8e354a646..361ce14b9 100644 --- a/api/src/Repository/HomoRepository.php +++ b/api/src/Repository/HomoRepository.php @@ -4,36 +4,54 @@ namespace HomoChecker\Repository; use HomoChecker\Contracts\Repository\HomoRepository as HomoRepositoryContract; +use Illuminate\Database\Query\Builder; use Illuminate\Database\Query\Grammars\PostgresGrammar; +use Illuminate\Database\Query\JoinClause; use Illuminate\Support\Facades\DB; class HomoRepository implements HomoRepositoryContract { - protected string $table = 'users'; - public function count(): int { - return DB::table($this->table)->count(); + return DB::table('users')->count(); } public function countByScreenName(string $screenName): int { - return DB::table($this->table)->where('screen_name', $screenName)->count(); + return DB::table('users')->where('screen_name', $screenName)->count(); + } + + protected function join(): Builder + { + return DB::table('users') + ->leftJoin( + 'profiles', + fn (JoinClause $join) => $join + ->on('users.screen_name', '=', 'profiles.screen_name') + ->whereRaw('profiles.expires_at >= CURRENT_TIMESTAMP'), + ) + ->select([ + 'users.id', + 'users.screen_name', + 'users.service', + 'users.url', + 'profiles.icon_url', + ]); } public function findAll(): array { - return DB::table($this->table)->get()->all(); + return $this->join()->get()->all(); } public function findByScreenName(string $screenName): array { - return DB::table($this->table)->where('screen_name', $screenName)->get()->all(); + return $this->join()->where('screen_name', $screenName)->get()->all(); } public function export(): string { - $builder = DB::table($this->table); + $builder = DB::table('users'); // Create a Grammar instance that doesn't parameterize its values $grammar = new class() extends PostgresGrammar { @@ -46,9 +64,9 @@ public function parameter($value) } }; - return $builder->get()->map(function (\stdClass $item) use ($builder, $grammar) { - unset($item->id); - return $grammar->compileInsert($builder, (array) $item) . ';'; - })->join("\n"); + return $builder + ->get(['screen_name', 'service', 'url']) + ->map(fn (\stdClass $item) => $grammar->compileInsert($builder, (array) $item) . ";\n") + ->join(''); } } diff --git a/api/src/Repository/ProfileRepository.php b/api/src/Repository/ProfileRepository.php new file mode 100644 index 000000000..813652d93 --- /dev/null +++ b/api/src/Repository/ProfileRepository.php @@ -0,0 +1,26 @@ +upsert( + [ + [ + 'screen_name' => $screenName, + 'icon_url' => $iconURL, + 'expires_at' => $expiresAt, + ], + ], + ['screen_name'], + ['icon_url', 'expires_at'], + ); + } +} diff --git a/api/src/Service/CacheService.php b/api/src/Service/CacheService.php deleted file mode 100644 index 7c2f07a2b..000000000 --- a/api/src/Service/CacheService.php +++ /dev/null @@ -1,52 +0,0 @@ -{$feature}( - str($identifier) - ->ucsplit() - ->map(fn (string $item) => str($item)->lower()) - ->push(collect($arguments)->first()) - ->join(':'), - collect($arguments) - ->slice(1) - ->toArray(), - ); - } - - public function load(string $key, array $arguments = []): ?string - { - return Cache::get($key, ...$arguments); - } - - public function save(string $key, array $arguments = []): void - { - Cache::put($key, ...$arguments); - } -} diff --git a/api/src/Service/CheckService.php b/api/src/Service/CheckService.php index 2ac1d35af..e24f8560d 100644 --- a/api/src/Service/CheckService.php +++ b/api/src/Service/CheckService.php @@ -138,7 +138,7 @@ protected function createStatusAsync(Homo $homo, callable $callback = null): Pro /** @var Result $result */ [$result, $icon] = yield Utils::all([ $this->validateAsync($homo), - $this->profiles->get($homo->getService())->getIconAsync($homo->getScreenName()), + $homo->getProfile()?->getIconUrl() ?? $this->profiles->get($homo->getService())->getIconAsync($homo->getScreenName()), ]); $status = new Status([ 'homo' => $homo, @@ -178,7 +178,7 @@ public function execute(string $screen_name = null, callable $callback = null): return Pool::batch( $this->client, collect($this->homo->find($screen_name)) - ->map(fn (\stdClass $item) => new Homo($item)) + ->mapInto(Homo::class) ->map(fn (Homo $item) => fn () => $this->createStatusAsync($item, $callback)) ->toArray(), [ diff --git a/api/src/Service/ClientService.php b/api/src/Service/ClientService.php index b9fc6bc16..f68c885ca 100644 --- a/api/src/Service/ClientService.php +++ b/api/src/Service/ClientService.php @@ -8,17 +8,40 @@ use GuzzleHttp\Psr7\Response as Psr7Response; use GuzzleHttp\RequestOptions; use GuzzleHttp\TransferStats; -use HomoChecker\Contracts\Service\CacheService as CacheServiceContract; +use HomoChecker\Contracts\Repository\AltsvcRepository as AltsvcRepositoryContract; use HomoChecker\Contracts\Service\Client\Altsvc; use HomoChecker\Contracts\Service\Client\Response; use HomoChecker\Contracts\Service\ClientService as ClientServiceContract; +use Illuminate\Support\Collection; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; class ClientService implements ClientServiceContract { - public function __construct(protected ClientInterface $client, protected CacheServiceContract $cache, protected int $redirect) + protected ?Collection $altsvc = null; + + public function __construct( + protected ClientInterface $client, + protected AltsvcRepositoryContract $altsvcRepository, + protected int $redirect, + ) { + } + + protected function getAltsvc(string $url): null|string + { + if (!$this->altsvc) { + $this->altsvc = collect($this->altsvcRepository->findAll()); + } + return $this->altsvc->where('url', $url)->pluck('protocol')->first(); + } + + protected function saveAltsvc(string $url, string $protocolId, float $maxAge): void { + $this->altsvcRepository->save( + $url, + $protocolId, + (new \DateTimeImmutable("{$maxAge} seconds"))->format(\DateTimeInterface::ATOM), + ); } /** @@ -31,7 +54,7 @@ public function getAsync(string $url): \Generator for ($i = 0; $i < $this->redirect; ++$i) { $options = []; - if (str_starts_with($this->cache->loadAltsvc($url, ''), 'h3')) { + if (str_starts_with($this->getAltsvc($url) ?? '', 'h3')) { $options['curl'] = [ CURLOPT_CERTINFO => true, CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_3, @@ -60,7 +83,7 @@ public function getAsync(string $url): \Generator return; } - $this->cache->saveAltsvc($url, $h3->getProtocolId(), $h3->getMaxAge()); + $this->saveAltsvc($url, $h3->getProtocolId(), $h3->getMaxAge()); }, RequestOptions::ON_STATS => function (TransferStats $stats) use (&$result) { $response = $stats->getResponse(); diff --git a/api/src/Service/Profile/MastodonProfileService.php b/api/src/Service/Profile/MastodonProfileService.php index 1dd5faa1f..fa43dc8a7 100644 --- a/api/src/Service/Profile/MastodonProfileService.php +++ b/api/src/Service/Profile/MastodonProfileService.php @@ -6,7 +6,7 @@ use GuzzleHttp\ClientInterface; use GuzzleHttp\Promise\Coroutine; use GuzzleHttp\Promise\PromiseInterface; -use HomoChecker\Contracts\Service\CacheService as CacheServiceContract; +use HomoChecker\Contracts\Repository\ProfileRepository as ProfileRepositoryContract; use HomoChecker\Contracts\Service\ProfileService as ProfileServiceContract; use Illuminate\Support\Facades\Log; use Prometheus\Counter; @@ -17,7 +17,7 @@ class MastodonProfileService implements ProfileServiceContract public function __construct( protected ClientInterface $client, - protected CacheServiceContract $cache, + protected ProfileRepositoryContract $repository, protected Counter $profileErrorCounter, ) { } @@ -42,10 +42,6 @@ public function parseScreenName(string $screen_name): array public function getIconAsync(string $screen_name): PromiseInterface { return Coroutine::of(function () use ($screen_name) { - if ($url = $this->cache->loadIconMastodon($screen_name)) { - return yield $url; - } - try { [ $username, $instance ] = $this->parseScreenName($screen_name); $target = "https://{$instance}/users/{$username}.json"; @@ -56,7 +52,11 @@ public function getIconAsync(string $screen_name): PromiseInterface throw new \RuntimeException('Avatar not found'); } - $this->cache->saveIconMastodon($screen_name, $url, static::CACHE_EXPIRE); + $this->repository->save( + $screen_name, + $url, + (new \DateTimeImmutable(static::CACHE_EXPIRE . ' seconds'))->format(\DateTimeInterface::ATOM), + ); return yield $url; } catch (\Throwable $e) { Log::debug($e); diff --git a/api/src/Service/Profile/TwitterProfileService.php b/api/src/Service/Profile/TwitterProfileService.php index 8c664ddc2..e367c094a 100644 --- a/api/src/Service/Profile/TwitterProfileService.php +++ b/api/src/Service/Profile/TwitterProfileService.php @@ -6,7 +6,7 @@ use GuzzleHttp\ClientInterface; use GuzzleHttp\Promise\Coroutine; use GuzzleHttp\Promise\PromiseInterface; -use HomoChecker\Contracts\Service\CacheService as CacheServiceContract; +use HomoChecker\Contracts\Repository\ProfileRepository as ProfileRepositoryContract; use HomoChecker\Contracts\Service\ProfileService as ProfileServiceContract; use Illuminate\Support\Facades\Log; use Prometheus\Counter; @@ -17,7 +17,7 @@ class TwitterProfileService implements ProfileServiceContract public function __construct( protected ClientInterface $client, - protected CacheServiceContract $cache, + protected ProfileRepositoryContract $repository, protected Counter $profileErrorCounter, ) { } @@ -30,17 +30,17 @@ public function __construct( public function getIconAsync(string $screen_name): PromiseInterface { return Coroutine::of(function () use ($screen_name) { - if ($url = $this->cache->loadIconTwitter($screen_name)) { - return yield $url; - } - try { $target = "users/show.json?screen_name={$screen_name}"; $response = yield $this->client->getAsync($target); $user = json_decode((string) $response->getBody()); $url = str_replace('_normal', '_200x200', $user->profile_image_url_https); - $this->cache->saveIconTwitter($screen_name, $url, static::CACHE_EXPIRE); + $this->repository->save( + $screen_name, + $url, + (new \DateTimeImmutable(static::CACHE_EXPIRE . ' seconds'))->format(\DateTimeInterface::ATOM), + ); return yield $url; } catch (\Throwable $e) { Log::debug($e); diff --git a/api/src/config.php b/api/src/config.php index 7d0d08d67..5d2272380 100644 --- a/api/src/config.php +++ b/api/src/config.php @@ -4,10 +4,6 @@ use HomoChecker\Logging\CustomizeFormatter; return [ - 'cache.default' => 'default', - 'cache.stores.default' => [ - 'driver' => 'redis', - ], 'database.default' => 'default', 'database.connections' => [ 'default' => [ @@ -24,12 +20,6 @@ 'charset' => 'utf8', ], ], - 'database.redis' => [ - 'default' => [ - 'host' => env('HOMOCHECKER_REDIS_HOST'), - 'port' => (int) env('HOMOCHECKER_REDIS_PORT', 6379), - ], - ], 'logging.default' => 'default', 'logging.channels.default' => [ 'driver' => 'single', diff --git a/api/tests/Case/Domain/HomoTest.php b/api/tests/Case/Domain/HomoTest.php index ffbb4bd5d..743962a7f 100644 --- a/api/tests/Case/Domain/HomoTest.php +++ b/api/tests/Case/Domain/HomoTest.php @@ -14,17 +14,44 @@ public function testConstruct(): void $screen_name = 'homo'; $service = 'twitter'; $url = 'https://xn--ydko.example.com'; + $icon_url = 'https://pbs.twimg.com/profile_images/114514/example_bigger.jpg'; $actual = new Homo(compact( 'id', 'screen_name', 'service', 'url', + 'icon_url', )); $this->assertEquals($id, $actual->getId()); $this->assertEquals($screen_name, $actual->getScreenName()); $this->assertEquals($service, $actual->getService()); $this->assertEquals($url, $actual->getUrl()); + + $this->assertNotNull($actual->getProfile()); + $this->assertEquals($icon_url, $actual->getProfile()->getIconUrl()); + } + + public function testConstructWithoutProfile(): void + { + $id = 1; + $screen_name = 'homo'; + $service = 'twitter'; + $url = 'https://xn--ydko.example.com'; + + $actual = new Homo(compact( + 'id', + 'screen_name', + 'service', + 'url', + )); + + $this->assertEquals($id, $actual->getId()); + $this->assertEquals($screen_name, $actual->getScreenName()); + $this->assertEquals($service, $actual->getService()); + $this->assertEquals($url, $actual->getUrl()); + + $this->assertNull($actual->getProfile()); } } diff --git a/api/tests/Case/Domain/ProfileTest.php b/api/tests/Case/Domain/ProfileTest.php new file mode 100644 index 000000000..96f27afca --- /dev/null +++ b/api/tests/Case/Domain/ProfileTest.php @@ -0,0 +1,21 @@ +assertEquals($icon_url, $actual->getIconUrl()); + } +} diff --git a/api/tests/Case/Http/NonBufferedBodyTest.php b/api/tests/Case/Http/NonBufferedBodyTest.php index 802c209c7..59bc21b58 100644 --- a/api/tests/Case/Http/NonBufferedBodyTest.php +++ b/api/tests/Case/Http/NonBufferedBodyTest.php @@ -19,6 +19,7 @@ public function testToString(): void /** @var MockInterface&Psr7NonBufferedBody $base */ $base = m::mock(Psr7NonBufferedBody::class); $base->shouldReceive('__toString') + ->once() ->andReturn(''); $actual = new NonBufferedBody($base); @@ -31,6 +32,7 @@ public function testClose(): void /** @var MockInterface&Psr7NonBufferedBody $base */ $base = m::mock(Psr7NonBufferedBody::class); $base->shouldReceive('close') + ->once() ->andReturn(); $actual = new NonBufferedBody($base); @@ -42,6 +44,7 @@ public function testDetach(): void /** @var MockInterface&Psr7NonBufferedBody $base */ $base = m::mock(Psr7NonBufferedBody::class); $base->shouldReceive('detach') + ->once() ->andReturn(null); $actual = new NonBufferedBody($base); @@ -64,6 +67,7 @@ public function testTell(): void /** @var MockInterface&Psr7NonBufferedBody $base */ $base = m::mock(Psr7NonBufferedBody::class); $base->shouldReceive('tell') + ->once() ->andReturn(0); $actual = new NonBufferedBody($base); @@ -76,6 +80,7 @@ public function testEof(): void /** @var MockInterface&Psr7NonBufferedBody $base */ $base = m::mock(Psr7NonBufferedBody::class); $base->shouldReceive('eof') + ->once() ->andReturn(true); $actual = new NonBufferedBody($base); @@ -88,6 +93,7 @@ public function testIsSeekable(): void /** @var MockInterface&Psr7NonBufferedBody $base */ $base = m::mock(Psr7NonBufferedBody::class); $base->shouldReceive('isSeekable') + ->once() ->andReturn(false); $actual = new NonBufferedBody($base); @@ -100,6 +106,7 @@ public function testSeek(): void /** @var MockInterface&Psr7NonBufferedBody $base */ $base = m::mock(Psr7NonBufferedBody::class); $base->shouldReceive('seek') + ->once() ->withArgs([0, SEEK_SET]) ->andReturn(); @@ -112,6 +119,7 @@ public function testRewind(): void /** @var MockInterface&Psr7NonBufferedBody $base */ $base = m::mock(Psr7NonBufferedBody::class); $base->shouldReceive('rewind') + ->once() ->andReturn(); $actual = new NonBufferedBody($base); @@ -123,6 +131,7 @@ public function testIsWritable(): void /** @var MockInterface&Psr7NonBufferedBody $base */ $base = m::mock(Psr7NonBufferedBody::class); $base->shouldReceive('isWritable') + ->once() ->andReturn(true); $actual = new NonBufferedBody($base); @@ -136,6 +145,7 @@ public function testWrite(): void $base = m::mock(Psr7NonBufferedBody::class); $base->size = 0; $base->shouldReceive('write') + ->once() ->withArgs(['test']) ->andReturn(4); @@ -151,6 +161,7 @@ public function testIsReadable(): void /** @var MockInterface&Psr7NonBufferedBody $base */ $base = m::mock(Psr7NonBufferedBody::class); $base->shouldReceive('isReadable') + ->once() ->andReturn(false); $actual = new NonBufferedBody($base); @@ -163,6 +174,7 @@ public function testRead(): void /** @var MockInterface&Psr7NonBufferedBody $base */ $base = m::mock(Psr7NonBufferedBody::class); $base->shouldReceive('read') + ->once() ->withArgs([4]) ->andReturn('homo'); @@ -176,6 +188,7 @@ public function testGetContents(): void /** @var MockInterface&Psr7NonBufferedBody $base */ $base = m::mock(Psr7NonBufferedBody::class); $base->shouldReceive('getContents') + ->once() ->andReturn(''); $actual = new NonBufferedBody($base); @@ -188,6 +201,7 @@ public function testGetMetadata(): void /** @var MockInterface&Psr7NonBufferedBody $base */ $base = m::mock(Psr7NonBufferedBody::class); $base->shouldReceive('getMetadata') + ->once() ->withArgs([null]) ->andReturn(null); diff --git a/api/tests/Case/Repository/AltsvcRepositoryTest.php b/api/tests/Case/Repository/AltsvcRepositoryTest.php new file mode 100644 index 000000000..b5bed9a14 --- /dev/null +++ b/api/tests/Case/Repository/AltsvcRepositoryTest.php @@ -0,0 +1,80 @@ +altsvcs = [ + (object) [ + 'url' => 'http://foo.example.com/1', + 'protocol' => 'h3', + 'expires_at' => '2022-12-31 23:59:59', + ], + (object) [ + 'url' => 'https://homo.example.com', + 'protocol' => 'h3', + 'expires_at' => '2022-12-31 23:59:59', + ], + ]; + } + + public function testFindAll(): void + { + /** @var Builder&MockInterface $builder */ + $builder = m::mock(Builder::class); + $builder->shouldReceive('whereRaw->get->all') + ->andReturn($this->altsvcs); + + DB::shouldReceive('table') + ->once() + ->with('altsvcs') + ->andReturn($builder); + + $altsvc = new AltsvcRepository(); + $actual = $altsvc->findAll(); + + $this->assertEquals($this->altsvcs, $actual); + } + + public function testSave(): void + { + /** @var Builder&MockInterface $builder */ + $builder = m::mock(Builder::class); + $builder->shouldReceive('upsert') + ->withArgs([ + [ + [ + 'url' => 'https://homo.example.com', + 'protocol' => 'h3', + 'expires_at' => '2022-12-31 23:59:59', + ], + ], + ['url'], + ['protocol', 'expires_at'], + ]); + + DB::shouldReceive('table') + ->once() + ->with('altsvcs') + ->andReturn($builder); + + $altsvc = new AltsvcRepository(); + $altsvc->save('https://homo.example.com', 'h3', '2022-12-31 23:59:59'); + } +} diff --git a/api/tests/Case/Repository/HomoRepositoryTest.php b/api/tests/Case/Repository/HomoRepositoryTest.php index 19ccbcbd8..b936f6257 100644 --- a/api/tests/Case/Repository/HomoRepositoryTest.php +++ b/api/tests/Case/Repository/HomoRepositoryTest.php @@ -9,6 +9,7 @@ use Illuminate\Support\Facades\Facade; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use Mockery as m; +use Mockery\MockInterface; use PHPUnit\Framework\TestCase; class HomoRepositoryTest extends TestCase @@ -25,24 +26,28 @@ public function setUp(): void 'screen_name' => 'foo', 'service' => 'twitter', 'url' => 'https://foo.example.com/1', + 'icon_url' => 'https://pbs.twimg.com/profile_images/114514/example_bigger.jpg', ], (object) [ 'id' => 2, 'screen_name' => 'foo', 'service' => 'twitter', 'url' => 'https://foo.example.com/2', + 'icon_url' => 'https://pbs.twimg.com/profile_images/114514/example_bigger.jpg', ], (object) [ 'id' => 3, 'screen_name' => 'bar', 'service' => 'mastodon', 'url' => 'http://bar.example.com', + 'icon_url' => null, ], (object) [ 'id' => 4, 'screen_name' => 'baz', 'service' => 'mastodon', 'url' => 'https://baz.example.com', + 'icon_url' => 'https://files.mastodon.social/accounts/avatars/000/000/001/original/114514.png', ], ]; @@ -52,18 +57,21 @@ public function setUp(): void 'screen_name' => 'foo', 'service' => 'twitter', 'url' => 'https://foo.example.com/1', + 'icon_url' => 'https://pbs.twimg.com/profile_images/114514/example_bigger.jpg', ], (object) [ 'id' => 2, 'screen_name' => 'foo', 'service' => 'twitter', 'url' => 'https://foo.example.com/2', + 'icon_url' => 'https://pbs.twimg.com/profile_images/114514/example_bigger.jpg', ], ]; } public function testCount(): void { + /** @var Builder&MockInterface $builder */ $builder = m::mock(Builder::class); $builder->shouldReceive('count') ->andReturn(4); @@ -83,6 +91,7 @@ public function testCountByScreenName(): void { $screen_name = 'foo'; + /** @var Builder&MockInterface $builder */ $builder = m::mock(Builder::class); $builder->shouldReceive('where->count') ->andReturn(2); @@ -100,14 +109,20 @@ public function testCountByScreenName(): void public function testFindAll(): void { + /** @var Builder&MockInterface $builder */ $builder = m::mock(Builder::class); $builder->shouldReceive('get->all') ->andReturn($this->users); + /** @var Builder&MockInterface $join_builder */ + $join_builder = m::mock(Builder::class); + $join_builder->shouldReceive('leftJoin->select') + ->andReturn($builder); + DB::shouldReceive('table') ->once() ->with('users') - ->andReturn($builder); + ->andReturn($join_builder); $homo = new HomoRepository(); $actual = $homo->findAll(); @@ -119,14 +134,20 @@ public function testFindByScreenName(): void { $screen_name = 'foo'; + /** @var Builder&MockInterface $builder */ $builder = m::mock(Builder::class); $builder->shouldReceive('where->get->all') ->andReturn($this->usersFoo); + /** @var Builder&MockInterface $join_builder */ + $join_builder = m::mock(Builder::class); + $join_builder->shouldReceive('leftJoin->select') + ->andReturn($builder); + DB::shouldReceive('table') ->once() ->with('users') - ->andReturn($builder); + ->andReturn($join_builder); $homo = new HomoRepository(); $actual = $homo->findByScreenName($screen_name); @@ -136,10 +157,32 @@ public function testFindByScreenName(): void public function testExport(): void { + /** @var Builder&MockInterface $builder */ $builder = m::mock(Builder::class); $builder->from = 'users'; $builder->shouldReceive('get') - ->andReturn(collect($this->users)); + ->andReturn(collect([ + (object) [ + 'screen_name' => 'foo', + 'service' => 'twitter', + 'url' => 'https://foo.example.com/1', + ], + (object) [ + 'screen_name' => 'foo', + 'service' => 'twitter', + 'url' => 'https://foo.example.com/2', + ], + (object) [ + 'screen_name' => 'bar', + 'service' => 'mastodon', + 'url' => 'http://bar.example.com', + ], + (object) [ + 'screen_name' => 'baz', + 'service' => 'mastodon', + 'url' => 'https://baz.example.com', + ], + ])); DB::shouldReceive('table') ->once() @@ -151,6 +194,7 @@ public function testExport(): void insert into "users" ("screen_name", "service", "url") values ('foo', 'twitter', 'https://foo.example.com/2'); insert into "users" ("screen_name", "service", "url") values ('bar', 'mastodon', 'http://bar.example.com'); insert into "users" ("screen_name", "service", "url") values ('baz', 'mastodon', 'https://baz.example.com'); + SQL; $homo = new HomoRepository(); diff --git a/api/tests/Case/Repository/ProfileRepositoryTest.php b/api/tests/Case/Repository/ProfileRepositoryTest.php new file mode 100644 index 000000000..524537eb4 --- /dev/null +++ b/api/tests/Case/Repository/ProfileRepositoryTest.php @@ -0,0 +1,49 @@ +shouldReceive('upsert') + ->withArgs([ + [ + [ + 'screen_name' => 'foo', + 'icon_url' => 'https://img.example.com/foo', + 'expires_at' => '2022-12-31 23:59:59', + ], + ], + ['screen_name'], + ['icon_url', 'expires_at'], + ]); + + DB::shouldReceive('table') + ->once() + ->with('profiles') + ->andReturn($builder); + + $altsvc = new ProfileRepository(); + $altsvc->save('foo', 'https://img.example.com/foo', '2022-12-31 23:59:59'); + } +} diff --git a/api/tests/Case/Service/CacheServiceTest.php b/api/tests/Case/Service/CacheServiceTest.php deleted file mode 100644 index 554949b68..000000000 --- a/api/tests/Case/Service/CacheServiceTest.php +++ /dev/null @@ -1,84 +0,0 @@ -once() - ->with('icon:mastodon:@example@mastodon.social') - ->andReturn($url); - - $cache = new CacheService(); - - $this->assertEquals($url, $cache->loadIconMastodon($screen_name)); - } - - public function testLoadIconTwitter(): void - { - $screen_name = 'example'; - $url = 'https://pbs.twimg.com/profile_images/114514/example_bigger.jpg'; - - Cache::shouldReceive('get') - ->once() - ->with('icon:twitter:example') - ->andReturn($url); - - $cache = new CacheService(); - - $this->assertEquals($url, $cache->loadIconTwitter($screen_name)); - } - - public function testSaveIconMastodon(): void - { - $screen_name = '@example@mastodon.social'; - $url = 'https://files.mastodon.social/accounts/avatars/000/000/001/original/114514.png'; - - Cache::shouldReceive('put') - ->once() - ->with('icon:mastodon:@example@mastodon.social', $url); - - $cache = new CacheService(); - $cache->saveIconMastodon($screen_name, $url); - } - - public function testSaveIconTwitter(): void - { - $screen_name = 'example'; - $url = 'https://pbs.twimg.com/profile_images/114514/example_bigger.jpg'; - - Cache::shouldReceive('put') - ->once() - ->with('icon:twitter:example', $url); - - $cache = new CacheService(); - $cache->saveIconTwitter($screen_name, $url); - } - - public function testInvalidCall(): void - { - $this->expectException(\LogicException::class); - - $cache = new CacheService(); - $cache->unsupportedMethod(); - } -} diff --git a/api/tests/Case/Service/CheckServiceTest.php b/api/tests/Case/Service/CheckServiceTest.php index 6054047f7..e3cdba683 100644 --- a/api/tests/Case/Service/CheckServiceTest.php +++ b/api/tests/Case/Service/CheckServiceTest.php @@ -38,36 +38,42 @@ public function setUp(): void 'screen_name' => 'foo', 'service' => 'twitter', 'url' => 'https://foo.example.com/1', + 'icon_url' => null, ], (object) [ 'id' => 2, 'screen_name' => 'foo', 'service' => 'twitter', 'url' => 'https://foo.example.com/2', + 'icon_url' => null, ], (object) [ 'id' => 3, 'screen_name' => 'bar', 'service' => 'mastodon', 'url' => 'http://bar.example.com', + 'icon_url' => null, ], (object) [ 'id' => 4, 'screen_name' => 'baz', 'service' => 'mastodon', 'url' => 'https://baz.example.com', + 'icon_url' => null, ], (object) [ 'id' => 5, 'screen_name' => 'qux', 'service' => 'mastodon', 'url' => 'https://qux.example.com', + 'icon_url' => 'https://img.example.com/qux', ], (object) [ 'id' => 6, 'screen_name' => 'quux', 'service' => 'mastodon', 'url' => '', + 'icon_url' => null, ], ]; } @@ -94,7 +100,6 @@ public function testExecuteAsync(): void ->andReturn( new FulfilledPromise('https://img.example.com/bar'), new FulfilledPromise('https://img.example.com/baz'), - new FulfilledPromise('https://img.example.com/qux'), ); /** @var MockInterface&ValidatorService $validator */ @@ -316,6 +321,7 @@ public function testExecuteAsync(): void 'screen_name' => 'foo', 'service' => 'twitter', 'url' => 'https://foo.example.com/1', + 'icon_url' => null, ]), 'result' => new Result([ 'status' => ValidationResult::OK, @@ -345,6 +351,7 @@ public function testExecuteAsync(): void 'screen_name' => 'foo', 'service' => 'twitter', 'url' => 'https://foo.example.com/2', + 'icon_url' => null, ]), 'result' => new Result([ 'status' => ValidationResult::WRONG, @@ -371,6 +378,7 @@ public function testExecuteAsync(): void 'screen_name' => 'bar', 'service' => 'mastodon', 'url' => 'http://bar.example.com', + 'icon_url' => null, ]), 'result' => new Result([ 'status' => ValidationResult::OK, @@ -389,6 +397,7 @@ public function testExecuteAsync(): void 'screen_name' => 'baz', 'service' => 'mastodon', 'url' => 'https://baz.example.com', + 'icon_url' => null, ]), 'result' => new Result([ 'status' => ValidationResult::ERROR, @@ -407,6 +416,7 @@ public function testExecuteAsync(): void 'screen_name' => 'qux', 'service' => 'mastodon', 'url' => 'https://qux.example.com', + 'icon_url' => 'https://img.example.com/qux', ]), 'result' => new Result([ 'status' => ValidationResult::ERROR, diff --git a/api/tests/Case/Service/ClientServiceTest.php b/api/tests/Case/Service/ClientServiceTest.php index 2676fa092..636f9ee69 100644 --- a/api/tests/Case/Service/ClientServiceTest.php +++ b/api/tests/Case/Service/ClientServiceTest.php @@ -12,7 +12,7 @@ use GuzzleHttp\Promise\PromiseInterface; use GuzzleHttp\Psr7\Request as Psr7Request; use GuzzleHttp\Psr7\Response as Psr7Response; -use HomoChecker\Contracts\Service\CacheService; +use HomoChecker\Contracts\Repository\AltsvcRepository; use HomoChecker\Contracts\Service\Client\Response; use HomoChecker\Service\ClientService; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; @@ -37,16 +37,12 @@ public function testGetAsyncWithSingleRedirectAndReturn(): void 'transfer_time' => 1.0, ]); - /** @var CacheService&MockInterface $cache */ - $cache = m::mock(CacheService::class); - $cache->shouldReceive('loadAltsvc') - ->withArgs(['https://foo.example.com/1', '']) - ->andReturn(''); - $cache->shouldReceive('loadAltsvc') - ->withArgs(['https://homo.example.com', '']) - ->andReturn(''); + /** @var AltsvcRepository&MockInterface $altsvc */ + $altsvc = m::mock(AltsvcRepository::class); + $altsvc->shouldReceive('findAll') + ->andReturn([]); - $service = new ClientService($client, $cache, 5); + $service = new ClientService($client, $altsvc, 5); $generator = $service->getAsync('https://foo.example.com/1'); $generator->rewind(); @@ -90,16 +86,12 @@ public function testGetAsyncWithMultipleRedirectsAndReturn(): void 'transfer_time' => 1.0, ]); - /** @var CacheService&MockInterface $cache */ - $cache = m::mock(CacheService::class); - $cache->shouldReceive('loadAltsvc') - ->withArgs(['https://foo.example.com/2', '']) - ->andReturn(''); - $cache->shouldReceive('loadAltsvc') - ->withArgs(['https://foo2.example.com', '']) - ->andReturn(''); + /** @var AltsvcRepository&MockInterface $altsvc */ + $altsvc = m::mock(AltsvcRepository::class); + $altsvc->shouldReceive('findAll') + ->andReturn([]); - $service = new ClientService($client, $cache, 5); + $service = new ClientService($client, $altsvc, 5); $generator = $service->getAsync('https://foo.example.com/2'); $generator->rewind(); @@ -169,16 +161,12 @@ public function testGetAsyncWithAltsvcH2(): void 'transfer_time' => 1.0, ]); - /** @var CacheService&MockInterface $cache */ - $cache = m::mock(CacheService::class); - $cache->shouldReceive('loadAltsvc') - ->withArgs(['https://foo.example.com/1', '']) - ->andReturn(''); - $cache->shouldReceive('loadAltsvc') - ->withArgs(['https://homo.example.com', '']) - ->andReturn(''); + /** @var AltsvcRepository&MockInterface $altsvc */ + $altsvc = m::mock(AltsvcRepository::class); + $altsvc->shouldReceive('findAll') + ->andReturn([]); - $service = new ClientService($client, $cache, 5); + $service = new ClientService($client, $altsvc, 5); $generator = $service->getAsync('https://foo.example.com/1'); $generator->rewind(); @@ -227,18 +215,23 @@ public function testGetAsyncWithAltsvcH3(): void 'transfer_time' => 1.0, ]); - /** @var CacheService&MockInterface $cache */ - $cache = m::mock(CacheService::class); - $cache->shouldReceive('loadAltsvc') - ->withArgs(['http://foo.example.com/1', '']) - ->andReturn('h3'); - $cache->shouldReceive('loadAltsvc') - ->withArgs(['https://homo.example.com', '']) - ->andReturn('h3'); - $cache->shouldReceive('saveAltsvc') - ->withArgs(['https://foo.example.com/1', 'h3', 86400]); - - $service = new ClientService($client, $cache, 5); + /** @var AltsvcRepository&MockInterface $altsvc */ + $altsvc = m::mock(AltsvcRepository::class); + $altsvc->shouldReceive('findAll') + ->andReturn([ + [ + 'url' => 'http://foo.example.com/1', + 'protocol' => 'h3', + ], + [ + 'url' => 'https://homo.example.com', + 'protocol' => 'h3', + ], + ]); + $altsvc->shouldReceive('save') + ->withArgs(['https://foo.example.com/1', 'h3', m::type('string')]); + + $service = new ClientService($client, $altsvc, 5); $generator = $service->getAsync('http://foo.example.com/1'); $generator->rewind(); @@ -281,13 +274,12 @@ public function testGetAsyncWithException(): void 'transfer_time' => 1.0, ]); - /** @var CacheService&MockInterface $cache */ - $cache = m::mock(CacheService::class); - $cache->shouldReceive('loadAltsvc') - ->withArgs(['https://baz.example.com', '']) - ->andReturn(''); + /** @var AltsvcRepository&MockInterface $altsvc */ + $altsvc = m::mock(AltsvcRepository::class); + $altsvc->shouldReceive('findAll') + ->andReturn([]); - $service = new ClientService($client, $cache, 5); + $service = new ClientService($client, $altsvc, 5); $generator = $service->getAsync('https://baz.example.com'); $generator->rewind(); @@ -321,10 +313,12 @@ public function testSend(): void ->withArgs([$request, ['http_errors' => false]]) ->andReturn($response); - /** @var CacheService&MockInterface $cache */ - $cache = m::mock(CacheService::class); + /** @var AltsvcRepository&MockInterface $altsvc */ + $altsvc = m::mock(AltsvcRepository::class); + $altsvc->shouldReceive('findAll') + ->andReturn([]); - $service = new ClientService($client, $cache, 5); + $service = new ClientService($client, $altsvc, 5); $actual = $service->send($request, [ 'http_errors' => false, ]); @@ -346,10 +340,12 @@ public function testSendAsync(): void ->withArgs([$request, ['http_errors' => false]]) ->andReturn($promise); - /** @var CacheService&MockInterface $cache */ - $cache = m::mock(CacheService::class); + /** @var AltsvcRepository&MockInterface $altsvc */ + $altsvc = m::mock(AltsvcRepository::class); + $altsvc->shouldReceive('findAll') + ->andReturn([]); - $service = new ClientService($client, $cache, 5); + $service = new ClientService($client, $altsvc, 5); $actual = $service->sendAsync($request, [ 'http_errors' => false, ]); @@ -368,10 +364,12 @@ public function testRequest(): void ->withArgs(['GET', 'https://example.com', ['http_errors' => false]]) ->andReturn($response); - /** @var CacheService&MockInterface $cache */ - $cache = m::mock(CacheService::class); + /** @var AltsvcRepository&MockInterface $altsvc */ + $altsvc = m::mock(AltsvcRepository::class); + $altsvc->shouldReceive('findAll') + ->andReturn([]); - $service = new ClientService($client, $cache, 5); + $service = new ClientService($client, $altsvc, 5); $actual = $service->request('GET', 'https://example.com', [ 'http_errors' => false, ]); @@ -390,10 +388,12 @@ public function testRequestAsync(): void ->withArgs(['GET', 'https://example.com', ['http_errors' => false]]) ->andReturn($promise); - /** @var CacheService&MockInterface $cache */ - $cache = m::mock(CacheService::class); + /** @var AltsvcRepository&MockInterface $altsvc */ + $altsvc = m::mock(AltsvcRepository::class); + $altsvc->shouldReceive('findAll') + ->andReturn([]); - $service = new ClientService($client, $cache, 5); + $service = new ClientService($client, $altsvc, 5); $actual = $service->requestAsync('GET', 'https://example.com', [ 'http_errors' => false, ]); @@ -408,10 +408,12 @@ public function testGetConfig(): void $client->shouldReceive('getConfig') ->andReturn(['http_errors' => false]); - /** @var CacheService&MockInterface $cache */ - $cache = m::mock(CacheService::class); + /** @var AltsvcRepository&MockInterface $altsvc */ + $altsvc = m::mock(AltsvcRepository::class); + $altsvc->shouldReceive('findAll') + ->andReturn([]); - $service = new ClientService($client, $cache, 5); + $service = new ClientService($client, $altsvc, 5); $actual = $service->getConfig(); $this->assertEquals(['http_errors' => false], $actual); diff --git a/api/tests/Case/Service/Profile/MastodonProfileServiceTest.php b/api/tests/Case/Service/Profile/MastodonProfileServiceTest.php index 5f64fed6c..e10684bcb 100644 --- a/api/tests/Case/Service/Profile/MastodonProfileServiceTest.php +++ b/api/tests/Case/Service/Profile/MastodonProfileServiceTest.php @@ -10,7 +10,7 @@ use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; -use HomoChecker\Contracts\Service\CacheService; +use HomoChecker\Contracts\Repository\ProfileRepository; use HomoChecker\Service\Profile\MastodonProfileService; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use Mockery as m; @@ -22,7 +22,7 @@ class MastodonProfileServiceTest extends TestCase { use MockeryPHPUnitIntegration; - public function testGetIconAsyncNotCached(): void + public function testGetIconAsync(): void { $screen_name = '@example@mastodon.social'; $url = 'https://files.mastodon.social/accounts/avatars/000/000/001/original/114514.png'; @@ -48,13 +48,10 @@ public function testGetIconAsyncNotCached(): void $client = new Client(compact('handler')); - /** @var CacheService&MockInterface $cache */ - $cache = m::mock(CacheService::class); - $cache->shouldReceive('loadIconMastodon') - ->andReturn(null); - - $cache->shouldReceive('saveIconMastodon') - ->with($screen_name, $url, m::any()); + /** @var MockInterface&ProfileRepository $repository */ + $repository = m::mock(ProfileRepository::class); + $repository->shouldReceive('save') + ->withArgs([$screen_name, $url, m::type('string')]); /** @var Counter&MockInterface $profileErrorCounter */ $profileErrorCounter = m::mock(Counter::class); @@ -75,7 +72,7 @@ public function testGetIconAsyncNotCached(): void ], ]); - $profile = new MastodonProfileService($client, $cache, $profileErrorCounter); + $profile = new MastodonProfileService($client, $repository, $profileErrorCounter); $this->assertEquals($url, $profile->getIconAsync($screen_name)->wait()); $this->assertEquals($profile->getDefaultUrl(), $profile->getIconAsync($screen_name)->wait()); @@ -83,26 +80,6 @@ public function testGetIconAsyncNotCached(): void $this->assertEquals($profile->getDefaultUrl(), $profile->getIconAsync('example@wrong-format.example.com')->wait()); } - public function testGetIconAsyncCached(): void - { - $url = 'https://files.mastodon.social/accounts/avatars/000/000/001/original/114514.png'; - $screen_name = '@example@mastodon.social'; - - /** @var ClientInterface&MockInterface $client */ - $client = m::mock(ClientInterface::class); - - /** @var CacheService&MockInterface $cache */ - $cache = m::mock(CacheService::class); - $cache->shouldReceive('loadIconMastodon') - ->andReturn($url); - - /** @var Counter&MockInterface $profileErrorCounter */ - $profileErrorCounter = m::mock(Counter::class); - - $profile = new MastodonProfileService($client, $cache, $profileErrorCounter); - $this->assertEquals($url, $profile->getIconAsync($screen_name)->wait()); - } - /** * @dataProvider screenNameProvider */ @@ -111,13 +88,13 @@ public function testParseScreenName($screen_name, $username, $instance): void /** @var ClientInterface&MockInterface $client */ $client = m::mock(ClientInterface::class); - /** @var CacheService&MockInterface $cache */ - $cache = m::mock(CacheService::class); + /** @var MockInterface&ProfileRepository $repository */ + $repository = m::mock(ProfileRepository::class); /** @var Counter&MockInterface $profileErrorCounter */ $profileErrorCounter = m::mock(Counter::class); - $profile = new MastodonProfileService($client, $cache, $profileErrorCounter); + $profile = new MastodonProfileService($client, $repository, $profileErrorCounter); $actual = $profile->parseScreenName($screen_name); $this->assertEquals([$username, $instance], $actual); @@ -133,13 +110,13 @@ public function testParseScreenNameInvalid($screen_name): void /** @var ClientInterface&MockInterface $client */ $client = m::mock(ClientInterface::class); - /** @var CacheService&MockInterface $cache */ - $cache = m::mock(CacheService::class); + /** @var MockInterface&ProfileRepository $repository */ + $repository = m::mock(ProfileRepository::class); /** @var Counter&MockInterface $profileErrorCounter */ $profileErrorCounter = m::mock(Counter::class); - $profile = new MastodonProfileService($client, $cache, $profileErrorCounter); + $profile = new MastodonProfileService($client, $repository, $profileErrorCounter); $profile->parseScreenName($screen_name); } diff --git a/api/tests/Case/Service/Profile/TwitterProfileServiceTest.php b/api/tests/Case/Service/Profile/TwitterProfileServiceTest.php index 436e56cdc..7067f23a1 100644 --- a/api/tests/Case/Service/Profile/TwitterProfileServiceTest.php +++ b/api/tests/Case/Service/Profile/TwitterProfileServiceTest.php @@ -10,7 +10,7 @@ use GuzzleHttp\HandlerStack; use GuzzleHttp\Psr7\Request; use GuzzleHttp\Psr7\Response; -use HomoChecker\Contracts\Service\CacheService; +use HomoChecker\Contracts\Repository\ProfileRepository; use HomoChecker\Service\Profile\TwitterProfileService; use Mockery\Adapter\Phpunit\MockeryPHPUnitIntegration; use Mockery as m; @@ -22,7 +22,7 @@ class TwitterProfileServiceTest extends TestCase { use MockeryPHPUnitIntegration; - public function testGetIconAsyncNotCached(): void + public function testGetIconAsync(): void { $screen_name = 'example'; $url = 'https://pbs.twimg.com/profile_images/114514/example_bigger.jpg'; @@ -52,13 +52,10 @@ public function testGetIconAsyncNotCached(): void /** @var ClientInterface&MockInterface $client */ $client = new Client(compact('handler')); - /** @var CacheService&MockInterface $cache */ - $cache = m::mock(CacheService::class); - $cache->shouldReceive('loadIconTwitter') - ->andReturn(null); - - $cache->shouldReceive('saveIconTwitter') - ->with($screen_name, $url, m::any()); + /** @var MockInterface&ProfileRepository $repository */ + $repository = m::mock(ProfileRepository::class); + $repository->shouldReceive('save') + ->withArgs([$screen_name, $url, m::type('string')]); /** @var Counter&MockInterface $profileErrorCounter */ $profileErrorCounter = m::mock(Counter::class); @@ -70,30 +67,10 @@ public function testGetIconAsyncNotCached(): void ], ]); - $profile = new TwitterProfileService($client, $cache, $profileErrorCounter); + $profile = new TwitterProfileService($client, $repository, $profileErrorCounter); $this->assertEquals($url, $profile->getIconAsync($screen_name)->wait()); $this->assertEquals($profile->getDefaultUrl(), $profile->getIconAsync($screen_name)->wait()); $this->assertEquals($profile->getDefaultUrl(), $profile->getIconAsync($screen_name)->wait()); } - - public function testGetIconAsyncCached(): void - { - $url = 'https://pbs.twimg.com/profile_images/114514/example_bigger.jpg'; - $screen_name = 'example'; - - /** @var ClientInterface&MockInterface $client */ - $client = m::mock(ClientInterface::class); - - /** @var CacheService&MockInterface $cache */ - $cache = m::mock(CacheService::class); - $cache->shouldReceive('loadIconTwitter') - ->andReturn($url); - - /** @var Counter&MockInterface $profileErrorCounter */ - $profileErrorCounter = m::mock(Counter::class); - - $profile = new TwitterProfileService($client, $cache, $profileErrorCounter); - $this->assertEquals($url, $profile->getIconAsync($screen_name)->wait()); - } } diff --git a/database/homo.sql b/database/homo.sql index 630bddd0f..57f45f728 100644 --- a/database/homo.sql +++ b/database/homo.sql @@ -1,7 +1,20 @@ CREATE TABLE IF NOT EXISTS "users" ( - "id" SERIAL NOT NULL PRIMARY KEY, + "id" serial NOT NULL PRIMARY KEY, "screen_name" varchar(255) NOT NULL, "service" varchar(20) NOT NULL, "url" varchar(255) NOT NULL ); -CREATE INDEX ON "users"("screen_name"); +CREATE INDEX ON "users" ("screen_name"); + +CREATE TABLE IF NOT EXISTS "profiles" ( + "screen_name" varchar(255) NOT NULL, + "icon_url" text NOT NULL, + "expires_at" timestamp NOT NULL, + UNIQUE ("screen_name") +); + +CREATE TABLE IF NOT EXISTS "altsvcs" ( + "url" varchar(255) NOT NULL PRIMARY KEY, + "protocol" varchar(20) NOT NULL, + "expires_at" timestamp NOT NULL +); diff --git a/docker-compose.yml b/docker-compose.yml index fb18adc7e..b65472e08 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -11,7 +11,6 @@ services: HOMOCHECKER_DB_USERNAME: homo HOMOCHECKER_DB_PASSWORD: homo HOMOCHECKER_LOG_LEVEL: debug - HOMOCHECKER_REDIS_HOST: redis HOMOCHECKER_TWITTER_CONSUMER_KEY: HOMOCHECKER_TWITTER_CONSUMER_SECRET: HOMOCHECKER_TWITTER_TOKEN: @@ -72,11 +71,6 @@ services: networks: - default - redis: - image: redis:7.0 - networks: - - default - volumes: database: driver: local