add(Duration::fromSeconds(3600)); $token = AccessToken::create('valid_access_token_1234567890', $expiresAt); expect($token->toString())->toBe('valid_access_token_1234567890'); expect($token->getExpiresAt())->toBe($expiresAt); }); it('creates from provider response with expires_in', function () { $token = AccessToken::fromProviderResponse('provider_token_1234567890', 3600); expect($token->toString())->toBe('provider_token_1234567890'); expect($token->getSecondsUntilExpiration())->toBeGreaterThan(3500); expect($token->getSecondsUntilExpiration())->toBeLessThanOrEqual(3600); }); it('rejects empty token', function () { $expiresAt = Timestamp::now()->add(Duration::fromSeconds(3600)); expect(fn() => AccessToken::create('', $expiresAt)) ->toThrow(\InvalidArgumentException::class, 'Access token cannot be empty'); }); it('rejects too short token', function () { $expiresAt = Timestamp::now()->add(Duration::fromSeconds(3600)); expect(fn() => AccessToken::create('short', $expiresAt)) ->toThrow(\InvalidArgumentException::class, 'Access token appears invalid (too short)'); }); it('detects expired tokens', function () { $expiredAt = Timestamp::now()->subtract(Duration::fromSeconds(100)); $token = AccessToken::create('expired_token_1234567890', $expiredAt); expect($token->isExpired())->toBeTrue(); expect($token->isValid())->toBeFalse(); }); it('detects valid non-expired tokens', function () { $expiresAt = Timestamp::now()->add(Duration::fromSeconds(3600)); $token = AccessToken::create('valid_token_1234567890', $expiresAt); expect($token->isExpired())->toBeFalse(); expect($token->isValid())->toBeTrue(); }); it('uses 60 second buffer for expiry check', function () { // Token expires in 30 seconds - should be considered expired due to 60s buffer $almostExpired = Timestamp::now()->add(Duration::fromSeconds(30)); $token = AccessToken::create('almost_expired_token_1234567890', $almostExpired); expect($token->isExpired())->toBeTrue(); expect($token->isValid())->toBeFalse(); }); it('calculates seconds until expiration correctly', function () { $expiresAt = Timestamp::now()->add(Duration::fromSeconds(1800)); // 30 minutes $token = AccessToken::create('token_1234567890', $expiresAt); $secondsLeft = $token->getSecondsUntilExpiration(); expect($secondsLeft)->toBeGreaterThan(1700); expect($secondsLeft)->toBeLessThanOrEqual(1800); }); it('returns zero seconds for expired tokens', function () { $expiredAt = Timestamp::now()->subtract(Duration::fromSeconds(100)); $token = AccessToken::create('expired_token_1234567890', $expiredAt); expect($token->getSecondsUntilExpiration())->toBe(0); }); it('masks token for logging', function () { $token = AccessToken::create('1234567890abcdefghijklmnop', Timestamp::now()->add(Duration::fromSeconds(3600))); $masked = $token->getMasked(); expect($masked)->toStartWith('1234'); expect($masked)->toEndWith('mnop'); expect($masked)->toContain('*'); expect(strlen($masked))->toBe(strlen('1234567890abcdefghijklmnop')); }); it('fully masks very short tokens', function () { $token = AccessToken::create('short_token_', Timestamp::now()->add(Duration::fromSeconds(3600))); $masked = $token->getMasked(); expect($masked)->toBe('************'); expect(str_contains($masked, 's'))->toBeFalse(); expect(str_contains($masked, '_'))->toBeFalse(); }); it('converts to string as masked', function () { $token = AccessToken::create('1234567890abcdefghijklmnop', Timestamp::now()->add(Duration::fromSeconds(3600))); expect((string) $token)->toBe($token->getMasked()); }); it('creates new token with updated expiration immutably', function () { $originalExpiry = Timestamp::now()->add(Duration::fromSeconds(3600)); $originalToken = AccessToken::create('token_1234567890', $originalExpiry); $newExpiry = Timestamp::now()->add(Duration::fromSeconds(7200)); $updatedToken = $originalToken->withExpiresAt($newExpiry); // Original unchanged expect($originalToken->getExpiresAt())->toBe($originalExpiry); expect($originalToken->toString())->toBe('token_1234567890'); // New instance updated expect($updatedToken->getExpiresAt())->toBe($newExpiry); expect($updatedToken->toString())->toBe('token_1234567890'); }); });