diff --git a/tests/Feature/AbstractUseStaleRequestTest.php b/tests/Feature/AbstractUseStaleRequestTest.php index 08f0e79..71b25cf 100644 --- a/tests/Feature/AbstractUseStaleRequestTest.php +++ b/tests/Feature/AbstractUseStaleRequestTest.php @@ -6,6 +6,7 @@ namespace Tests\Feature; +use Carbon\Carbon; use Carsdotcom\ApiRequest\Testing\MocksGuzzleInstance; use Carsdotcom\ApiRequest\Testing\RequestClassAssertions; use GuzzleHttp\Psr7\Response; @@ -119,4 +120,103 @@ public function testRefreshOnNextRequest(): void self::assertTrue($request->canBeFulfilledByCache(), 'Cache is still available!'); self::assertTrue($request->needsRefresh(), 'But we want to refresh opportunistically'); } + + public function testCacheBehaviorUnderHeavyLoad(): void + { + Queue::fake(); + $this->mockGuzzleWithTapper()->addMatchBody('GET', '/test/', 'constant'); + $request = new ConcreteUseStaleRequest('thing'); + + // Never called + self::assertTrue($request->needsRefresh()); + self::assertFalse($request->canBeFulfilledByCache()); + + // First called + Carbon::setTestNow('2026-01-01 00:00:00'); + self::assertSame('constant', $request->sync()); + self::assertFalse($request->needsRefresh()); + self::assertTrue($request->canBeFulfilledByCache()); + Queue::assertNothingPushed(); + $this->assertAllTapperRequestsLike([['GET', '#test/thing#']]); + + // Called before refreshAfter, still in cache + Carbon::setTestNow('2026-01-01 00:01:00'); + self::assertFalse($request->needsRefresh()); + self::assertTrue($request->canBeFulfilledByCache()); + self::assertSame('constant', $request->sync()); + Queue::assertNothingPushed(); // No deferred refresh + $this->assertAllTapperRequestsLike([['GET', '#test/thing#']]); // no additional calls + + // Called after refreshAfter, still in cache + Carbon::setTestNow('2026-01-01 00:16:00'); + self::assertTrue($request->needsRefresh()); + self::assertTrue($request->canBeFulfilledByCache()); + self::assertSame('constant', $request->sync()); + Queue::assertPushed(CallQueuedClosure::class); + $this->assertAllTapperRequestsLike([['GET', '#test/thing#']]); // no additional calls (because we broke the job) + self::assertFalse($request->needsRefresh()); // Gets deferred by waitBetweenRefreshes (5 min) + self::assertTrue($request->canBeFulfilledByCache()); + + // Called after the refresh job failed, but before waitBetweenRefreshes + Queue::fake(); // Discard the "failing" refresh job + Carbon::setTestNow('2026-01-01 00:17:00'); + self::assertFalse($request->needsRefresh()); + self::assertTrue($request->canBeFulfilledByCache()); + self::assertSame('constant', $request->sync()); + Queue::assertNothingPushed(); // No deferred refresh + $this->assertAllTapperRequestsLike([['GET', '#test/thing#']]); // no additional calls (because we broke the job) + + // Called after waitBetweenRefreshes + Carbon::setTestNow('2026-01-01 00:22:00'); + self::assertTrue($request->needsRefresh()); + self::assertTrue($request->canBeFulfilledByCache()); + self::assertSame('constant', $request->sync()); + Queue::assertPushed(function (CallQueuedClosure $job) use (&$callOnOurTerms) { + // deferred refresh + $callOnOurTerms = $job->closure->getClosure(); + return true; + }); + $this->assertAllTapperRequestsLike([['GET', '#test/thing#']]); // no additional calls (the job is waiting for us) + + // Job happens! + Carbon::setTestNow('2026-01-01 00:22:01'); + $callOnOurTerms(); + self::assertFalse($request->needsRefresh()); + self::assertTrue($request->canBeFulfilledByCache()); + $this->assertAllTapperRequestsLike([['GET', '#test/thing#'], ['GET', '#test/thing#']]); // A second call! + + // Another call, after the job and after waitBetweenRefreshes, but before refreshAfter + Carbon::setTestNow('2026-01-01 00:35:01'); + self::assertFalse($request->needsRefresh()); // I won't need another refresh until refreshAfter + self::assertTrue($request->canBeFulfilledByCache()); + self::assertSame('constant', $request->sync()); + $this->assertAllTapperRequestsLike([['GET', '#test/thing#'], ['GET', '#test/thing#']]); // no third call, was in cache + + // Another call, after the job and refreshAfter + Carbon::setTestNow('2026-01-01 00:37:02'); + self::assertTrue($request->needsRefresh()); // I won't need another refresh until refreshAfter + self::assertTrue($request->canBeFulfilledByCache()); + self::assertSame('constant', $request->sync()); + Queue::assertPushed(function (CallQueuedClosure $job) use (&$callOnOurTerms) { + // deferred refresh + $callOnOurTerms = $job->closure->getClosure(); + return true; + }); + $this->assertAllTapperRequestsLike([['GET', '#test/thing#'], ['GET', '#test/thing#']]); // no third call, was in cache + + // Second job succeeds + $callOnOurTerms(); + self::assertFalse($request->needsRefresh()); + self::assertTrue($request->canBeFulfilledByCache()); + $this->assertAllTapperRequestsLike([['GET', '#test/thing#'], ['GET', '#test/thing#'], ['GET', '#test/thing#']]); // A third call! + + // Called well after cacheExpiresTime + Queue::fake(); // Discard the jobs above + Carbon::setTestNow('2026-01-05 00:00:00'); + self::assertTrue($request->needsRefresh()); + self::assertFalse($request->canBeFulfilledByCache()); + self::assertSame('constant', $request->sync()); + Queue::assertNothingPushed(); // No deferred refresh + $this->assertAllTapperRequestsLike([['GET', '#test/thing#'], ['GET', '#test/thing#'], ['GET', '#test/thing#'], ['GET', '#test/thing#']]); // Immediately calls + } } diff --git a/tests/MockClasses/ConcreteUseStaleRequest.php b/tests/MockClasses/ConcreteUseStaleRequest.php index 04ac633..3444d09 100644 --- a/tests/MockClasses/ConcreteUseStaleRequest.php +++ b/tests/MockClasses/ConcreteUseStaleRequest.php @@ -25,7 +25,7 @@ public function getURL(): string public function refreshAfter(): Carbon { - return Carbon::now()->addMinutes(5); + return Carbon::now()->addMinutes(15); } public function getLogFolder(): string