- 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.
226 lines
10 KiB
PHP
226 lines
10 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Core\ValueObjects\Timestamp;
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
use App\Framework\Exception\FrameworkException;
|
|
use App\Framework\OAuth\OAuthProvider;
|
|
use App\Framework\OAuth\OAuthService;
|
|
use App\Framework\OAuth\Storage\InMemoryOAuthTokenRepository;
|
|
use App\Framework\OAuth\Storage\StoredOAuthToken;
|
|
use App\Framework\OAuth\ValueObjects\AccessToken;
|
|
use App\Framework\OAuth\ValueObjects\OAuthToken;
|
|
use App\Framework\OAuth\ValueObjects\RefreshToken;
|
|
use App\Framework\OAuth\ValueObjects\TokenType;
|
|
|
|
beforeEach(function () {
|
|
$this->repository = new InMemoryOAuthTokenRepository();
|
|
$this->provider = Mockery::mock(OAuthProvider::class);
|
|
$this->provider->allows('getName')->andReturn('test-provider');
|
|
|
|
$this->service = new OAuthService($this->repository, ['test-provider' => $this->provider]);
|
|
});
|
|
|
|
afterEach(function () {
|
|
Mockery::close();
|
|
});
|
|
|
|
describe('OAuthService - Token Retrieval', function () {
|
|
it('retrieves valid non-expired token', function () {
|
|
$validAccessToken = AccessToken::create('valid_access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600)));
|
|
$oauthToken = new OAuthToken($validAccessToken, null, TokenType::BEARER, null);
|
|
|
|
// Save token to repository
|
|
$this->repository->saveForUser('user-123', 'test-provider', $oauthToken);
|
|
|
|
$result = $this->service->getTokenForUser('user-123', 'test-provider');
|
|
|
|
expect($result->userId)->toBe('user-123');
|
|
expect($result->provider)->toBe('test-provider');
|
|
expect($result->isExpired())->toBeFalse();
|
|
});
|
|
|
|
it('automatically refreshes expired token when refresh token exists', function () {
|
|
// Expired token with refresh token
|
|
$expiredAccessToken = AccessToken::create('expired_access_1234567890', Timestamp::now()->subtract(Duration::fromSeconds(100)));
|
|
$refreshToken = RefreshToken::create('refresh_1234567890');
|
|
$expiredOAuthToken = new OAuthToken($expiredAccessToken, $refreshToken, TokenType::BEARER, null);
|
|
|
|
// Save expired token
|
|
$this->repository->saveForUser('user-123', 'test-provider', $expiredOAuthToken);
|
|
|
|
// New refreshed token from provider
|
|
$newAccessToken = AccessToken::create('new_access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600)));
|
|
$newRefreshToken = RefreshToken::create('new_refresh_1234567890');
|
|
$newOAuthToken = new OAuthToken($newAccessToken, $newRefreshToken, TokenType::BEARER, null);
|
|
|
|
$this->provider->expects('refreshToken')
|
|
->once()
|
|
->andReturn($newOAuthToken);
|
|
|
|
$result = $this->service->getTokenForUser('user-123', 'test-provider');
|
|
|
|
expect($result->token->accessToken->toString())->toBe('new_access_1234567890');
|
|
expect($result->isExpired())->toBeFalse();
|
|
});
|
|
|
|
it('throws exception for expired token without refresh token', function () {
|
|
$expiredAccessToken = AccessToken::create('expired_1234567890', Timestamp::now()->subtract(Duration::fromSeconds(100)));
|
|
$expiredOAuthToken = new OAuthToken($expiredAccessToken, null, TokenType::BEARER, null);
|
|
|
|
$this->repository->saveForUser('user-123', 'test-provider', $expiredOAuthToken);
|
|
|
|
$this->service->getTokenForUser('user-123', 'test-provider');
|
|
})->throws(FrameworkException::class);
|
|
|
|
it('throws exception when token not found', function () {
|
|
$this->service->getTokenForUser('user-123', 'test-provider');
|
|
})->throws(FrameworkException::class);
|
|
});
|
|
|
|
describe('OAuthService - Explicit Token Refresh', function () {
|
|
it('refreshes token successfully', function () {
|
|
$oldAccessToken = AccessToken::create('old_access_1234567890', Timestamp::now()->add(Duration::fromSeconds(100)));
|
|
$refreshToken = RefreshToken::create('refresh_1234567890');
|
|
$oldOAuthToken = new OAuthToken($oldAccessToken, $refreshToken, TokenType::BEARER, null);
|
|
$storedToken = $this->repository->saveForUser('user-123', 'test-provider', $oldOAuthToken);
|
|
|
|
$newAccessToken = AccessToken::create('new_access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600)));
|
|
$newRefreshToken = RefreshToken::create('new_refresh_1234567890');
|
|
$newOAuthToken = new OAuthToken($newAccessToken, $newRefreshToken, TokenType::BEARER, null);
|
|
|
|
$this->provider->expects('refreshToken')
|
|
->once()
|
|
->andReturn($newOAuthToken);
|
|
|
|
$result = $this->service->refreshToken($storedToken);
|
|
|
|
expect($result->token->accessToken->toString())->toBe('new_access_1234567890');
|
|
});
|
|
|
|
it('handles provider refresh failures', function () {
|
|
$oldAccessToken = AccessToken::create('old_access_1234567890', Timestamp::now()->add(Duration::fromSeconds(100)));
|
|
$refreshToken = RefreshToken::create('refresh_1234567890');
|
|
$oldOAuthToken = new OAuthToken($oldAccessToken, $refreshToken, TokenType::BEARER, null);
|
|
$storedToken = $this->repository->saveForUser('user-123', 'test-provider', $oldOAuthToken);
|
|
|
|
$this->provider->expects('refreshToken')
|
|
->once()
|
|
->andThrow(new \RuntimeException('Provider refresh failed'));
|
|
|
|
$this->service->refreshToken($storedToken);
|
|
})->throws(\RuntimeException::class, 'Provider refresh failed');
|
|
});
|
|
|
|
describe('OAuthService - Batch Token Refresh', function () {
|
|
it('refreshes expiring tokens in batch', function () {
|
|
// Token 1: Expiring soon (200 seconds) with refresh token
|
|
$accessToken1 = AccessToken::create('access1_1234567890', Timestamp::now()->add(Duration::fromSeconds(200)));
|
|
$refreshToken1 = RefreshToken::create('refresh1_1234567890');
|
|
$oauthToken1 = new OAuthToken($accessToken1, $refreshToken1, TokenType::BEARER, null);
|
|
$this->repository->saveForUser('user-1', 'test-provider', $oauthToken1);
|
|
|
|
// Token 2: Expiring soon (150 seconds) with refresh token
|
|
$accessToken2 = AccessToken::create('access2_1234567890', Timestamp::now()->add(Duration::fromSeconds(150)));
|
|
$refreshToken2 = RefreshToken::create('refresh2_1234567890');
|
|
$oauthToken2 = new OAuthToken($accessToken2, $refreshToken2, TokenType::BEARER, null);
|
|
$this->repository->saveForUser('user-2', 'test-provider', $oauthToken2);
|
|
|
|
// Mock refresh for both tokens
|
|
$this->provider->allows('refreshToken')
|
|
->andReturnUsing(function ($token) {
|
|
return new OAuthToken(
|
|
AccessToken::create('new_' . $token->accessToken->toString(), Timestamp::now()->add(Duration::fromSeconds(3600))),
|
|
RefreshToken::create('new_refresh_1234567890'),
|
|
TokenType::BEARER,
|
|
null
|
|
);
|
|
});
|
|
|
|
$refreshedCount = $this->service->refreshExpiringTokens(300);
|
|
|
|
expect($refreshedCount)->toBe(2);
|
|
});
|
|
|
|
it('continues batch refresh even if some tokens fail', function () {
|
|
// Token 1: Will fail to refresh
|
|
$accessToken1 = AccessToken::create('access1_1234567890', Timestamp::now()->add(Duration::fromSeconds(200)));
|
|
$refreshToken1 = RefreshToken::create('refresh1_1234567890');
|
|
$oauthToken1 = new OAuthToken($accessToken1, $refreshToken1, TokenType::BEARER, null);
|
|
$this->repository->saveForUser('user-1', 'test-provider', $oauthToken1);
|
|
|
|
// Token 2: Will succeed
|
|
$accessToken2 = AccessToken::create('access2_1234567890', Timestamp::now()->add(Duration::fromSeconds(150)));
|
|
$refreshToken2 = RefreshToken::create('refresh2_1234567890');
|
|
$oauthToken2 = new OAuthToken($accessToken2, $refreshToken2, TokenType::BEARER, null);
|
|
$this->repository->saveForUser('user-2', 'test-provider', $oauthToken2);
|
|
|
|
// Mock to fail first, succeed second
|
|
$callCount = 0;
|
|
$this->provider->allows('refreshToken')
|
|
->andReturnUsing(function () use (&$callCount) {
|
|
$callCount++;
|
|
if ($callCount === 1) {
|
|
throw new \RuntimeException('Provider error');
|
|
}
|
|
return new OAuthToken(
|
|
AccessToken::create('new_access2_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600))),
|
|
RefreshToken::create('new_refresh2_1234567890'),
|
|
TokenType::BEARER,
|
|
null
|
|
);
|
|
});
|
|
|
|
$refreshedCount = $this->service->refreshExpiringTokens(300);
|
|
|
|
expect($refreshedCount)->toBe(1); // Only 1 succeeded
|
|
});
|
|
|
|
it('returns zero when no tokens are expiring', function () {
|
|
$refreshedCount = $this->service->refreshExpiringTokens(300);
|
|
|
|
expect($refreshedCount)->toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('OAuthService - Token Cleanup', function () {
|
|
it('cleans up expired tokens without refresh capability', function () {
|
|
// Create 5 expired tokens without refresh token
|
|
for ($i = 1; $i <= 5; $i++) {
|
|
$expiredAccessToken = AccessToken::create("expired_{$i}_1234567890", Timestamp::now()->subtract(Duration::fromSeconds(100)));
|
|
$expiredToken = new OAuthToken($expiredAccessToken, null, TokenType::BEARER, null);
|
|
$this->repository->saveForUser("user-{$i}", 'test-provider', $expiredToken);
|
|
}
|
|
|
|
// Create 1 expired token WITH refresh token (should not be deleted)
|
|
$expiredWithRefresh = AccessToken::create('expired_with_refresh_1234567890', Timestamp::now()->subtract(Duration::fromSeconds(100)));
|
|
$refreshToken = RefreshToken::create('refresh_1234567890');
|
|
$tokenWithRefresh = new OAuthToken($expiredWithRefresh, $refreshToken, TokenType::BEARER, null);
|
|
$this->repository->saveForUser('user-6', 'test-provider', $tokenWithRefresh);
|
|
|
|
$deletedCount = $this->service->cleanupExpiredTokens();
|
|
|
|
expect($deletedCount)->toBe(5);
|
|
expect($this->repository->all())->toHaveCount(1); // Only token with refresh remains
|
|
});
|
|
|
|
it('returns zero when no tokens to cleanup', function () {
|
|
$deletedCount = $this->service->cleanupExpiredTokens();
|
|
|
|
expect($deletedCount)->toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('OAuthService - Provider Configuration', function () {
|
|
it('finds configured provider', function () {
|
|
$provider = $this->service->getProvider('test-provider');
|
|
|
|
expect($provider)->toBe($this->provider);
|
|
});
|
|
|
|
it('throws exception for unconfigured provider', function () {
|
|
$this->service->getProvider('unknown-provider');
|
|
})->throws(FrameworkException::class);
|
|
});
|