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