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
This commit is contained in:
360
tests/Unit/Framework/Cryptography/SecureTokenTest.php
Normal file
360
tests/Unit/Framework/Cryptography/SecureTokenTest.php
Normal file
@@ -0,0 +1,360 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Cryptography\SecureToken;
|
||||
use App\Framework\Cryptography\SecureTokenGenerator;
|
||||
|
||||
it('creates secure token with valid parameters', function () {
|
||||
$token = new SecureToken(
|
||||
value: 'test-token-value',
|
||||
type: SecureTokenGenerator::TYPE_API_KEY,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: 'ak',
|
||||
rawBytes: 'raw-bytes-data',
|
||||
metadata: ['purpose' => 'testing']
|
||||
);
|
||||
|
||||
expect($token->getValue())->toBe('test-token-value');
|
||||
expect($token->getType())->toBe(SecureTokenGenerator::TYPE_API_KEY);
|
||||
expect($token->getFormat())->toBe(SecureTokenGenerator::FORMAT_BASE64_URL);
|
||||
expect($token->getLength())->toBe(32);
|
||||
expect($token->getPrefix())->toBe('ak');
|
||||
expect($token->getRawBytes())->toBe('raw-bytes-data');
|
||||
expect($token->getMetadata())->toBe(['purpose' => 'testing']);
|
||||
});
|
||||
|
||||
it('throws exception for empty token value', function () {
|
||||
expect(fn () => new SecureToken(
|
||||
value: '',
|
||||
type: SecureTokenGenerator::TYPE_API_KEY,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: null,
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: []
|
||||
))->toThrow(InvalidArgumentException::class, 'Token value cannot be empty');
|
||||
});
|
||||
|
||||
it('provides string representation', function () {
|
||||
$tokenValue = 'test-token-value';
|
||||
$token = new SecureToken(
|
||||
value: $tokenValue,
|
||||
type: SecureTokenGenerator::TYPE_SESSION,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: null,
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: []
|
||||
);
|
||||
|
||||
expect($token->toString())->toBe($tokenValue);
|
||||
expect((string)$token)->toBe($tokenValue);
|
||||
});
|
||||
|
||||
it('extracts value without prefix', function () {
|
||||
$token = new SecureToken(
|
||||
value: 'ak_abcd1234efgh5678',
|
||||
type: SecureTokenGenerator::TYPE_API_KEY,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: 'ak',
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: []
|
||||
);
|
||||
|
||||
expect($token->getValueWithoutPrefix())->toBe('abcd1234efgh5678');
|
||||
expect($token->hasPrefix())->toBeTrue();
|
||||
});
|
||||
|
||||
it('handles token without prefix', function () {
|
||||
$token = new SecureToken(
|
||||
value: 'abcd1234efgh5678',
|
||||
type: SecureTokenGenerator::TYPE_SESSION,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: null,
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: []
|
||||
);
|
||||
|
||||
expect($token->getValueWithoutPrefix())->toBe('abcd1234efgh5678');
|
||||
expect($token->hasPrefix())->toBeFalse();
|
||||
});
|
||||
|
||||
it('provides raw bytes in different formats', function () {
|
||||
$rawBytes = "\x01\x02\x03\x04";
|
||||
$token = new SecureToken(
|
||||
value: 'test-token',
|
||||
type: SecureTokenGenerator::TYPE_API_KEY,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: null,
|
||||
rawBytes: $rawBytes,
|
||||
metadata: []
|
||||
);
|
||||
|
||||
expect($token->getRawBytesHex())->toBe('01020304');
|
||||
expect($token->getRawBytesBase64())->toBe(base64_encode($rawBytes));
|
||||
});
|
||||
|
||||
it('checks equality correctly with timing-safe comparison', function () {
|
||||
$tokenValue = 'same-token-value';
|
||||
|
||||
$token1 = new SecureToken(
|
||||
value: $tokenValue,
|
||||
type: SecureTokenGenerator::TYPE_API_KEY,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: null,
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: []
|
||||
);
|
||||
|
||||
$token2 = new SecureToken(
|
||||
value: $tokenValue,
|
||||
type: SecureTokenGenerator::TYPE_SESSION,
|
||||
format: SecureTokenGenerator::FORMAT_HEX,
|
||||
length: 64,
|
||||
prefix: 'test',
|
||||
rawBytes: 'different-raw',
|
||||
metadata: ['different' => 'metadata']
|
||||
);
|
||||
|
||||
$token3 = new SecureToken(
|
||||
value: 'different-token-value',
|
||||
type: SecureTokenGenerator::TYPE_API_KEY,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: null,
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: []
|
||||
);
|
||||
|
||||
expect($token1->equals($token2))->toBeTrue(); // Same value
|
||||
expect($token1->equals($token3))->toBeFalse(); // Different value
|
||||
});
|
||||
|
||||
it('verifies token value with timing-safe comparison', function () {
|
||||
$tokenValue = 'secure-token-value';
|
||||
$token = new SecureToken(
|
||||
value: $tokenValue,
|
||||
type: SecureTokenGenerator::TYPE_API_KEY,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: null,
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: []
|
||||
);
|
||||
|
||||
expect($token->verify($tokenValue))->toBeTrue();
|
||||
expect($token->verify('wrong-token-value'))->toBeFalse();
|
||||
});
|
||||
|
||||
it('identifies token types correctly', function () {
|
||||
$apiKeyToken = new SecureToken(
|
||||
value: 'test-token',
|
||||
type: SecureTokenGenerator::TYPE_API_KEY,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: null,
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: []
|
||||
);
|
||||
|
||||
$sessionToken = new SecureToken(
|
||||
value: 'test-token',
|
||||
type: SecureTokenGenerator::TYPE_SESSION,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: null,
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: []
|
||||
);
|
||||
|
||||
expect($apiKeyToken->isApiKey())->toBeTrue();
|
||||
expect($apiKeyToken->isSessionToken())->toBeFalse();
|
||||
expect($sessionToken->isSessionToken())->toBeTrue();
|
||||
expect($sessionToken->isApiKey())->toBeFalse();
|
||||
});
|
||||
|
||||
it('checks token properties from metadata', function () {
|
||||
$singleUseToken = new SecureToken(
|
||||
value: 'test-token',
|
||||
type: SecureTokenGenerator::TYPE_VERIFICATION,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: null,
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: ['single_use' => true, 'long_lived' => false]
|
||||
);
|
||||
|
||||
$longLivedToken = new SecureToken(
|
||||
value: 'test-token',
|
||||
type: SecureTokenGenerator::TYPE_REFRESH,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 64,
|
||||
prefix: null,
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: ['single_use' => false, 'long_lived' => true]
|
||||
);
|
||||
|
||||
expect($singleUseToken->isSingleUse())->toBeTrue();
|
||||
expect($singleUseToken->isLongLived())->toBeFalse();
|
||||
expect($longLivedToken->isLongLived())->toBeTrue();
|
||||
expect($longLivedToken->isSingleUse())->toBeFalse();
|
||||
});
|
||||
|
||||
it('gets specific metadata values', function () {
|
||||
$token = new SecureToken(
|
||||
value: 'test-token',
|
||||
type: SecureTokenGenerator::TYPE_API_KEY,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: null,
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: ['purpose' => 'api_auth', 'expires' => 3600]
|
||||
);
|
||||
|
||||
expect($token->getMetadataValue('purpose'))->toBe('api_auth');
|
||||
expect($token->getMetadataValue('expires'))->toBe(3600);
|
||||
expect($token->getMetadataValue('nonexistent', 'default'))->toBe('default');
|
||||
});
|
||||
|
||||
it('calculates token age', function () {
|
||||
$token = new SecureToken(
|
||||
value: 'test-token',
|
||||
type: SecureTokenGenerator::TYPE_SESSION,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: null,
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: []
|
||||
);
|
||||
|
||||
sleep(1);
|
||||
$age = $token->getAgeInSeconds();
|
||||
expect($age)->toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
|
||||
it('exports to and imports from array', function () {
|
||||
$originalToken = new SecureToken(
|
||||
value: 'test-token-value',
|
||||
type: SecureTokenGenerator::TYPE_API_KEY,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: 'ak',
|
||||
rawBytes: 'raw-bytes-data',
|
||||
metadata: ['purpose' => 'testing', 'scope' => 'read']
|
||||
);
|
||||
|
||||
$array = $originalToken->toArray();
|
||||
$restoredToken = SecureToken::fromArray($array);
|
||||
|
||||
expect($restoredToken->getValue())->toBe($originalToken->getValue());
|
||||
expect($restoredToken->getType())->toBe($originalToken->getType());
|
||||
expect($restoredToken->getFormat())->toBe($originalToken->getFormat());
|
||||
expect($restoredToken->getLength())->toBe($originalToken->getLength());
|
||||
expect($restoredToken->getPrefix())->toBe($originalToken->getPrefix());
|
||||
expect($restoredToken->getRawBytes())->toBe($originalToken->getRawBytes());
|
||||
expect($restoredToken->getMetadata())->toBe($originalToken->getMetadata());
|
||||
});
|
||||
|
||||
it('provides safe summary without sensitive data', function () {
|
||||
$token = new SecureToken(
|
||||
value: 'very-secret-token-value',
|
||||
type: SecureTokenGenerator::TYPE_API_KEY,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: 'ak',
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: ['purpose' => 'api_auth', 'scope' => 'admin']
|
||||
);
|
||||
|
||||
$summary = $token->getSafeSummary();
|
||||
|
||||
expect($summary)->toHaveKey('type');
|
||||
expect($summary)->toHaveKey('format');
|
||||
expect($summary)->toHaveKey('length');
|
||||
expect($summary)->toHaveKey('prefix');
|
||||
expect($summary)->toHaveKey('has_prefix');
|
||||
expect($summary)->toHaveKey('metadata_keys');
|
||||
expect($summary)->not->toHaveKey('value'); // Should not contain sensitive value
|
||||
expect($summary)->not->toHaveKey('raw_bytes'); // Should not contain raw bytes
|
||||
|
||||
expect($summary['metadata_keys'])->toBe(['purpose', 'scope']);
|
||||
});
|
||||
|
||||
it('generates fingerprint for identification', function () {
|
||||
$token = new SecureToken(
|
||||
value: 'test-token-value',
|
||||
type: SecureTokenGenerator::TYPE_API_KEY,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: null,
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: []
|
||||
);
|
||||
|
||||
$fingerprint = $token->getFingerprint();
|
||||
$shortFingerprint = $token->getShortFingerprint();
|
||||
|
||||
expect($fingerprint)->toHaveLength(64); // SHA-256 hex
|
||||
expect($shortFingerprint)->toHaveLength(16);
|
||||
expect($fingerprint)->toStartWith($shortFingerprint);
|
||||
});
|
||||
|
||||
it('masks token value for safe logging', function () {
|
||||
$longToken = new SecureToken(
|
||||
value: 'this-is-a-very-long-token-value-for-testing',
|
||||
type: SecureTokenGenerator::TYPE_API_KEY,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: null,
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: []
|
||||
);
|
||||
|
||||
$shortToken = new SecureToken(
|
||||
value: 'short',
|
||||
type: SecureTokenGenerator::TYPE_API_KEY,
|
||||
format: SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
length: 32,
|
||||
prefix: null,
|
||||
rawBytes: 'raw-bytes',
|
||||
metadata: []
|
||||
);
|
||||
|
||||
$longMasked = $longToken->getMaskedValue();
|
||||
$shortMasked = $shortToken->getMaskedValue();
|
||||
|
||||
expect($longMasked)->toStartWith('this');
|
||||
expect($longMasked)->toEndWith('ting');
|
||||
expect($longMasked)->toContain('*');
|
||||
expect($shortMasked)->toBe('***'); // Short tokens fully masked
|
||||
});
|
||||
|
||||
it('throws exception for missing required fields in fromArray', function () {
|
||||
$incompleteData = [
|
||||
'value' => 'test-token',
|
||||
'type' => SecureTokenGenerator::TYPE_API_KEY,
|
||||
// Missing format, length, raw_bytes
|
||||
];
|
||||
|
||||
expect(fn () => SecureToken::fromArray($incompleteData))
|
||||
->toThrow(InvalidArgumentException::class, 'Missing required field');
|
||||
});
|
||||
|
||||
it('throws exception for invalid base64 raw bytes in fromArray', function () {
|
||||
$invalidData = [
|
||||
'value' => 'test-token',
|
||||
'type' => SecureTokenGenerator::TYPE_API_KEY,
|
||||
'format' => SecureTokenGenerator::FORMAT_BASE64_URL,
|
||||
'length' => 32,
|
||||
'raw_bytes' => 'invalid-base64!@#',
|
||||
];
|
||||
|
||||
expect(fn () => SecureToken::fromArray($invalidData))
|
||||
->toThrow(InvalidArgumentException::class, 'Invalid Base64 raw bytes');
|
||||
});
|
||||
Reference in New Issue
Block a user