previousDb = $GLOBALS['db'] ?? null; unset($GLOBALS['db']); }); afterEach(function (): void { if ($this->previousDb !== null) { $GLOBALS['db'] = $this->previousDb; return; } unset($GLOBALS['db']); }); it('generates a key_id with the default prefix and 22-char base62 random', function (): void { $keyId = api_key_generator::generateKeyId(); expect($keyId) ->toStartWith('truck_live_') ->and(strlen($keyId))->toBe(strlen('truck_live_') + 22) ->and(api_key_generator::isBase62(substr($keyId, strlen('truck_live_'))))->toBeTrue(); }); it('honours a custom env tag in the key_id', function (): void { $keyId = api_key_generator::generateKeyId('test'); expect($keyId)->toStartWith('truck_test_'); }); it('falls back to "live" for empty or invalid env tags', function (): void { expect(api_key_generator::generateKeyId(''))->toStartWith('truck_live_'); expect(api_key_generator::generateKeyId(' '))->toStartWith('truck_live_'); expect(api_key_generator::generateKeyId('weird!@# chars'))->toStartWith('truck_live_'); }); it('generates a 32-char base62 secret with no dots', function (): void { $secret = api_key_generator::generateSecret(); expect($secret) ->toHaveLength(32) ->and($secret)->not->toContain('.') ->and(api_key_generator::isBase62($secret))->toBeTrue(); }); it('generates unique values across many calls', function (): void { $seen = []; for ($i = 0; $i < 200; $i++) { $seen[] = api_key_generator::generateKeyId() . '.' . api_key_generator::generateSecret(); } expect(count(array_unique($seen)))->toBe(200); }); it('formats a key as "key_id.secret"', function (): void { $full = api_key_generator::formatKey('truck_live_abc', 'xyz'); expect($full)->toBe('truck_live_abc.xyz'); }); it('rejects formatted keys where either part contains a dot', function (): void { expect(fn () => api_key_generator::formatKey('bad.dot', 'secret')) ->toThrow(InvalidArgumentException::class); expect(fn () => api_key_generator::formatKey('key_id', 'bad.dot')) ->toThrow(InvalidArgumentException::class); }); it('hashes with argon2id and verifies the same plaintext', function (): void { $plain = 'truck_live_abc' . '.' . 'thisIsTheSecretPart'; $hash = api_key_generator::hash($plain); expect($hash) ->toBeString() ->not->toBe($plain) ->toStartWith('$argon2id$'); expect(api_key_generator::verify($plain, $hash))->toBeTrue(); }); it('produces different hashes for the same plaintext (salt randomness)', function (): void { $plain = 'truck_live_abc' . '.' . 'thisIsTheSecretPart'; $h1 = api_key_generator::hash($plain); $h2 = api_key_generator::hash($plain); expect($h1)->not->toBe($h2); expect(api_key_generator::verify($plain, $h1))->toBeTrue(); expect(api_key_generator::verify($plain, $h2))->toBeTrue(); }); it('rejects an empty hash input', function (): void { expect(fn () => api_key_generator::hash('')) ->toThrow(InvalidArgumentException::class); }); it('verify returns false for empty inputs', function (): void { expect(api_key_generator::verify('', '$argon2id$something'))->toBeFalse(); expect(api_key_generator::verify('plain', ''))->toBeFalse(); }); it('parses a well-formed full key', function (): void { $full = 'truck_live_abcDEF1234567890xyz' . '.' . 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6'; $parsed = api_key_generator::parseKey($full); expect($parsed) ->toBe(['key_id' => 'truck_live_abcDEF1234567890xyz', 'secret' => 'A1B2C3D4E5F6G7H8I9J0K1L2M3N4O5P6']); }); it('returns null when parsing a malformed key', function (): void { expect(api_key_generator::parseKey(''))->toBeNull(); expect(api_key_generator::parseKey(' '))->toBeNull(); expect(api_key_generator::parseKey('no-dot-here'))->toBeNull(); expect(api_key_generator::parseKey('.only-secret'))->toBeNull(); expect(api_key_generator::parseKey('only-key.'))->toBeNull(); expect(api_key_generator::parseKey('has spaces.in-secret'))->toBeNull(); expect(api_key_generator::parseKey('has/slash.in-secret'))->toBeNull(); }); it('round-trips generateKeyId + generateSecret through formatKey + parseKey', function (): void { $keyId = api_key_generator::generateKeyId('live'); $secret = api_key_generator::generateSecret(); $full = api_key_generator::formatKey($keyId, $secret); $parsed = api_key_generator::parseKey($full); expect($parsed)->toBe(['key_id' => $keyId, 'secret' => $secret]); }); it('isBase62 accepts alphanumerics and rejects everything else', function (): void { expect(api_key_generator::isBase62('abc123XYZ'))->toBeTrue(); expect(api_key_generator::isBase62(''))->toBeFalse(); expect(api_key_generator::isBase62('abc-123'))->toBeFalse(); expect(api_key_generator::isBase62('abc.123'))->toBeFalse(); expect(api_key_generator::isBase62('abc 123'))->toBeFalse(); expect(api_key_generator::isBase62('abc/123'))->toBeFalse(); expect(api_key_generator::isBase62('abc+123'))->toBeFalse(); });