Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
100 changes: 100 additions & 0 deletions tests/Feature/AbstractUseStaleRequestTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

namespace Tests\Feature;

use Carbon\Carbon;
use Carsdotcom\ApiRequest\Testing\MocksGuzzleInstance;
use Carsdotcom\ApiRequest\Testing\RequestClassAssertions;
use GuzzleHttp\Psr7\Response;
Expand Down Expand Up @@ -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
}
}
2 changes: 1 addition & 1 deletion tests/MockClasses/ConcreteUseStaleRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down