diff --git a/app/Services/DefaultCachedScraperService.php b/app/Services/DefaultCachedScraperService.php index adb32c2..3ea7986 100644 --- a/app/Services/DefaultCachedScraperService.php +++ b/app/Services/DefaultCachedScraperService.php @@ -7,10 +7,11 @@ use App\Contracts\Repository; use App\Http\HttpHelper; use App\Support\CachedData; use Illuminate\Contracts\Database\Query\Builder; +use Illuminate\Support\Carbon; use Illuminate\Support\Collection; use Jikan\MyAnimeList\MalClient; +use JMS\Serializer\SerializerInterface; use MongoDB\BSON\UTCDateTime; -use JMS\Serializer\Serializer; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** @@ -21,7 +22,7 @@ final class DefaultCachedScraperService implements CachedScraperService public function __construct( private readonly Repository $repository, private readonly MalClient $jikan, - private readonly Serializer $serializer, + private readonly SerializerInterface $serializer, ) { } @@ -143,9 +144,7 @@ final class DefaultCachedScraperService implements CachedScraperService // insert cache if resource doesn't exist if ($results->isEmpty()) { $this->repository->insert($response->toArray()); - } - - if ($results->isExpired()) { + } else if ($results->isExpired()) { $this->getQueryableByCacheKey($cacheKey)->update($response->toArray()); } @@ -157,13 +156,15 @@ final class DefaultCachedScraperService implements CachedScraperService $meta = []; if ($resultsEmpty) { $meta = [ - 'createdAt' => new UTCDateTime(), + // Using Carbon here for testability + 'createdAt' => new UTCDateTime(Carbon::now()->getPreciseTimestamp(3)), 'request_hash' => $cacheKey ]; } // Update `modifiedAt` meta - $meta['modifiedAt'] = new UTCDateTime(); + // Using Carbon here for testability + $meta['modifiedAt'] = new UTCDateTime(Carbon::now()->getPreciseTimestamp(3)); // join meta data with response return CachedData::from(collect($meta + $scraperResponse)); diff --git a/tests/Unit/DefaultCachedScraperServiceTest.php b/tests/Unit/DefaultCachedScraperServiceTest.php new file mode 100644 index 0000000..2e64d21 --- /dev/null +++ b/tests/Unit/DefaultCachedScraperServiceTest.php @@ -0,0 +1,142 @@ +makePartial(); + $repositoryMock = Mockery::mock(Repository::class); + $serializerMock = Mockery::mock(SerializerInterface::class); + + $repositoryMock + ->expects() + ->where("request_hash", $this->requestHash()) + ->times($repositoryWhereCallCount) + ->andReturn($queryBuilderMock); + + return [ + $queryBuilderMock, + $repositoryMock, + $serializerMock + ]; + } + + public function testIfFindListReturnsNotExpiredItems() + { + $testRequestHash = $this->requestHash(); + // the cached data in the database + $dummyResults = collect([ + ["dummy" => "dummy1", "modifiedAt" => new UTCDateTime()], + ["dummy" => "dummy2", "modifiedAt" => new UTCDateTime()] + ]); + [$queryBuilderMock, $repositoryMock, $serializerMock] = $this->makeCtorArgMocks(); + $queryBuilderMock->expects()->get()->once()->andReturn($dummyResults); + + $target = new DefaultCachedScraperService($repositoryMock, new MalClient(), $serializerMock); + + $result = $target->findList($testRequestHash, fn() => []); + + $this->assertEquals($dummyResults->toArray(), $result->toArray()); + } + + public function testIfFindListUpdatesCacheIfItemsExpired() + { + $testRequestHash = $this->requestHash(); + $now = Carbon::now(); + Carbon::setTestNow($now); + + // the cached data in the database + $dummyResults = collect([ + ["dummy" => "dummy1", "modifiedAt" => new UTCDateTime($now->sub("2 days")->timestamp)], + ["dummy" => "dummy2", "modifiedAt" => new UTCDateTime($now->sub("2 days")->timestamp)] + ]); + + // the data returned by the scraper + $scraperData = [ + "results" => [ + ["dummy" => "dummy1"], + ["dummy" => "dummy2"] + ] + ]; + [$queryBuilderMock, $repositoryMock, $serializerMock] = $this->makeCtorArgMocks(3); + $queryBuilderMock->expects()->get()->twice()->andReturn($dummyResults); + $queryBuilderMock->expects()->update(Mockery::any())->once()->andReturn(1); + + $serializerMock->allows([ + "toArray" => $scraperData + ]); + + $target = new DefaultCachedScraperService($repositoryMock, new MalClient(), $serializerMock); + $result = $target->findList($testRequestHash, fn() => []); + + $this->assertEquals([ + ["dummy" => "dummy1", "modifiedAt" => new UTCDateTime($now->getPreciseTimestamp(3))], + ["dummy" => "dummy2", "modifiedAt" => new UTCDateTime($now->getPreciseTimestamp(3))] + ], $result->toArray()); + } + + public function testIfFindListUpdatesCacheIfCacheIsEmpty() + { + $testRequestHash = $this->requestHash(); + $now = Carbon::now(); + Carbon::setTestNow($now); + + // the data returned by the scraper + $scraperData = [ + "results" => [ + ["dummy" => "dummy1"], + ["dummy" => "dummy2"] + ] + ]; + + $cacheData = [ + ["dummy" => "dummy1", "modifiedAt" => new UTCDateTime($now->getPreciseTimestamp(3))], + ["dummy" => "dummy2", "modifiedAt" => new UTCDateTime($now->getPreciseTimestamp(3))] + ]; + + [$queryBuilderMock, $repositoryMock, $serializerMock] = $this->makeCtorArgMocks(2); + // at first the cache is empty + $queryBuilderMock->expects()->get()->once()->andReturn(collect()); + // then it gets "inserted" into + $queryBuilderMock->expects()->get()->once()->andReturn(collect($cacheData)); + $repositoryMock->expects()->insert(Mockery::any())->once()->andReturn(true); + + $serializerMock->allows([ + "toArray" => $scraperData + ]); + + $target = new DefaultCachedScraperService($repositoryMock, new MalClient(), $serializerMock); + $result = $target->findList($testRequestHash, fn() => []); + + $this->assertEquals($cacheData, $result->toArray()); + } + + public function testIfModifiedAtValueSetCorrectlyDuringCacheUpdate() + { + + } +}