diff --git a/src/Credentials.php b/src/Credentials.php index 29c9059..8ade1a0 100644 --- a/src/Credentials.php +++ b/src/Credentials.php @@ -112,8 +112,16 @@ public function addAuthToRequest(PendingRequest $httpClient, Options $options): } if (is_callable($this->customCallback)) { return ($this->customCallback)($httpClient); + } + if ($options->authType === self::AUTH_TYPE_BASIC) { + if (! $this->clientId || ! $this->clientSecret) { + throw new InvalidArgumentException('Basic auth requires client id and client secret. Check documentation/readme.'); + } + + return $httpClient->withBasicAuth($this->clientId, $this->clientSecret); } + if ($this->token) { if ($options->authType === self::AUTH_TYPE_QUERY) { return $httpClient->withQueryParameters([ @@ -123,16 +131,7 @@ public function addAuthToRequest(PendingRequest $httpClient, Options $options): return $httpClient->withToken($this->token, $options->authType); } - if ($options->authType === self::AUTH_TYPE_BASIC) { - if (! $this->clientId || ! $this->clientSecret) { - throw new InvalidArgumentException('Basic auth requires client id and client secret. Check documentation/readme. '); - } - return $httpClient->withBasicAuth($this->clientId, $this->clientSecret); - } - if ($options->authType === self::AUTH_TYPE_CUSTOM && is_callable($this->customCallback)) { - return ($this->customCallback)($httpClient); - } return $httpClient; } diff --git a/src/Options.php b/src/Options.php index a47999d..99e6528 100644 --- a/src/Options.php +++ b/src/Options.php @@ -2,6 +2,7 @@ namespace Pelmered\LaravelHttpOAuthHelper; +use Carbon\Carbon; use Closure; use Illuminate\Support\Facades\Validator; use Illuminate\Validation\Rule; @@ -13,11 +14,11 @@ class Options */ final public function __construct( public array $scopes = [], - public string $authType = Credentials::AUTH_TYPE_BASIC, //TODO: Which auth type should be default? + public string $authType = Credentials::AUTH_TYPE_BEARER, public string $grantType = Credentials::GRANT_TYPE_CLIENT_CREDENTIALS, public string $tokenType = AccessToken::TOKEN_TYPE_BEARER, public string $tokenName = 'token', - public int|string|Closure $expires = 3600, + public int|string|Closure|Carbon $expires = 3600, public string|Closure $accessToken = 'access_token', public ?Closure $tokenTypeCustomCallback = null, public ?string $cacheKey = null, @@ -91,7 +92,7 @@ protected static function getDefaults(): array 'scopes' => [], 'grantType' => Credentials::GRANT_TYPE_CLIENT_CREDENTIALS, 'tokenType' => AccessToken::TOKEN_TYPE_BEARER, - 'authType' => Credentials::AUTH_TYPE_BASIC, + 'authType' => Credentials::AUTH_TYPE_BEARER, 'expires' => 3600, 'accessToken' => 'access_token', ]; diff --git a/src/RefreshToken.php b/src/RefreshToken.php index 885601c..234dddf 100644 --- a/src/RefreshToken.php +++ b/src/RefreshToken.php @@ -67,6 +67,10 @@ protected function getExpiresAtFromResponse(Response $response, callable|string| $expires = $response->json()[$expires]; } + if (is_int($expires)) { + return Carbon::now()->addSeconds($expires - 60); + } + return Carbon::parse($expires)->subMinute(); } diff --git a/tests/Pest.php b/tests/Pest.php index 5949c61..22dc778 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -24,10 +24,16 @@ | */ +use Carbon\Carbon; + expect()->extend('toBeOne', function () { return $this->toBe(1); }); +expect()->extend('toBeWithin', function ($integer, $acceptableDiff) { + return $this->toBeBetween($integer-$acceptableDiff, $integer+$acceptableDiff); +}); + /* |-------------------------------------------------------------------------- | Functions @@ -41,5 +47,12 @@ function something() { - // .. + +} +function isSameAccessToken($accessToken1, $accessToken2) +{ + expect($accessToken1->getAccessToken())->toBe($accessToken2->getAccessToken()) + ->and($accessToken1->getExpiresIn())->toBeWithin($accessToken1->getExpiresIn(), 10) + ->and($accessToken1->getExpiresAt())->toBeInstanceOf(Carbon::class) + ->and($accessToken1->getCustomCallback())->toBe($accessToken1->getCustomCallback()); } diff --git a/tests/Unit/MacroTest.php b/tests/Unit/MacroTest.php index 3801418..1c90324 100644 --- a/tests/Unit/MacroTest.php +++ b/tests/Unit/MacroTest.php @@ -32,7 +32,10 @@ [ 'my_client_id', 'my_client_secret', ], - ['scopes' => ['scope1', 'scope2']], + [ + 'scopes' => ['scope1', 'scope2'], + 'authType' => 'basic', + ], )->get('https://example.com/api'); expect($response->json()['data'])->toBe('some data with bearer token'); diff --git a/tests/Unit/RefreshTokenTest.php b/tests/Unit/RefreshTokenTest.php index 82208b2..652e3df 100644 --- a/tests/Unit/RefreshTokenTest.php +++ b/tests/Unit/RefreshTokenTest.php @@ -26,6 +26,7 @@ new Options( scopes: ['scope1', 'scope2'], grantType: 'client_credentials', + authType: 'basic' ), ); @@ -38,6 +39,23 @@ }); }); + test('refresh token basic with invalid credentials', function () { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Basic auth requires client id and client secret. Check documentation/readme.'); + + $accessToken = app(RefreshToken::class)( + 'https://example.com/oauth/token', + new Credentials([ + 'token', + ]), + new Options( + scopes: ['scope1', 'scope2'], + authType: Credentials::AUTH_TYPE_BASIC, + grantType: 'client_credentials', + ), + ); + }); + test('refresh token body', function () { Cache::clear(); $accessToken = app(RefreshToken::class)( @@ -186,6 +204,7 @@ accessToken: static function ($response) { return $response->json()['custom_access_token']; }, + authType: Credentials::AUTH_TYPE_BASIC, ), ); @@ -302,6 +321,7 @@ && $request->url() === 'https://example.com/oauth/token'; }); }); + test('auth type query', function () { app(RefreshToken::class)( @@ -322,9 +342,92 @@ expect($token)->toBe('my_query_token'); - return $request->url() === 'https://example.com/oauth/token?custom_token_name=my_query_token'; }); }); + test('set token expiry with string key with date', function () { + + $this->clearExistingFakes(); + + /** @var Carbon $nowDate */ + $nowDate = Carbon::create(2024, 11, 11, 11); + + Carbon::setTestNow($nowDate); + + Http::fake([ + 'https://example.com/oauth/token' => Http::response([ + 'token_type' => 'Bearer', + 'access_token' => 'my_custom_access_token', + 'scope' => 'scope1 scope2', + 'expires_date' => $nowDate->addHour(), + ], 200), + ]); + + $accessToken = app(RefreshToken::class)( + 'https://example.com/oauth/token', + new Credentials('my_query_token'), + new Options( + scopes: ['scope1', 'scope2'], + expires: 'expires_date', + ), + ); + + expect($accessToken->getExpiresAt()->timestamp)->toBe($nowDate->subMinute()->timestamp); + }); + + test('set token expiry with string key with integer', function () { + + /** @var Carbon $nowDate */ + $nowDate = Carbon::create(2024, 11, 11, 11); + + Carbon::setTestNow($nowDate); + + $accessToken = app(RefreshToken::class)( + 'https://example.com/oauth/token', + new Credentials('my_query_token'), + new Options( + scopes: ['scope1', 'scope2'], + expires: 'expires_in', + ), + ); + + expect($accessToken->getExpiresAt()->timestamp)->toBe($nowDate->addSeconds(7200)->subMinute()->timestamp); + }); + + test('set token expiry with carbon object', function () { + + /** @var Carbon $nowDate */ + $nowDate = Carbon::create(2024, 11, 11, 11); + + Carbon::setTestNow($nowDate); + + $accessToken = app(RefreshToken::class)( + 'https://example.com/oauth/token', + new Credentials('my_query_token'), + new Options( + scopes: ['scope1', 'scope2'], + expires: Carbon::now()->addHour(), + ), + ); + + expect($accessToken->getExpiresAt()->timestamp)->toBe($nowDate->addHour()->subMinute()->timestamp); + }); + + test('invalid token expiry', function () { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Invalid expires option'); + + app(RefreshToken::class)( + 'https://example.com/oauth/token', + new Credentials('my_query_token'), + new Options( + scopes: ['scope1', 'scope2'], + expires: function () { + return new stdClass; + }, + ), + ); + }); + })->done(assignee: 'pelmered'); diff --git a/tests/Unit/TokenStoreTest.php b/tests/Unit/TokenStoreTest.php index f0d4510..d83dba6 100644 --- a/tests/Unit/TokenStoreTest.php +++ b/tests/Unit/TokenStoreTest.php @@ -2,9 +2,12 @@ uses(\Pelmered\LaravelHttpOAuthHelper\Tests\TestCase::class); +use Carbon\Carbon; +use Illuminate\Cache\ArrayStore; use Illuminate\Cache\FileStore; use Illuminate\Support\Facades\Cache; use Orchestra\Testbench\Attributes\DefineEnvironment; +use Pelmered\LaravelHttpOAuthHelper\AccessToken; use Pelmered\LaravelHttpOAuthHelper\Credentials; use Pelmered\LaravelHttpOAuthHelper\Options; use Pelmered\LaravelHttpOAuthHelper\TokenStore; @@ -14,29 +17,43 @@ }); -/* -it('reads and stores a token in cache', function () { +it('reads and stores a token in cache be default', function () { Cache::clear(); - Cache::spy(); - Cache::shouldReceive('get')->once()->with('oauth_token_example.comoauthtoken')->andReturn(null); + /** @var Carbon $nowDate */ + $nowDate = Carbon::create(2024, 11, 11, 11); - $accessToken = TokenStore::get( + Carbon::setTestNow($nowDate); + + $cacheBefore = Cache::get('oauth_token_example.comoauthtoken'); + + $accessToken1 = TokenStore::get( 'https://example.com/oauth/token', - new Credentials( - clientId: 'this_is_my_client_id', - clientSecret: 'this_is_my_client_secret', + new Credentials('my_token'), + new Options( + scopes: ['scope1', 'scope2'], ), + ); + + $cacheAfterOne = Cache::get('oauth_token_example.comoauthtoken'); + + Carbon::setTestNow($nowDate->addHour()); + + $accessToken2 = TokenStore::get( + 'https://example.com/oauth/token', + new Credentials('my_token'), new Options( scopes: ['scope1', 'scope2'], - authType: Credentials::AUTH_TYPE_BASIC, ), ); - // Does not work with composer update --prefer-lowest - //Cache::shouldHaveReceived('put')->once()->with('oauth_token_example.comoauthtoken', $accessToken, 3540); + expect($cacheBefore)->toBeNull(); + + isSameAccessToken($accessToken1, $cacheAfterOne); + + isSameAccessToken($accessToken1, $accessToken2); + }); -*/ it('reads and stores a token in cache with custom cache driver', function () {