Files
michaelschiemer/tests/Unit/Framework/OAuth/OAuthServiceTest.php
Michael Schiemer fc3d7e6357 feat(Production): Complete production deployment infrastructure
- 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.
2025-10-25 19:18:37 +02:00

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);
});