Files
michaelschiemer/tests/Unit/Framework/Cryptography/SecureTokenGeneratorTest.php
Michael Schiemer 55a330b223 Enable Discovery debug logging for production troubleshooting
- Add DISCOVERY_LOG_LEVEL=debug
- Add DISCOVERY_SHOW_PROGRESS=true
- Temporary changes for debugging InitializerProcessor fixes on production
2025-08-11 20:13:26 +02:00

245 lines
10 KiB
PHP

<?php
declare(strict_types=1);
use App\Framework\Cryptography\SecureToken;
use App\Framework\Cryptography\SecureTokenGenerator;
use App\Framework\Random\SecureRandomGenerator;
it('generates secure token with default parameters', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
$token = $generator->generate('api_key');
expect($token)->toBeInstanceOf(SecureToken::class);
expect($token->getType())->toBe('api_key');
expect($token->getFormat())->toBe(SecureTokenGenerator::FORMAT_BASE64_URL);
expect($token->getLength())->toBe(32);
});
it('generates API key with prefix', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
$token = $generator->generateApiKey('myapp');
expect($token->getType())->toBe(SecureTokenGenerator::TYPE_API_KEY);
expect($token->getPrefix())->toBe('myapp');
expect($token->getValue())->toStartWith('myapp_');
});
it('generates session token', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
$token = $generator->generateSessionToken();
expect($token->getType())->toBe(SecureTokenGenerator::TYPE_SESSION);
expect($token->getMetadataValue('purpose'))->toBe('session_management');
expect($token->getMetadataValue('secure'))->toBeTrue();
});
it('generates CSRF token', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
$token = $generator->generateCsrfToken();
expect($token->getType())->toBe(SecureTokenGenerator::TYPE_CSRF);
expect($token->getMetadataValue('purpose'))->toBe('csrf_protection');
});
it('generates verification token', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
$token = $generator->generateVerificationToken('email_verification');
expect($token->getType())->toBe(SecureTokenGenerator::TYPE_VERIFICATION);
expect($token->getMetadataValue('purpose'))->toBe('email_verification');
expect($token->getMetadataValue('single_use'))->toBeTrue();
});
it('generates OTP token with correct digits', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
$token = $generator->generateOtpToken(6);
expect($token->getType())->toBe(SecureTokenGenerator::TYPE_OTP);
expect($token->getValue())->toMatch('/^\d{6}$/');
expect($token->getLength())->toBe(6);
expect($token->getMetadataValue('digits'))->toBe(6);
});
it('generates webhook token', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
$token = $generator->generateWebhookToken();
expect($token->getType())->toBe(SecureTokenGenerator::TYPE_WEBHOOK);
expect($token->getPrefix())->toBe('whsec');
expect($token->getValue())->toStartWith('whsec_');
});
it('generates bearer token', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
$token = $generator->generateBearerToken();
expect($token->getType())->toBe(SecureTokenGenerator::TYPE_BEARER);
expect($token->getMetadataValue('purpose'))->toBe('api_authorization');
});
it('generates refresh token with longer length', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
$token = $generator->generateRefreshToken();
expect($token->getType())->toBe(SecureTokenGenerator::TYPE_REFRESH);
expect($token->getLength())->toBe(64);
expect($token->getMetadataValue('long_lived'))->toBeTrue();
});
it('generates tokens in different formats', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
$base64Token = $generator->generate(SecureTokenGenerator::TYPE_API_KEY, 32, SecureTokenGenerator::FORMAT_BASE64);
$hexToken = $generator->generate(SecureTokenGenerator::TYPE_API_KEY, 32, SecureTokenGenerator::FORMAT_HEX);
$base32Token = $generator->generate(SecureTokenGenerator::TYPE_API_KEY, 32, SecureTokenGenerator::FORMAT_BASE32);
expect($base64Token->getFormat())->toBe(SecureTokenGenerator::FORMAT_BASE64);
expect($hexToken->getFormat())->toBe(SecureTokenGenerator::FORMAT_HEX);
expect($base32Token->getFormat())->toBe(SecureTokenGenerator::FORMAT_BASE32);
// Check format patterns
expect($hexToken->getValue())->toMatch('/^[0-9a-f]+$/');
expect($base32Token->getValue())->toMatch('/^[A-Z2-7]+$/');
});
it('generates custom tokens with alphabet', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
$alphabet = '0123456789ABCDEF';
$token = $generator->generateCustom($alphabet, 16);
expect($token->getValue())->toMatch('/^[0-9A-F]{16}$/');
expect($token->getMetadataValue('alphabet'))->toBe($alphabet);
expect($token->getMetadataValue('alphabet_size'))->toBe(16);
});
it('generates batch of tokens', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
$tokens = $generator->generateBatch('session', 5, 32);
expect($tokens)->toHaveCount(5);
expect($tokens[0])->toBeInstanceOf(SecureToken::class);
// All tokens should be unique
$values = array_map(fn ($token) => $token->getValue(), $tokens);
$uniqueValues = array_unique($values);
expect($uniqueValues)->toHaveCount(5);
});
it('validates token formats correctly', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
expect($generator->isValidFormat('dGVzdA', SecureTokenGenerator::FORMAT_BASE64))->toBeTrue();
expect($generator->isValidFormat('dGVzdA-_', SecureTokenGenerator::FORMAT_BASE64_URL))->toBeTrue();
expect($generator->isValidFormat('deadbeef', SecureTokenGenerator::FORMAT_HEX))->toBeTrue();
expect($generator->isValidFormat('MFRGG', SecureTokenGenerator::FORMAT_BASE32))->toBeTrue();
expect($generator->isValidFormat('Test123', SecureTokenGenerator::FORMAT_ALPHANUMERIC))->toBeTrue();
// Invalid formats
expect($generator->isValidFormat('invalid!@#', SecureTokenGenerator::FORMAT_BASE64_URL))->toBeFalse();
expect($generator->isValidFormat('invalidhex', SecureTokenGenerator::FORMAT_HEX))->toBeFalse();
});
it('calculates token entropy correctly', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
$base64Token = $generator->generate(SecureTokenGenerator::TYPE_API_KEY, 32, SecureTokenGenerator::FORMAT_BASE64_URL);
$hexToken = $generator->generate(SecureTokenGenerator::TYPE_API_KEY, 32, SecureTokenGenerator::FORMAT_HEX);
$base64Entropy = $generator->getEntropy($base64Token);
$hexEntropy = $generator->getEntropy($hexToken);
expect($base64Entropy)->toBeGreaterThan($hexEntropy); // Base64 has higher entropy per character
expect($base64Entropy)->toBeGreaterThan(100); // Should have significant entropy
});
it('throws exception for too short token length', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
expect(fn () => $generator->generate('test', 8))
->toThrow(InvalidArgumentException::class, 'Token length must be at least 16 bytes');
});
it('throws exception for too long token length', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
expect(fn () => $generator->generate('test', 512))
->toThrow(InvalidArgumentException::class, 'Token length cannot exceed 256 bytes');
});
it('throws exception for unsupported token type', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
expect(fn () => $generator->generate('unsupported_type'))
->toThrow(InvalidArgumentException::class, 'Unsupported token type');
});
it('throws exception for unsupported format', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
expect(fn () => $generator->generate('api_key', 32, 'unsupported_format'))
->toThrow(InvalidArgumentException::class, 'Unsupported format');
});
it('throws exception for invalid OTP digits', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
expect(fn () => $generator->generateOtpToken(2))
->toThrow(InvalidArgumentException::class, 'OTP digits must be between 4 and 12');
expect(fn () => $generator->generateOtpToken(15))
->toThrow(InvalidArgumentException::class, 'OTP digits must be between 4 and 12');
});
it('throws exception for too small custom alphabet', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
expect(fn () => $generator->generateCustom('ABC', 16))
->toThrow(InvalidArgumentException::class, 'Alphabet must contain at least 16 characters');
});
it('throws exception for too short custom token', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
expect(fn () => $generator->generateCustom('0123456789ABCDEF', 4))
->toThrow(InvalidArgumentException::class, 'Token length must be at least 8 characters');
});
it('throws exception for invalid batch count', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
expect(fn () => $generator->generateBatch('session', 0))
->toThrow(InvalidArgumentException::class, 'Count must be positive');
expect(fn () => $generator->generateBatch('session', 2000))
->toThrow(InvalidArgumentException::class, 'Batch size cannot exceed 1000');
});
it('creates instance using factory method', function () {
$randomGen = new SecureRandomGenerator();
$generator = SecureTokenGenerator::create($randomGen);
expect($generator)->toBeInstanceOf(SecureTokenGenerator::class);
});
it('removes duplicate characters from custom alphabet', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
$alphabetWithDuplicates = '0123456789ABCDEFABCDEF'; // Has duplicate ABCDEF
$token = $generator->generateCustom($alphabetWithDuplicates, 8);
$metadata = $token->getMetadata();
$actualAlphabet = $metadata['alphabet'];
expect($actualAlphabet)->toBe('0123456789ABCDEF'); // Duplicates removed
expect($metadata['alphabet_size'])->toBe(16);
});
it('generates unique tokens in batch', function () {
$generator = new SecureTokenGenerator(new SecureRandomGenerator());
$tokens = $generator->generateBatch('api_key', 10, 32);
$values = array_map(fn ($token) => $token->getValue(), $tokens);
$uniqueValues = array_unique($values);
expect($uniqueValues)->toHaveCount(10); // All should be unique
});