diff --git a/app/Exceptions/CustomTestException.php b/app/Exceptions/CustomTestException.php index 885120d..8097479 100644 --- a/app/Exceptions/CustomTestException.php +++ b/app/Exceptions/CustomTestException.php @@ -2,6 +2,9 @@ namespace App\Exceptions; +/** + * @codeCoverageIgnore + */ class CustomTestException extends \Exception { } diff --git a/app/Support/CacheOptions.php b/app/Support/CacheOptions.php index 71be4fb..83857a0 100644 --- a/app/Support/CacheOptions.php +++ b/app/Support/CacheOptions.php @@ -2,6 +2,9 @@ namespace App\Support; +/** + * A class to provide strongly typed options about the cache configuration. + */ final class CacheOptions { private ?int $ttl = null; diff --git a/app/Support/CachedData.php b/app/Support/CachedData.php index e54351d..898009c 100644 --- a/app/Support/CachedData.php +++ b/app/Support/CachedData.php @@ -50,11 +50,6 @@ final class CachedData implements ArrayAccess return self::fromArray($model->toArray()); } - public function collect(): Collection - { - return $this->scraperResult; - } - public function isEmpty(): bool { return $this->scraperResult->isEmpty(); @@ -85,11 +80,6 @@ final class CachedData implements ArrayAccess return $modifiedAt !== null ? $ttl + $modifiedAt : $ttl; } - public function cacheTtl(): int - { - return $this->cacheTimeToLive; - } - public function lastModified(): ?int { if ($this->scraperResult->isEmpty()) { diff --git a/app/Support/Lazy.php b/app/Support/Lazy.php deleted file mode 100644 index 41980d7..0000000 --- a/app/Support/Lazy.php +++ /dev/null @@ -1,25 +0,0 @@ - $callback - */ - public function __construct(private readonly \Closure $callback) - { - } - - /** - * @return T - */ - public function value() - { - $callback = $this->callback; - return $callback(); - } -} diff --git a/app/Testing/Concerns/MakesHttpRequestsEx.php b/app/Testing/Concerns/MakesHttpRequestsEx.php index 5bf3cc4..7ab064f 100644 --- a/app/Testing/Concerns/MakesHttpRequestsEx.php +++ b/app/Testing/Concerns/MakesHttpRequestsEx.php @@ -1,6 +1,9 @@ call('migrate:fresh', []); } catch (\Exception $ex) { - print_r($ex->getMessage()); - print_r($ex); - throw $ex; + dd($ex); } } } diff --git a/tests/Unit/CachedDataTest.php b/tests/Unit/CachedDataTest.php new file mode 100644 index 0000000..416653d --- /dev/null +++ b/tests/Unit/CachedDataTest.php @@ -0,0 +1,81 @@ + [ + $now, new UTCDateTime($now->getPreciseTimestamp(3)) + ], + "built-in datetime" => [ + $now, $now->toDateTime() + ], + "atom string datetime" => [ + $now, $now->toAtomString() + ] + ]; + } + + public function invalidModifiedAtValuesProvider(): array + { + return [ + "number value" => [ + ["modifiedAt" => 1] + ], + "float value" => [ + ["modifiedAt" => 1.3] + ], + "bool value" => [ + ["modifiedAt" => true] + ], + "modifiedAt key not present" => [ + ["someotherkey" => 1] + ] + ]; + } + + public function testLastModifiedReturnsNullIfInternalCollectionIsEmpty() + { + $sut = CachedData::from(collect()); + $this->assertNull($sut->lastModified()); + } + + /** + * @dataProvider invalidModifiedAtValuesProvider + */ + public function testLastModifiedReturnsNullIfModifiedAtIsUnknownFormat(array $contents) + { + $sut = CachedData::from(collect($contents)); + $this->assertNull($sut->lastModified()); + } + + /** + * @dataProvider dateTimeProvider + */ + public function testLastModifiedReturnsModifiedTime($now, $dateTime) + { + $sut = CachedData::from(collect(["modifiedAt" => $dateTime])); + $this->assertEquals($now->getTimestamp(), $sut->lastModified()); + } + + public function testIsEmptyReturnsTrueIfEmpty() + { + $sut = CachedData::from(collect()); + $this->assertEquals(true, $sut->isEmpty()); + } + + public function testIsExpiredReturnTrueIfEmpty() + { + $sut = CachedData::from(collect()); + $this->assertEquals(true, $sut->isExpired()); + } +} diff --git a/tests/Unit/DefaultCachedScraperServiceTest.php b/tests/Unit/DefaultCachedScraperServiceTest.php index 1e6c4eb..46f5e5e 100644 --- a/tests/Unit/DefaultCachedScraperServiceTest.php +++ b/tests/Unit/DefaultCachedScraperServiceTest.php @@ -1,13 +1,12 @@ expects() + ->allows() ->where("request_hash", $this->requestHash()) + ->atMost() ->times($repositoryWhereCallCount) ->andReturn($queryBuilderMock); @@ -240,6 +241,7 @@ final class DefaultCachedScraperServiceTest extends TestCase ]); [$queryBuilderMock, $repositoryMock, $serializerMock] = $this->makeCtorArgMocks(); + // stale record in db $repositoryMock->expects()->getAllByMalId($malId)->andReturns(collect([ $mockModel ])); @@ -294,7 +296,7 @@ final class DefaultCachedScraperServiceTest extends TestCase ]); [$queryBuilderMock, $repositoryMock, $serializerMock] = $this->makeCtorArgMocks(); - $repositoryMock->expects()->where("username", $username)->andReturns($queryBuilderMock); + $repositoryMock->expects()->where("username", $username)->atMost()->times(2)->andReturns($queryBuilderMock); // nothing in db $queryBuilderMock->expects()->get()->andReturns(collect()); // scrape returns data @@ -311,4 +313,46 @@ final class DefaultCachedScraperServiceTest extends TestCase $this->assertEquals($mockModel->toArray(), $result->toArray()); } + + public function testIfFindByKeyUpdatesCache() + { + $malId = 1; + $username = "kompot"; + $testRequestHash = $this->requestHash(); + $now = Carbon::now(); + $mockModel = Profile::factory()->makeOne([ + "mal_id" => $malId, + "username" => $username, + "modifiedAt" => new UTCDateTime($now->sub("3 days")->getPreciseTimestamp(3)), + "createdAt" => new UTCDateTime($now->sub("3 days")->getPreciseTimestamp(3)) + ]); + $now = Carbon::now(); + Carbon::setTestNow($now); + $updatedMockModel = Profile::factory()->makeOne([ + ...$mockModel->toArray(), + "location" => "North Pole", + "modifiedAt" => new UTCDateTime(Carbon::now()->getPreciseTimestamp(3)), + "createdAt" => new UTCDateTime(Carbon::now()->getPreciseTimestamp(3)) + ]); + [$queryBuilderMock, $repositoryMock, $serializerMock] = $this->makeCtorArgMocks(); + // stale record in db + $repositoryMock->expects()->where("username", $username)->atMost()->times(3)->andReturns($queryBuilderMock); + $queryBuilderMock->expects()->get()->andReturns(collect([ + $mockModel + ])); + $repositoryMock->expects()->scrape($username)->andReturns( + collect($updatedMockModel->toArray())->except(["request_hash", "modifiedAt", "createdAt"])->toArray() + ); + // mock out update + $queryBuilderMock->expects()->update(Mockery::any())->andReturns($malId); + // second call to ->get() should return the updated value + $queryBuilderMock->expects()->get()->andReturns(collect([ + $updatedMockModel + ])); + + $sut = new DefaultCachedScraperService($repositoryMock, new MalClient(), $serializerMock); + $result = $sut->findByKey("username", $username, $testRequestHash); + + $this->assertEquals($updatedMockModel->toArray(), $result->toArray()); + } } diff --git a/tests/Unit/DefaultMediatorTest.php b/tests/Unit/DefaultMediatorTest.php new file mode 100644 index 0000000..3b5f5eb --- /dev/null +++ b/tests/Unit/DefaultMediatorTest.php @@ -0,0 +1,37 @@ +allows()->requestClass()->andReturns(AnimeSearchCommand::class); + $sut = new DefaultMediator($mockHandler); + + $response = $sut->send(\Mockery::mock(DataRequest::class)); + $this->assertEquals(500, $response->status()); + } + + public function testSendShouldCallHandleOnHandlerIfFound() + { + $mockHandler = \Mockery::mock(RequestHandler::class); + $mockRequest = \Mockery::mock(DataRequest::class); + $mockHandler->allows()->requestClass()->andReturns(get_class($mockRequest)); + $mockHandler->expects()->handle($mockRequest)->once()->andReturns(response()->json([ + "message" => "success" + ])); + + $sut = new DefaultMediator($mockHandler); + $response = $sut->send($mockRequest); + $this->assertEquals(200, $response->status()); + $this->assertEquals(json_encode(["message" => "success"]), $response->content()); + } +}