Skip to content

Commit 302087a

Browse files
committed
Add PSR-20 Clock Support
1 parent b1868db commit 302087a

18 files changed

+162
-98
lines changed

composer.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
"guzzlehttp/guzzle": "^7.6",
2424
"guzzlehttp/promises": "^1.5 || ^2.0",
2525
"guzzlehttp/psr7": "^2.0",
26+
"psr/clock": "^1.0",
2627
"psr/http-factory": "^1.0",
2728
"psr/http-message": "^1.1 || ^2.0"
2829
},

src/Helpers/SystemClock.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Saloon\Helpers;
6+
7+
use DateTimeImmutable;
8+
use Psr\Clock\ClockInterface;
9+
10+
class SystemClock implements ClockInterface
11+
{
12+
public function now(): DateTimeImmutable
13+
{
14+
return new DateTimeImmutable();
15+
}
16+
}

src/Http/Auth/AccessTokenAuthenticator.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
namespace Saloon\Http\Auth;
66

77
use DateTimeImmutable;
8+
use Psr\Clock\ClockInterface;
9+
use Saloon\Helpers\SystemClock;
810
use Saloon\Http\PendingRequest;
911
use Saloon\Contracts\OAuthAuthenticator;
1012

@@ -17,6 +19,7 @@ public function __construct(
1719
public readonly string $accessToken,
1820
public readonly ?string $refreshToken = null,
1921
public readonly ?DateTimeImmutable $expiresAt = null,
22+
private readonly ?ClockInterface $clock = null,
2023
) {
2124
//
2225
}
@@ -38,7 +41,7 @@ public function hasExpired(): bool
3841
return false;
3942
}
4043

41-
return $this->expiresAt->getTimestamp() <= (new DateTimeImmutable)->getTimestamp();
44+
return $this->expiresAt->getTimestamp() <= ($this->clock ?? new SystemClock())->now()->getTimestamp();
4245
}
4346

4447
/**

src/Http/BaseResource.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class BaseResource
99
/**
1010
* Constructor
1111
*/
12-
public function __construct(readonly protected Connector $connector)
12+
public function __construct(protected readonly Connector $connector)
1313
{
1414
//
1515
}

src/Traits/OAuth2/AuthorizationCodeGrant.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
trait AuthorizationCodeGrant
2626
{
2727
use HasOAuthConfig;
28+
use HasClock;
2829

2930
/**
3031
* The state generated by the getAuthorizationUrl method.
@@ -158,7 +159,7 @@ protected function createOAuthAuthenticatorFromResponse(Response $response, ?str
158159
$expiresAt = null;
159160

160161
if (isset($responseData->expires_in) && is_numeric($responseData->expires_in)) {
161-
$expiresAt = (new DateTimeImmutable)->add(
162+
$expiresAt = $this->getClock()->now()->add(
162163
DateInterval::createFromDateString((int)$responseData->expires_in . ' seconds')
163164
);
164165
}
@@ -171,7 +172,7 @@ protected function createOAuthAuthenticatorFromResponse(Response $response, ?str
171172
*/
172173
protected function createOAuthAuthenticator(string $accessToken, ?string $refreshToken = null, ?DateTimeImmutable $expiresAt = null): OAuthAuthenticator
173174
{
174-
return new AccessTokenAuthenticator($accessToken, $refreshToken, $expiresAt);
175+
return new AccessTokenAuthenticator($accessToken, $refreshToken, $expiresAt, $this->getClock());
175176
}
176177

177178
/**

src/Traits/OAuth2/ClientCredentialsGrant.php

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
trait ClientCredentialsGrant
2020
{
2121
use HasOAuthConfig;
22+
use HasClock;
2223

2324
/**
2425
* Get the access token
@@ -62,7 +63,7 @@ protected function createOAuthAuthenticatorFromResponse(Response $response): OAu
6263
$expiresAt = null;
6364

6465
if (isset($responseData->expires_in) && is_numeric($responseData->expires_in)) {
65-
$expiresAt = (new DateTimeImmutable)->add(
66+
$expiresAt = $this->getClock()->now()->add(
6667
DateInterval::createFromDateString((int)$responseData->expires_in . ' seconds')
6768
);
6869
}
@@ -75,7 +76,7 @@ protected function createOAuthAuthenticatorFromResponse(Response $response): OAu
7576
*/
7677
protected function createOAuthAuthenticator(string $accessToken, ?DateTimeImmutable $expiresAt = null): OAuthAuthenticator
7778
{
78-
return new AccessTokenAuthenticator($accessToken, null, $expiresAt);
79+
return new AccessTokenAuthenticator($accessToken, null, $expiresAt, $this->getClock());
7980
}
8081

8182
/**

src/Traits/OAuth2/HasClock.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Saloon\Traits\OAuth2;
6+
7+
use Psr\Clock\ClockInterface;
8+
use Saloon\Helpers\SystemClock;
9+
10+
/**
11+
* @phpstan-ignore trait.unused
12+
*/
13+
trait HasClock
14+
{
15+
protected ?ClockInterface $clock = null;
16+
17+
protected function getClock(): ClockInterface
18+
{
19+
return $this->clock ??= new SystemClock();
20+
}
21+
}

tests/Feature/Oauth2/AuthCodeFlowConnectorTest.php

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@
44

55
use Saloon\Http\Request;
66
use Saloon\Http\Response;
7-
use Saloon\Tests\Helpers\Date;
87
use Saloon\Http\Faking\MockClient;
98
use Saloon\Http\Faking\MockResponse;
9+
use Saloon\Tests\Helpers\FrozenClock;
1010
use Saloon\Http\OAuth2\GetUserRequest;
1111
use Saloon\Exceptions\InvalidStateException;
1212
use Saloon\Http\OAuth2\GetAccessTokenRequest;
@@ -97,7 +97,8 @@
9797
MockResponse::make(['access_token' => 'access', 'refresh_token' => 'refresh', 'expires_in' => 3600], 200),
9898
]);
9999

100-
$connector = new OAuth2Connector;
100+
$frozenClock = FrozenClock::fromString('2024-01-01T12:00:00+00:00');
101+
$connector = new OAuth2Connector($frozenClock);
101102

102103
$connector->withMockClient($mockClient);
103104

@@ -107,6 +108,7 @@
107108
expect($authenticator->getAccessToken())->toEqual('access');
108109
expect($authenticator->getRefreshToken())->toEqual('refresh');
109110
expect($authenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class);
111+
expect($authenticator->getExpiresAt()->getTimestamp() - $frozenClock->now()->getTimestamp())->toEqual(3600);
110112
});
111113

112114
test('you can tap into the access token request and modify it', function () {
@@ -151,7 +153,7 @@
151153
$connector = new OAuth2Connector;
152154

153155
$state = 'secret';
154-
$url = $connector->getAuthorizationUrl(['scope-1', 'scope-2'], $state);
156+
$connector->getAuthorizationUrl(['scope-1', 'scope-2'], $state);
155157

156158
$connector->getAccessToken('code', 'invalid', $state);
157159
})->throws(InvalidStateException::class, 'Invalid state.');
@@ -161,18 +163,20 @@
161163
MockResponse::make(['access_token' => 'access-new', 'refresh_token' => 'refresh-new', 'expires_in' => 3600]),
162164
]);
163165

164-
$connector = new OAuth2Connector;
166+
$frozenClock = FrozenClock::fromString('2024-01-01T12:00:00+00:00');
167+
$connector = new OAuth2Connector($frozenClock);
165168

166169
$connector->withMockClient($mockClient);
167170

168-
$authenticator = new AccessTokenAuthenticator('access', 'refresh', Date::now()->addSeconds(3600)->toDateTime());
171+
$authenticator = new AccessTokenAuthenticator('access', 'refresh', $frozenClock->addSeconds(3600), $frozenClock);
169172

170173
$newAuthenticator = $connector->refreshAccessToken($authenticator);
171174

172175
expect($newAuthenticator)->toBeInstanceOf(AccessTokenAuthenticator::class);
173176
expect($newAuthenticator->getAccessToken())->toEqual('access-new');
174177
expect($newAuthenticator->getRefreshToken())->toEqual('refresh-new');
175178
expect($newAuthenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class);
179+
expect($newAuthenticator->getExpiresAt()->getTimestamp() - $frozenClock->now()->getTimestamp())->toEqual(3600);
176180
});
177181

178182
test('you can tap into the refresh token request', function () {
@@ -184,7 +188,7 @@
184188

185189
$connector->withMockClient($mockClient);
186190

187-
$authenticator = new AccessTokenAuthenticator('access', 'refresh', Date::now()->addSeconds(3600)->toDateTime());
191+
$authenticator = new AccessTokenAuthenticator('access', 'refresh', FrozenClock::fromString('2024-01-01T12:00:00+00:00')->addSeconds(3600));
188192

189193
$newAuthenticator = $connector->refreshAccessToken($authenticator, requestModifier: function (Request $request) {
190194
$request->query()->add('yee', 'haw');
@@ -209,7 +213,7 @@
209213

210214
$connector->withMockClient($mockClient);
211215

212-
$authenticator = new AccessTokenAuthenticator('access', null, Date::now()->addSeconds(3600)->toDateTime());
216+
$authenticator = new AccessTokenAuthenticator('access', null, FrozenClock::fromString('2024-01-01T12:00:00+00:00')->addSeconds(3600));
213217

214218
$this->expectException(InvalidArgumentException::class);
215219
$this->expectExceptionMessage('The provided OAuthAuthenticator does not contain a refresh token.');
@@ -226,7 +230,7 @@
226230

227231
$connector->withMockClient($mockClient);
228232

229-
$authenticator = new AccessTokenAuthenticator('access', 'refresh', Date::now()->addSeconds(3600)->toDateTime());
233+
$authenticator = new AccessTokenAuthenticator('access', 'refresh', FrozenClock::fromString('2024-01-01T12:00:00+00:00')->addSeconds(3600));
230234

231235
$response = $connector->refreshAccessToken($authenticator, true);
232236

@@ -242,7 +246,7 @@
242246
$connector = new OAuth2Connector;
243247
$connector->withMockClient($mockClient);
244248

245-
$accessToken = new AccessTokenAuthenticator('access', 'refresh', Date::now()->addSeconds(3600)->toDateTime());
249+
$accessToken = new AccessTokenAuthenticator('access', 'refresh', FrozenClock::fromString('2024-01-01T12:00:00+00:00')->addSeconds(3600));
246250

247251
$response = $connector->getUser($accessToken);
248252

@@ -265,7 +269,7 @@
265269
$connector = new OAuth2Connector;
266270
$connector->withMockClient($mockClient);
267271

268-
$accessToken = new AccessTokenAuthenticator('access', 'refresh', Date::now()->addSeconds(3600)->toDateTime());
272+
$accessToken = new AccessTokenAuthenticator('access', 'refresh', FrozenClock::fromString('2024-01-01T12:00:00+00:00')->addSeconds(3600));
269273

270274
$response = $connector->getUser($accessToken, function (Request $request) {
271275
$request->query()->add('yee', 'haw');
@@ -305,7 +309,8 @@
305309
GetUserRequest::class => MockResponse::make(['user' => 'Sam']),
306310
]);
307311

308-
$connector = new OAuth2Connector;
312+
$frozenClock = FrozenClock::fromString('2024-01-01T12:00:00+00:00');
313+
$connector = new OAuth2Connector($frozenClock);
309314
$requests = [];
310315

311316
$connector->oauthConfig()->setRequestModifier(function (Request $request) use (&$requests) {
@@ -326,6 +331,7 @@
326331
expect($authenticator->getAccessToken())->toEqual('access');
327332
expect($authenticator->getRefreshToken())->toEqual('refresh');
328333
expect($authenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class);
334+
expect($authenticator->getExpiresAt()->getTimestamp() - $frozenClock->now()->getTimestamp())->toEqual(3600);
329335
expect($mockClient->getLastPendingRequest()->query()->all())->toEqual(['request' => 'access']);
330336

331337
$newAuthenticator = $connector->refreshAccessToken($authenticator);
@@ -334,6 +340,7 @@
334340
expect($newAuthenticator->getAccessToken())->toEqual('access-new');
335341
expect($newAuthenticator->getRefreshToken())->toEqual('refresh-new');
336342
expect($newAuthenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class);
343+
expect($authenticator->getExpiresAt()->getTimestamp() - $frozenClock->now()->getTimestamp())->toEqual(3600);
337344
expect($mockClient->getLastPendingRequest()->query()->all())->toEqual(['request' => 'refresh']);
338345

339346
$response = $connector->getUser($newAuthenticator);

tests/Feature/Oauth2/ClientCredentialsFlowConnectorTest.php

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Saloon\Http\Response;
77
use Saloon\Http\Faking\MockClient;
88
use Saloon\Http\Faking\MockResponse;
9+
use Saloon\Tests\Helpers\FrozenClock;
910
use Saloon\Http\Auth\AccessTokenAuthenticator;
1011
use Saloon\Exceptions\OAuthConfigValidationException;
1112
use Saloon\Tests\Fixtures\Connectors\ClientCredentialsConnector;
@@ -19,7 +20,9 @@
1920
MockResponse::make(['access_token' => 'access', 'expires_in' => 3600], 200),
2021
]);
2122

22-
$connector = new ClientCredentialsConnector;
23+
24+
$frozenClock = FrozenClock::fromString('2024-01-01T12:00:00+00:00');
25+
$connector = new ClientCredentialsConnector($frozenClock);
2326
$connector->withMockClient($mockClient);
2427

2528
$authenticator = $connector->getAccessToken();
@@ -29,6 +32,7 @@
2932
expect($authenticator->getRefreshToken())->toBeNull();
3033
expect($authenticator->isRefreshable())->toBeFalse();
3134
expect($authenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class);
35+
expect($authenticator->getExpiresAt()->getTimestamp() - $frozenClock->now()->getTimestamp())->toEqual(3600);
3236

3337
$mockClient->assertSentCount(1);
3438

@@ -203,7 +207,8 @@
203207
MockResponse::make(['access_token' => 'access', 'expires_in' => 3600], 200),
204208
]);
205209

206-
$connector = new ClientCredentialsBasicAuthConnector;
210+
$frozenClock = FrozenClock::fromString('2024-01-01T12:00:00+00:00');
211+
$connector = new ClientCredentialsBasicAuthConnector($frozenClock);
207212
$connector->withMockClient($mockClient);
208213

209214
$authenticator = $connector->getAccessToken();
@@ -213,6 +218,7 @@
213218
expect($authenticator->getRefreshToken())->toBeNull();
214219
expect($authenticator->isRefreshable())->toBeFalse();
215220
expect($authenticator->getExpiresAt())->toBeInstanceOf(DateTimeImmutable::class);
221+
expect($authenticator->getExpiresAt()->getTimestamp() - $frozenClock->now()->getTimestamp())->toEqual(3600);
216222

217223
$mockClient->assertSentCount(1);
218224

tests/Fixtures/Authenticators/CustomOAuthAuthenticator.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,10 @@ class CustomOAuthAuthenticator extends AccessTokenAuthenticator
1313
* Constructor
1414
*/
1515
public function __construct(
16-
readonly public string $accessToken,
17-
readonly public string $greeting,
18-
readonly public ?string $refreshToken = null,
19-
readonly public ?DateTimeImmutable $expiresAt = null,
16+
public readonly string $accessToken,
17+
public readonly string $greeting,
18+
public readonly ?string $refreshToken = null,
19+
public readonly ?DateTimeImmutable $expiresAt = null,
2020
) {
2121
//
2222
}

0 commit comments

Comments
 (0)