add(Duration::fromSeconds(3600))); $refreshToken = RefreshToken::create('refresh_token_1234567890'); $tokenType = TokenType::BEARER; $scope = TokenScope::fromArray(['read', 'write']); $oauthToken = new OAuthToken($accessToken, $refreshToken, $tokenType, $scope); expect($oauthToken->accessToken)->toBe($accessToken); expect($oauthToken->refreshToken)->toBe($refreshToken); expect($oauthToken->tokenType)->toBe($tokenType); expect($oauthToken->scope)->toBe($scope); }); it('creates from provider response with all fields', function () { $response = [ 'access_token' => 'provider_access_token_1234567890', 'refresh_token' => 'provider_refresh_token_1234567890', 'token_type' => 'Bearer', 'expires_in' => 3600, 'scope' => 'read write admin', ]; $token = OAuthToken::fromProviderResponse($response); expect($token->accessToken->toString())->toBe('provider_access_token_1234567890'); expect($token->refreshToken->toString())->toBe('provider_refresh_token_1234567890'); expect($token->tokenType)->toBe(TokenType::BEARER); expect($token->scope->toArray())->toBe(['read', 'write', 'admin']); }); it('creates from provider response with minimal fields', function () { $response = [ 'access_token' => 'minimal_access_token_1234567890', ]; $token = OAuthToken::fromProviderResponse($response); expect($token->accessToken->toString())->toBe('minimal_access_token_1234567890'); expect($token->refreshToken)->toBeNull(); expect($token->tokenType)->toBe(TokenType::BEARER); // Default expect($token->scope)->toBeNull(); }); it('uses default expires_in when not provided', function () { $response = [ 'access_token' => 'token_1234567890', ]; $token = OAuthToken::fromProviderResponse($response); // Default is 3600 seconds (1 hour) $secondsLeft = $token->accessToken->getSecondsUntilExpiration(); expect($secondsLeft)->toBeGreaterThan(3500); expect($secondsLeft)->toBeLessThanOrEqual(3600); }); it('rejects provider response without access_token', function () { OAuthToken::fromProviderResponse([]); })->throws(\InvalidArgumentException::class, 'Provider response missing access_token'); it('detects expired token', function () { $expiredAccessToken = AccessToken::create('expired_1234567890', Timestamp::now()->subtract(Duration::fromSeconds(100))); $token = new OAuthToken($expiredAccessToken, null, TokenType::BEARER, null); expect($token->isExpired())->toBeTrue(); }); it('detects valid non-expired token', function () { $validAccessToken = AccessToken::create('valid_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600))); $token = new OAuthToken($validAccessToken, null, TokenType::BEARER, null); expect($token->isExpired())->toBeFalse(); }); it('checks if token can refresh when refresh token exists', function () { $accessToken = AccessToken::create('access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600))); $refreshToken = RefreshToken::create('refresh_1234567890'); $token = new OAuthToken($accessToken, $refreshToken, TokenType::BEARER, null); expect($token->canRefresh())->toBeTrue(); }); it('checks if token cannot refresh without refresh token', function () { $accessToken = AccessToken::create('access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600))); $token = new OAuthToken($accessToken, null, TokenType::BEARER, null); expect($token->canRefresh())->toBeFalse(); }); it('creates new token with refreshed access token immutably', function () { $originalAccessToken = AccessToken::create('original_access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600))); $originalRefreshToken = RefreshToken::create('original_refresh_1234567890'); $originalToken = new OAuthToken($originalAccessToken, $originalRefreshToken, TokenType::BEARER, null); $newAccessToken = AccessToken::create('new_access_1234567890', Timestamp::now()->add(Duration::fromSeconds(7200))); $newRefreshToken = RefreshToken::create('new_refresh_1234567890'); $refreshedToken = $originalToken->withRefreshedAccessToken($newAccessToken, $newRefreshToken); // Original unchanged expect($originalToken->accessToken->toString())->toBe('original_access_1234567890'); expect($originalToken->refreshToken->toString())->toBe('original_refresh_1234567890'); // New instance updated expect($refreshedToken->accessToken->toString())->toBe('new_access_1234567890'); expect($refreshedToken->refreshToken->toString())->toBe('new_refresh_1234567890'); expect($refreshedToken->tokenType)->toBe(TokenType::BEARER); }); it('refreshes access token while keeping original refresh token', function () { $originalAccessToken = AccessToken::create('original_access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600))); $originalRefreshToken = RefreshToken::create('original_refresh_1234567890'); $originalToken = new OAuthToken($originalAccessToken, $originalRefreshToken, TokenType::BEARER, null); $newAccessToken = AccessToken::create('new_access_1234567890', Timestamp::now()->add(Duration::fromSeconds(7200))); $refreshedToken = $originalToken->withRefreshedAccessToken($newAccessToken); // Original refresh token preserved expect($refreshedToken->refreshToken->toString())->toBe('original_refresh_1234567890'); expect($refreshedToken->accessToken->toString())->toBe('new_access_1234567890'); }); it('converts to array correctly', function () { $accessToken = AccessToken::create('access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600))); $refreshToken = RefreshToken::create('refresh_1234567890'); $scope = TokenScope::fromArray(['read', 'write']); $token = new OAuthToken($accessToken, $refreshToken, TokenType::BEARER, $scope); $array = $token->toArray(); expect($array['access_token'])->toBe('access_1234567890'); expect($array['refresh_token'])->toBe('refresh_1234567890'); expect($array['token_type'])->toBe('Bearer'); expect($array['scope'])->toBe('read write'); expect($array)->toHaveKey('expires_at'); }); it('converts to array with masked tokens', function () { $accessToken = AccessToken::create('access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600))); $refreshToken = RefreshToken::create('refresh_1234567890'); $token = new OAuthToken($accessToken, $refreshToken, TokenType::BEARER, null); $masked = $token->toArrayMasked(); expect($masked['access_token'])->toContain('*'); expect($masked['refresh_token'])->toContain('*'); expect($masked['access_token'] !== 'access_1234567890')->toBeTrue(); expect($masked['refresh_token'] !== 'refresh_1234567890')->toBeTrue(); }); it('handles null refresh token in array conversion', function () { $accessToken = AccessToken::create('access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600))); $token = new OAuthToken($accessToken, null, TokenType::BEARER, null); $array = $token->toArray(); expect($array)->toHaveKey('access_token'); expect(array_key_exists('refresh_token', $array))->toBeFalse(); }); it('handles null scope in array conversion', function () { $accessToken = AccessToken::create('access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600))); $token = new OAuthToken($accessToken, null, TokenType::BEARER, null); $array = $token->toArray(); expect($array)->toHaveKey('access_token'); expect(array_key_exists('scope', $array))->toBeFalse(); }); it('creates authorization header with Bearer type', function () { $accessToken = AccessToken::create('access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600))); $token = new OAuthToken($accessToken, null, TokenType::BEARER, null); $header = $token->getAuthorizationHeader(); expect($header)->toBe('Bearer access_1234567890'); }); it('creates authorization header with MAC type', function () { $accessToken = AccessToken::create('access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600))); $token = new OAuthToken($accessToken, null, TokenType::MAC, null); $header = $token->getAuthorizationHeader(); expect($header)->toBe('MAC access_1234567890'); }); it('creates from array correctly', function () { $expiresAt = Timestamp::now()->add(Duration::fromSeconds(3600)); $array = [ 'access_token' => 'access_1234567890', 'refresh_token' => 'refresh_1234567890', 'token_type' => 'Bearer', 'expires_at' => $expiresAt->format('Y-m-d H:i:s'), 'scope' => 'read write', ]; $token = OAuthToken::fromArray($array); expect($token->accessToken->toString())->toBe('access_1234567890'); expect($token->refreshToken->toString())->toBe('refresh_1234567890'); expect($token->tokenType)->toBe(TokenType::BEARER); expect($token->scope->toArray())->toBe(['read', 'write']); }); });