From 302087a2572892bede61bea4554db199591d3389 Mon Sep 17 00:00:00 2001 From: yankewei Date: Tue, 21 Oct 2025 19:25:46 +0800 Subject: [PATCH] Add PSR-20 Clock Support --- composer.json | 1 + src/Helpers/SystemClock.php | 16 +++++ src/Http/Auth/AccessTokenAuthenticator.php | 5 +- src/Http/BaseResource.php | 2 +- src/Traits/OAuth2/AuthorizationCodeGrant.php | 5 +- src/Traits/OAuth2/ClientCredentialsGrant.php | 5 +- src/Traits/OAuth2/HasClock.php | 21 ++++++ .../Oauth2/AuthCodeFlowConnectorTest.php | 29 +++++---- .../ClientCredentialsFlowConnectorTest.php | 10 ++- .../CustomOAuthAuthenticator.php | 8 +-- .../ClientCredentialsBasicAuthConnector.php | 7 ++ .../Connectors/ClientCredentialsConnector.php | 7 ++ tests/Fixtures/Connectors/OAuth2Connector.php | 7 ++ .../Requests/QueryParameterRequest.php | 2 +- tests/Helpers/Date.php | 64 ------------------- tests/Helpers/FrozenClock.php | 47 ++++++++++++++ .../Oauth2/AccessTokenAuthenticatorTest.php | 17 +++-- .../Unit/Oauth2/AuthCodeFlowConnectorTest.php | 7 +- 18 files changed, 162 insertions(+), 98 deletions(-) create mode 100644 src/Helpers/SystemClock.php create mode 100644 src/Traits/OAuth2/HasClock.php delete mode 100644 tests/Helpers/Date.php create mode 100644 tests/Helpers/FrozenClock.php diff --git a/composer.json b/composer.json index 9fd431cc..4b9bfc49 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "guzzlehttp/guzzle": "^7.6", "guzzlehttp/promises": "^1.5 || ^2.0", "guzzlehttp/psr7": "^2.0", + "psr/clock": "^1.0", "psr/http-factory": "^1.0", "psr/http-message": "^1.1 || ^2.0" }, diff --git a/src/Helpers/SystemClock.php b/src/Helpers/SystemClock.php new file mode 100644 index 00000000..adc6e1f7 --- /dev/null +++ b/src/Helpers/SystemClock.php @@ -0,0 +1,16 @@ +expiresAt->getTimestamp() <= (new DateTimeImmutable)->getTimestamp(); + return $this->expiresAt->getTimestamp() <= ($this->clock ?? new SystemClock())->now()->getTimestamp(); } /** diff --git a/src/Http/BaseResource.php b/src/Http/BaseResource.php index 3188bebf..b37441e6 100644 --- a/src/Http/BaseResource.php +++ b/src/Http/BaseResource.php @@ -9,7 +9,7 @@ class BaseResource /** * Constructor */ - public function __construct(readonly protected Connector $connector) + public function __construct(protected readonly Connector $connector) { // } diff --git a/src/Traits/OAuth2/AuthorizationCodeGrant.php b/src/Traits/OAuth2/AuthorizationCodeGrant.php index 8db18a81..92de1dd7 100644 --- a/src/Traits/OAuth2/AuthorizationCodeGrant.php +++ b/src/Traits/OAuth2/AuthorizationCodeGrant.php @@ -25,6 +25,7 @@ trait AuthorizationCodeGrant { use HasOAuthConfig; + use HasClock; /** * The state generated by the getAuthorizationUrl method. @@ -158,7 +159,7 @@ protected function createOAuthAuthenticatorFromResponse(Response $response, ?str $expiresAt = null; if (isset($responseData->expires_in) && is_numeric($responseData->expires_in)) { - $expiresAt = (new DateTimeImmutable)->add( + $expiresAt = $this->getClock()->now()->add( DateInterval::createFromDateString((int)$responseData->expires_in . ' seconds') ); } @@ -171,7 +172,7 @@ protected function createOAuthAuthenticatorFromResponse(Response $response, ?str */ protected function createOAuthAuthenticator(string $accessToken, ?string $refreshToken = null, ?DateTimeImmutable $expiresAt = null): OAuthAuthenticator { - return new AccessTokenAuthenticator($accessToken, $refreshToken, $expiresAt); + return new AccessTokenAuthenticator($accessToken, $refreshToken, $expiresAt, $this->getClock()); } /** diff --git a/src/Traits/OAuth2/ClientCredentialsGrant.php b/src/Traits/OAuth2/ClientCredentialsGrant.php index 2e6a0f97..d8789085 100644 --- a/src/Traits/OAuth2/ClientCredentialsGrant.php +++ b/src/Traits/OAuth2/ClientCredentialsGrant.php @@ -19,6 +19,7 @@ trait ClientCredentialsGrant { use HasOAuthConfig; + use HasClock; /** * Get the access token @@ -62,7 +63,7 @@ protected function createOAuthAuthenticatorFromResponse(Response $response): OAu $expiresAt = null; if (isset($responseData->expires_in) && is_numeric($responseData->expires_in)) { - $expiresAt = (new DateTimeImmutable)->add( + $expiresAt = $this->getClock()->now()->add( DateInterval::createFromDateString((int)$responseData->expires_in . ' seconds') ); } @@ -75,7 +76,7 @@ protected function createOAuthAuthenticatorFromResponse(Response $response): OAu */ protected function createOAuthAuthenticator(string $accessToken, ?DateTimeImmutable $expiresAt = null): OAuthAuthenticator { - return new AccessTokenAuthenticator($accessToken, null, $expiresAt); + return new AccessTokenAuthenticator($accessToken, null, $expiresAt, $this->getClock()); } /** diff --git a/src/Traits/OAuth2/HasClock.php b/src/Traits/OAuth2/HasClock.php new file mode 100644 index 00000000..4016d50c --- /dev/null +++ b/src/Traits/OAuth2/HasClock.php @@ -0,0 +1,21 @@ +clock ??= new SystemClock(); + } +} diff --git a/tests/Feature/Oauth2/AuthCodeFlowConnectorTest.php b/tests/Feature/Oauth2/AuthCodeFlowConnectorTest.php index 0e4f6e83..01165052 100644 --- a/tests/Feature/Oauth2/AuthCodeFlowConnectorTest.php +++ b/tests/Feature/Oauth2/AuthCodeFlowConnectorTest.php @@ -4,9 +4,9 @@ use Saloon\Http\Request; use Saloon\Http\Response; -use Saloon\Tests\Helpers\Date; use Saloon\Http\Faking\MockClient; use Saloon\Http\Faking\MockResponse; +use Saloon\Tests\Helpers\FrozenClock; use Saloon\Http\OAuth2\GetUserRequest; use Saloon\Exceptions\InvalidStateException; use Saloon\Http\OAuth2\GetAccessTokenRequest; @@ -97,7 +97,8 @@ MockResponse::make(['access_token' => 'access', 'refresh_token' => 'refresh', 'expires_in' => 3600], 200), ]); - $connector = new OAuth2Connector; + $frozenClock = FrozenClock::fromString('2024-01-01T12:00:00+00:00'); + $connector = new OAuth2Connector($frozenClock); $connector->withMockClient($mockClient); @@ -107,6 +108,7 @@ expect($authenticator->getAccessToken())->toEqual('access'); expect($authenticator->getRefreshToken())->toEqual('refresh'); expect($authenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class); + expect($authenticator->getExpiresAt()->getTimestamp() - $frozenClock->now()->getTimestamp())->toEqual(3600); }); test('you can tap into the access token request and modify it', function () { @@ -151,7 +153,7 @@ $connector = new OAuth2Connector; $state = 'secret'; - $url = $connector->getAuthorizationUrl(['scope-1', 'scope-2'], $state); + $connector->getAuthorizationUrl(['scope-1', 'scope-2'], $state); $connector->getAccessToken('code', 'invalid', $state); })->throws(InvalidStateException::class, 'Invalid state.'); @@ -161,11 +163,12 @@ MockResponse::make(['access_token' => 'access-new', 'refresh_token' => 'refresh-new', 'expires_in' => 3600]), ]); - $connector = new OAuth2Connector; + $frozenClock = FrozenClock::fromString('2024-01-01T12:00:00+00:00'); + $connector = new OAuth2Connector($frozenClock); $connector->withMockClient($mockClient); - $authenticator = new AccessTokenAuthenticator('access', 'refresh', Date::now()->addSeconds(3600)->toDateTime()); + $authenticator = new AccessTokenAuthenticator('access', 'refresh', $frozenClock->addSeconds(3600), $frozenClock); $newAuthenticator = $connector->refreshAccessToken($authenticator); @@ -173,6 +176,7 @@ expect($newAuthenticator->getAccessToken())->toEqual('access-new'); expect($newAuthenticator->getRefreshToken())->toEqual('refresh-new'); expect($newAuthenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class); + expect($newAuthenticator->getExpiresAt()->getTimestamp() - $frozenClock->now()->getTimestamp())->toEqual(3600); }); test('you can tap into the refresh token request', function () { @@ -184,7 +188,7 @@ $connector->withMockClient($mockClient); - $authenticator = new AccessTokenAuthenticator('access', 'refresh', Date::now()->addSeconds(3600)->toDateTime()); + $authenticator = new AccessTokenAuthenticator('access', 'refresh', FrozenClock::fromString('2024-01-01T12:00:00+00:00')->addSeconds(3600)); $newAuthenticator = $connector->refreshAccessToken($authenticator, requestModifier: function (Request $request) { $request->query()->add('yee', 'haw'); @@ -209,7 +213,7 @@ $connector->withMockClient($mockClient); - $authenticator = new AccessTokenAuthenticator('access', null, Date::now()->addSeconds(3600)->toDateTime()); + $authenticator = new AccessTokenAuthenticator('access', null, FrozenClock::fromString('2024-01-01T12:00:00+00:00')->addSeconds(3600)); $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('The provided OAuthAuthenticator does not contain a refresh token.'); @@ -226,7 +230,7 @@ $connector->withMockClient($mockClient); - $authenticator = new AccessTokenAuthenticator('access', 'refresh', Date::now()->addSeconds(3600)->toDateTime()); + $authenticator = new AccessTokenAuthenticator('access', 'refresh', FrozenClock::fromString('2024-01-01T12:00:00+00:00')->addSeconds(3600)); $response = $connector->refreshAccessToken($authenticator, true); @@ -242,7 +246,7 @@ $connector = new OAuth2Connector; $connector->withMockClient($mockClient); - $accessToken = new AccessTokenAuthenticator('access', 'refresh', Date::now()->addSeconds(3600)->toDateTime()); + $accessToken = new AccessTokenAuthenticator('access', 'refresh', FrozenClock::fromString('2024-01-01T12:00:00+00:00')->addSeconds(3600)); $response = $connector->getUser($accessToken); @@ -265,7 +269,7 @@ $connector = new OAuth2Connector; $connector->withMockClient($mockClient); - $accessToken = new AccessTokenAuthenticator('access', 'refresh', Date::now()->addSeconds(3600)->toDateTime()); + $accessToken = new AccessTokenAuthenticator('access', 'refresh', FrozenClock::fromString('2024-01-01T12:00:00+00:00')->addSeconds(3600)); $response = $connector->getUser($accessToken, function (Request $request) { $request->query()->add('yee', 'haw'); @@ -305,7 +309,8 @@ GetUserRequest::class => MockResponse::make(['user' => 'Sam']), ]); - $connector = new OAuth2Connector; + $frozenClock = FrozenClock::fromString('2024-01-01T12:00:00+00:00'); + $connector = new OAuth2Connector($frozenClock); $requests = []; $connector->oauthConfig()->setRequestModifier(function (Request $request) use (&$requests) { @@ -326,6 +331,7 @@ expect($authenticator->getAccessToken())->toEqual('access'); expect($authenticator->getRefreshToken())->toEqual('refresh'); expect($authenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class); + expect($authenticator->getExpiresAt()->getTimestamp() - $frozenClock->now()->getTimestamp())->toEqual(3600); expect($mockClient->getLastPendingRequest()->query()->all())->toEqual(['request' => 'access']); $newAuthenticator = $connector->refreshAccessToken($authenticator); @@ -334,6 +340,7 @@ expect($newAuthenticator->getAccessToken())->toEqual('access-new'); expect($newAuthenticator->getRefreshToken())->toEqual('refresh-new'); expect($newAuthenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class); + expect($authenticator->getExpiresAt()->getTimestamp() - $frozenClock->now()->getTimestamp())->toEqual(3600); expect($mockClient->getLastPendingRequest()->query()->all())->toEqual(['request' => 'refresh']); $response = $connector->getUser($newAuthenticator); diff --git a/tests/Feature/Oauth2/ClientCredentialsFlowConnectorTest.php b/tests/Feature/Oauth2/ClientCredentialsFlowConnectorTest.php index c6f4a892..6897819f 100644 --- a/tests/Feature/Oauth2/ClientCredentialsFlowConnectorTest.php +++ b/tests/Feature/Oauth2/ClientCredentialsFlowConnectorTest.php @@ -6,6 +6,7 @@ use Saloon\Http\Response; use Saloon\Http\Faking\MockClient; use Saloon\Http\Faking\MockResponse; +use Saloon\Tests\Helpers\FrozenClock; use Saloon\Http\Auth\AccessTokenAuthenticator; use Saloon\Exceptions\OAuthConfigValidationException; use Saloon\Tests\Fixtures\Connectors\ClientCredentialsConnector; @@ -19,7 +20,9 @@ MockResponse::make(['access_token' => 'access', 'expires_in' => 3600], 200), ]); - $connector = new ClientCredentialsConnector; + + $frozenClock = FrozenClock::fromString('2024-01-01T12:00:00+00:00'); + $connector = new ClientCredentialsConnector($frozenClock); $connector->withMockClient($mockClient); $authenticator = $connector->getAccessToken(); @@ -29,6 +32,7 @@ expect($authenticator->getRefreshToken())->toBeNull(); expect($authenticator->isRefreshable())->toBeFalse(); expect($authenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class); + expect($authenticator->getExpiresAt()->getTimestamp() - $frozenClock->now()->getTimestamp())->toEqual(3600); $mockClient->assertSentCount(1); @@ -203,7 +207,8 @@ MockResponse::make(['access_token' => 'access', 'expires_in' => 3600], 200), ]); - $connector = new ClientCredentialsBasicAuthConnector; + $frozenClock = FrozenClock::fromString('2024-01-01T12:00:00+00:00'); + $connector = new ClientCredentialsBasicAuthConnector($frozenClock); $connector->withMockClient($mockClient); $authenticator = $connector->getAccessToken(); @@ -213,6 +218,7 @@ expect($authenticator->getRefreshToken())->toBeNull(); expect($authenticator->isRefreshable())->toBeFalse(); expect($authenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class); + expect($authenticator->getExpiresAt()->getTimestamp() - $frozenClock->now()->getTimestamp())->toEqual(3600); $mockClient->assertSentCount(1); diff --git a/tests/Fixtures/Authenticators/CustomOAuthAuthenticator.php b/tests/Fixtures/Authenticators/CustomOAuthAuthenticator.php index 8ea17b3b..b56237f6 100644 --- a/tests/Fixtures/Authenticators/CustomOAuthAuthenticator.php +++ b/tests/Fixtures/Authenticators/CustomOAuthAuthenticator.php @@ -13,10 +13,10 @@ class CustomOAuthAuthenticator extends AccessTokenAuthenticator * Constructor */ public function __construct( - readonly public string $accessToken, - readonly public string $greeting, - readonly public ?string $refreshToken = null, - readonly public ?DateTimeImmutable $expiresAt = null, + public readonly string $accessToken, + public readonly string $greeting, + public readonly ?string $refreshToken = null, + public readonly ?DateTimeImmutable $expiresAt = null, ) { // } diff --git a/tests/Fixtures/Connectors/ClientCredentialsBasicAuthConnector.php b/tests/Fixtures/Connectors/ClientCredentialsBasicAuthConnector.php index c161dbf9..0bbd74c7 100644 --- a/tests/Fixtures/Connectors/ClientCredentialsBasicAuthConnector.php +++ b/tests/Fixtures/Connectors/ClientCredentialsBasicAuthConnector.php @@ -5,6 +5,8 @@ namespace Saloon\Tests\Fixtures\Connectors; use Saloon\Http\Connector; +use Psr\Clock\ClockInterface; +use Saloon\Tests\Helpers\FrozenClock; use Saloon\Helpers\OAuth2\OAuthConfig; use Saloon\Traits\OAuth2\ClientCredentialsBasicAuthGrant; @@ -12,6 +14,11 @@ class ClientCredentialsBasicAuthConnector extends Connector { use ClientCredentialsBasicAuthGrant; + public function __construct(?ClockInterface $clock = null) + { + $this->clock = $clock ?? FrozenClock::fromString('2024-01-01T12:00:00+00:00'); + } + /** * Define the base URL. */ diff --git a/tests/Fixtures/Connectors/ClientCredentialsConnector.php b/tests/Fixtures/Connectors/ClientCredentialsConnector.php index 6863618a..d215161a 100644 --- a/tests/Fixtures/Connectors/ClientCredentialsConnector.php +++ b/tests/Fixtures/Connectors/ClientCredentialsConnector.php @@ -5,6 +5,8 @@ namespace Saloon\Tests\Fixtures\Connectors; use Saloon\Http\Connector; +use Psr\Clock\ClockInterface; +use Saloon\Tests\Helpers\FrozenClock; use Saloon\Helpers\OAuth2\OAuthConfig; use Saloon\Traits\OAuth2\ClientCredentialsGrant; @@ -12,6 +14,11 @@ class ClientCredentialsConnector extends Connector { use ClientCredentialsGrant; + public function __construct(?ClockInterface $clock = null) + { + $this->clock = $clock ?? FrozenClock::fromString('2024-01-01T12:00:00+00:00'); + } + /** * Define the base URL. */ diff --git a/tests/Fixtures/Connectors/OAuth2Connector.php b/tests/Fixtures/Connectors/OAuth2Connector.php index abbdc478..95bc9df0 100644 --- a/tests/Fixtures/Connectors/OAuth2Connector.php +++ b/tests/Fixtures/Connectors/OAuth2Connector.php @@ -5,6 +5,8 @@ namespace Saloon\Tests\Fixtures\Connectors; use Saloon\Http\Connector; +use Psr\Clock\ClockInterface; +use Saloon\Tests\Helpers\FrozenClock; use Saloon\Helpers\OAuth2\OAuthConfig; use Saloon\Traits\OAuth2\AuthorizationCodeGrant; @@ -12,6 +14,11 @@ class OAuth2Connector extends Connector { use AuthorizationCodeGrant; + public function __construct(?ClockInterface $clock = null) + { + $this->clock = $clock ?? FrozenClock::fromString('2024-01-01T12:00:00+00:00'); + } + /** * Define the base URL. */ diff --git a/tests/Fixtures/Requests/QueryParameterRequest.php b/tests/Fixtures/Requests/QueryParameterRequest.php index b7c8e5fd..9f4c863e 100644 --- a/tests/Fixtures/Requests/QueryParameterRequest.php +++ b/tests/Fixtures/Requests/QueryParameterRequest.php @@ -25,7 +25,7 @@ class QueryParameterRequest extends Request /** * Constructor */ - public function __construct(readonly public string $endpoint = '/user') + public function __construct(public readonly string $endpoint = '/user') { // } diff --git a/tests/Helpers/Date.php b/tests/Helpers/Date.php deleted file mode 100644 index 3ce133e4..00000000 --- a/tests/Helpers/Date.php +++ /dev/null @@ -1,64 +0,0 @@ -dateTime->add( - DateInterval::createFromDateString($seconds . ' seconds') - ); - - return $this; - } - - /** - * Subtract minutes - * - * @return $this - */ - public function subMinutes(int $minutes): self - { - $this->dateTime->sub( - DateInterval::createFromDateString($minutes . ' minutes') - ); - - return $this; - } - - /** - * Get the datetime instance - */ - public function toDateTime(): DateTimeImmutable - { - return DateTimeImmutable::createFromMutable($this->dateTime); - } -} diff --git a/tests/Helpers/FrozenClock.php b/tests/Helpers/FrozenClock.php new file mode 100644 index 00000000..b9a34a60 --- /dev/null +++ b/tests/Helpers/FrozenClock.php @@ -0,0 +1,47 @@ +dateTime; + } + + public function addSeconds(int $seconds): DateTimeImmutable + { + return $this->dateTime->add(DateInterval::createFromDateString($seconds . ' seconds')); + } + + public function subSeconds(int $seconds): DateTimeImmutable + { + return $this->dateTime->sub(DateInterval::createFromDateString($seconds . ' seconds')); + } + + public function subMinutes(int $minutes): DateTimeImmutable + { + return $this->dateTime->sub(DateInterval::createFromDateString($minutes . ' minutes')); + } +} diff --git a/tests/Unit/Oauth2/AccessTokenAuthenticatorTest.php b/tests/Unit/Oauth2/AccessTokenAuthenticatorTest.php index fc3eb0a5..0bd4e924 100644 --- a/tests/Unit/Oauth2/AccessTokenAuthenticatorTest.php +++ b/tests/Unit/Oauth2/AccessTokenAuthenticatorTest.php @@ -2,15 +2,16 @@ declare(strict_types=1); -use Saloon\Tests\Helpers\Date; +use Saloon\Tests\Helpers\FrozenClock; use Saloon\Http\Auth\AccessTokenAuthenticator; it('can be serialized and unserialized', function () { $accessToken = 'access'; $refreshToken = 'refresh'; - $expiresAt = Date::now()->toDateTime(); + $frozenClock = FrozenClock::fromString('2024-01-01T12:00:00+00:00'); + $expiresAt = $frozenClock->addSeconds(3600); - $authenticator = new AccessTokenAuthenticator($accessToken, $refreshToken, $expiresAt); + $authenticator = new AccessTokenAuthenticator($accessToken, $refreshToken, $expiresAt, $frozenClock); expect($authenticator->getAccessToken())->toEqual($accessToken); expect($authenticator->getRefreshToken())->toEqual($refreshToken); @@ -28,9 +29,10 @@ it('can return if it has expired or not', function () { $accessToken = 'access'; $refreshToken = 'refresh'; - $expiresAt = Date::now()->subMinutes(5)->toDateTime(); + $frozenClock = FrozenClock::fromString('2024-01-01T12:00:00+00:00'); + $expiresAt = $frozenClock->subMinutes(5); - $authenticator = new AccessTokenAuthenticator($accessToken, $refreshToken, $expiresAt); + $authenticator = new AccessTokenAuthenticator($accessToken, $refreshToken, $expiresAt, $frozenClock); expect($authenticator->isRefreshable())->toBeTrue(); expect($authenticator->isNotRefreshable())->toBeFalse(); @@ -49,9 +51,10 @@ }); test('can be constructed with just an access token and expiry', function () { - $expiresAt = Date::now()->subMinutes(5)->toDateTime(); + $frozenClock = FrozenClock::fromString('2024-01-01T12:00:00+00:00'); + $expiresAt = $frozenClock->subMinutes(5); - $authenticator = new AccessTokenAuthenticator('access', null, $expiresAt); + $authenticator = new AccessTokenAuthenticator('access', null, $expiresAt, $frozenClock); expect($authenticator->hasExpired())->toBeTrue(); expect($authenticator->hasNotExpired())->toBeFalse(); diff --git a/tests/Unit/Oauth2/AuthCodeFlowConnectorTest.php b/tests/Unit/Oauth2/AuthCodeFlowConnectorTest.php index c0dd8887..bc88b199 100644 --- a/tests/Unit/Oauth2/AuthCodeFlowConnectorTest.php +++ b/tests/Unit/Oauth2/AuthCodeFlowConnectorTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -use Saloon\Tests\Helpers\Date; use Saloon\Http\Faking\MockClient; use Saloon\Http\Faking\MockResponse; +use Saloon\Tests\Helpers\FrozenClock; use Saloon\Helpers\OAuth2\OAuthConfig; use Saloon\Http\Auth\AccessTokenAuthenticator; use Saloon\Exceptions\OAuthConfigValidationException; @@ -47,11 +47,12 @@ MockResponse::make(['access_token' => 'access-new', 'expires_in' => 3600]), ]); - $connector = new OAuth2Connector; + $frozenClock = FrozenClock::fromString('2024-01-01T12:00:00+00:00'); + $connector = new OAuth2Connector($frozenClock); $connector->withMockClient($mockClient); - $authenticator = new AccessTokenAuthenticator('access', 'refresh-old', Date::now()->addSeconds(3600)->toDateTime()); + $authenticator = new AccessTokenAuthenticator('access', 'refresh-old', $frozenClock->addSeconds(3600), $frozenClock); $newAuthenticator = $connector->refreshAccessToken($authenticator);