- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
220 lines
10 KiB
PHP
220 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Core\ValueObjects\Timestamp;
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
use App\Framework\OAuth\ValueObjects\AccessToken;
|
|
use App\Framework\OAuth\ValueObjects\OAuthToken;
|
|
use App\Framework\OAuth\ValueObjects\RefreshToken;
|
|
use App\Framework\OAuth\ValueObjects\TokenScope;
|
|
use App\Framework\OAuth\ValueObjects\TokenType;
|
|
|
|
describe('OAuthToken Composite Value Object', function () {
|
|
it('creates complete OAuth token', function () {
|
|
$accessToken = AccessToken::create('access_token_1234567890', Timestamp::now()->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']);
|
|
});
|
|
});
|