- 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.
188 lines
8.7 KiB
PHP
188 lines
8.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Core\ValueObjects\Timestamp;
|
|
use App\Framework\Core\ValueObjects\Duration;
|
|
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;
|
|
|
|
describe('StoredOAuthToken Entity', function () {
|
|
it('creates new stored token', function () {
|
|
$accessToken = AccessToken::create('access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600)));
|
|
$oauthToken = new OAuthToken($accessToken, null, TokenType::BEARER, null);
|
|
|
|
$stored = StoredOAuthToken::create('user-123', 'spotify', $oauthToken);
|
|
|
|
expect($stored->id)->toBeNull(); // Not persisted yet
|
|
expect($stored->userId)->toBe('user-123');
|
|
expect($stored->provider)->toBe('spotify');
|
|
expect($stored->token)->toBe($oauthToken);
|
|
});
|
|
|
|
it('sets timestamps on creation', function () {
|
|
$accessToken = AccessToken::create('access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600)));
|
|
$oauthToken = new OAuthToken($accessToken, null, TokenType::BEARER, null);
|
|
|
|
$beforeCreate = Timestamp::now();
|
|
$stored = StoredOAuthToken::create('user-123', 'spotify', $oauthToken);
|
|
$afterCreate = Timestamp::now();
|
|
|
|
expect($stored->createdAt->toTimestamp())->toBeGreaterThanOrEqual($beforeCreate->toTimestamp());
|
|
expect($stored->createdAt->toTimestamp())->toBeLessThanOrEqual($afterCreate->toTimestamp());
|
|
expect($stored->updatedAt->toTimestamp())->toBe($stored->createdAt->toTimestamp());
|
|
});
|
|
|
|
it('creates from database row', function () {
|
|
$now = Timestamp::now();
|
|
$row = [
|
|
'id' => 1,
|
|
'user_id' => 'user-456',
|
|
'provider' => 'github',
|
|
'access_token' => 'access_1234567890',
|
|
'refresh_token' => 'refresh_1234567890',
|
|
'token_type' => 'Bearer',
|
|
'expires_at' => $now->add(Duration::fromSeconds(3600))->format('Y-m-d H:i:s'),
|
|
'scope' => 'read write',
|
|
'created_at' => $now->format('Y-m-d H:i:s'),
|
|
'updated_at' => $now->format('Y-m-d H:i:s'),
|
|
];
|
|
|
|
$stored = StoredOAuthToken::fromArray($row);
|
|
|
|
expect($stored->id)->toBe(1);
|
|
expect($stored->userId)->toBe('user-456');
|
|
expect($stored->provider)->toBe('github');
|
|
expect($stored->token->accessToken->toString())->toBe('access_1234567890');
|
|
expect($stored->token->refreshToken->toString())->toBe('refresh_1234567890');
|
|
});
|
|
|
|
it('updates token immutably', function () {
|
|
$originalAccessToken = AccessToken::create('original_access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600)));
|
|
$originalToken = new OAuthToken($originalAccessToken, null, TokenType::BEARER, null);
|
|
$originalStored = StoredOAuthToken::create('user-123', 'spotify', $originalToken);
|
|
|
|
$newAccessToken = AccessToken::create('new_access_1234567890', Timestamp::now()->add(Duration::fromSeconds(7200)));
|
|
$newToken = new OAuthToken($newAccessToken, null, TokenType::BEARER, null);
|
|
$updatedStored = $originalStored->withRefreshedToken($newToken);
|
|
|
|
// Original unchanged
|
|
expect($originalStored->token->accessToken->toString())->toBe('original_access_1234567890');
|
|
expect($originalStored->userId)->toBe('user-123');
|
|
|
|
// New instance updated
|
|
expect($updatedStored->token->accessToken->toString())->toBe('new_access_1234567890');
|
|
expect($updatedStored->userId)->toBe('user-123');
|
|
expect($updatedStored->provider)->toBe('spotify');
|
|
});
|
|
|
|
it('updates updatedAt timestamp on token refresh', function () {
|
|
$originalAccessToken = AccessToken::create('original_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600)));
|
|
$originalToken = new OAuthToken($originalAccessToken, null, TokenType::BEARER, null);
|
|
$originalStored = StoredOAuthToken::create('user-123', 'spotify', $originalToken);
|
|
|
|
// Wait a full second to ensure different timestamp
|
|
sleep(1);
|
|
|
|
$newAccessToken = AccessToken::create('new_1234567890', Timestamp::now()->add(Duration::fromSeconds(7200)));
|
|
$newToken = new OAuthToken($newAccessToken, null, TokenType::BEARER, null);
|
|
$updatedStored = $originalStored->withRefreshedToken($newToken);
|
|
|
|
expect($updatedStored->updatedAt->toTimestamp())
|
|
->toBeGreaterThanOrEqual($originalStored->updatedAt->toTimestamp() + 1);
|
|
expect($updatedStored->createdAt->toTimestamp())
|
|
->toBe($originalStored->createdAt->toTimestamp());
|
|
});
|
|
|
|
it('preserves id during token refresh', function () {
|
|
$accessToken = AccessToken::create('access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600)));
|
|
$oauthToken = new OAuthToken($accessToken, null, TokenType::BEARER, null);
|
|
$stored = new StoredOAuthToken(
|
|
id: 42,
|
|
userId: 'user-123',
|
|
provider: 'spotify',
|
|
token: $oauthToken,
|
|
createdAt: Timestamp::now(),
|
|
updatedAt: Timestamp::now()
|
|
);
|
|
|
|
$newAccessToken = AccessToken::create('new_1234567890', Timestamp::now()->add(Duration::fromSeconds(7200)));
|
|
$newToken = new OAuthToken($newAccessToken, null, TokenType::BEARER, null);
|
|
$updated = $stored->withRefreshedToken($newToken);
|
|
|
|
expect($updated->id)->toBe(42);
|
|
});
|
|
|
|
it('detects expired token', function () {
|
|
$expiredAccessToken = AccessToken::create('expired_1234567890', Timestamp::now()->subtract(Duration::fromSeconds(100)));
|
|
$expiredToken = new OAuthToken($expiredAccessToken, null, TokenType::BEARER, null);
|
|
$stored = StoredOAuthToken::create('user-123', 'spotify', $expiredToken);
|
|
|
|
expect($stored->isExpired())->toBeTrue();
|
|
});
|
|
|
|
it('detects valid non-expired token', function () {
|
|
$validAccessToken = AccessToken::create('valid_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600)));
|
|
$validToken = new OAuthToken($validAccessToken, null, TokenType::BEARER, null);
|
|
$stored = StoredOAuthToken::create('user-123', 'spotify', $validToken);
|
|
|
|
expect($stored->isExpired())->toBeFalse();
|
|
});
|
|
|
|
it('checks if token can refresh', function () {
|
|
$accessToken = AccessToken::create('access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600)));
|
|
$refreshToken = RefreshToken::create('refresh_1234567890');
|
|
$oauthToken = new OAuthToken($accessToken, $refreshToken, TokenType::BEARER, null);
|
|
$stored = StoredOAuthToken::create('user-123', 'spotify', $oauthToken);
|
|
|
|
expect($stored->canRefresh())->toBeTrue();
|
|
});
|
|
|
|
it('checks if token cannot refresh without refresh token', function () {
|
|
$accessToken = AccessToken::create('access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600)));
|
|
$oauthToken = new OAuthToken($accessToken, null, TokenType::BEARER, null);
|
|
$stored = StoredOAuthToken::create('user-123', 'spotify', $oauthToken);
|
|
|
|
expect($stored->canRefresh())->toBeFalse();
|
|
});
|
|
|
|
it('converts to array for database storage', function () {
|
|
$accessToken = AccessToken::create('access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600)));
|
|
$refreshToken = RefreshToken::create('refresh_1234567890');
|
|
$oauthToken = new OAuthToken($accessToken, $refreshToken, TokenType::BEARER, null);
|
|
$stored = new StoredOAuthToken(
|
|
id: 1,
|
|
userId: 'user-123',
|
|
provider: 'spotify',
|
|
token: $oauthToken,
|
|
createdAt: Timestamp::now(),
|
|
updatedAt: Timestamp::now()
|
|
);
|
|
|
|
$array = $stored->toArray();
|
|
|
|
expect($array['id'])->toBe(1);
|
|
expect($array['user_id'])->toBe('user-123');
|
|
expect($array['provider'])->toBe('spotify');
|
|
expect($array['access_token'])->toBe('access_1234567890');
|
|
expect($array['refresh_token'])->toBe('refresh_1234567890');
|
|
expect($array['token_type'])->toBe('Bearer');
|
|
expect($array)->toHaveKey('expires_at');
|
|
expect($array)->toHaveKey('created_at');
|
|
expect($array)->toHaveKey('updated_at');
|
|
});
|
|
|
|
it('handles null id in array conversion', function () {
|
|
$accessToken = AccessToken::create('access_1234567890', Timestamp::now()->add(Duration::fromSeconds(3600)));
|
|
$oauthToken = new OAuthToken($accessToken, null, TokenType::BEARER, null);
|
|
$stored = StoredOAuthToken::create('user-123', 'spotify', $oauthToken);
|
|
|
|
$array = $stored->toArray();
|
|
|
|
expect(array_key_exists('id', $array))->toBeFalse();
|
|
});
|
|
});
|