generate(); expect($ksuid)->toBeInstanceOf(Ksuid::class); expect($ksuid->getTimestamp())->toBeGreaterThanOrEqual(time() - 1); expect($ksuid->getTimestamp())->toBeLessThanOrEqual(time() + 1); }); it('generates KSUID at specific timestamp', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); $timestamp = time() - 3600; // 1 hour ago $ksuid = $generator->generateAt($timestamp); expect($ksuid->getTimestamp())->toBe($timestamp); }); it('generates KSUID at specific DateTime', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); $dateTime = new DateTimeImmutable('2021-01-01 12:00:00 UTC'); $ksuid = $generator->generateAtDateTime($dateTime); expect($ksuid->getTimestamp())->toBe($dateTime->getTimestamp()); }); it('generates KSUID in the past', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); $secondsAgo = 3600; // 1 hour ago $ksuid = $generator->generateInPast($secondsAgo); $expectedTimestamp = time() - $secondsAgo; expect($ksuid->getTimestamp())->toBeGreaterThanOrEqual($expectedTimestamp - 1); expect($ksuid->getTimestamp())->toBeLessThanOrEqual($expectedTimestamp + 1); }); it('generates batch of KSUIDs', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); $count = 10; $ksuids = $generator->generateBatch($count); expect($ksuids)->toHaveCount($count); expect($ksuids[0])->toBeInstanceOf(Ksuid::class); // All should have the same timestamp $firstTimestamp = $ksuids[0]->getTimestamp(); foreach ($ksuids as $ksuid) { expect($ksuid->getTimestamp())->toBe($firstTimestamp); } // All should be unique $values = array_map(fn ($ksuid) => $ksuid->toString(), $ksuids); $uniqueValues = array_unique($values); expect($uniqueValues)->toHaveCount($count); }); it('generates batch with custom timestamp', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); $timestamp = time() - 7200; // 2 hours ago $count = 5; $ksuids = $generator->generateBatch($count, $timestamp); expect($ksuids)->toHaveCount($count); foreach ($ksuids as $ksuid) { expect($ksuid->getTimestamp())->toBe($timestamp); } }); it('generates sequence of KSUIDs', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); $count = 5; $interval = 10; // 10 seconds apart $ksuids = $generator->generateSequence($count, $interval); expect($ksuids)->toHaveCount($count); // Check timestamps are incrementing for ($i = 1; $i < $count; $i++) { $timeDiff = $ksuids[$i]->getTimestamp() - $ksuids[$i - 1]->getTimestamp(); expect($timeDiff)->toBe($interval); } }); it('generates KSUID with prefix', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); $prefix = 'test'; $ksuid = $generator->generateWithPrefix($prefix); $payload = $ksuid->getPayload(); expect(substr($payload, 0, strlen($prefix)))->toBe($prefix); expect(strlen($payload))->toBe(Ksuid::PAYLOAD_BYTES); }); it('validates KSUID strings', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); $validKsuid = $generator->generate(); expect($generator->isValid($validKsuid->toString()))->toBeTrue(); expect($generator->isValid('invalid'))->toBeFalse(); expect($generator->isValid(''))->toBeFalse(); expect($generator->isValid(str_repeat('!', Ksuid::ENCODED_LENGTH)))->toBeFalse(); }); it('parses KSUID strings', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); $original = $generator->generate(); $parsed = $generator->parse($original->toString()); expect($parsed->equals($original))->toBeTrue(); }); it('gets min/max KSUIDs for timestamp', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); $timestamp = time(); $min = $generator->getMinForTimestamp($timestamp); $max = $generator->getMaxForTimestamp($timestamp); expect($min->getTimestamp())->toBe($timestamp); expect($max->getTimestamp())->toBe($timestamp); expect($min->compare($max))->toBeLessThan(0); // Min should have all zero payload expect($min->getPayload())->toBe(str_repeat("\0", Ksuid::PAYLOAD_BYTES)); // Max should have all 0xFF payload expect($max->getPayload())->toBe(str_repeat("\xFF", Ksuid::PAYLOAD_BYTES)); }); it('generates time range for queries', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); $startTime = time() - 3600; $endTime = time(); $range = $generator->generateTimeRange($startTime, $endTime); expect($range)->toHaveKey('min'); expect($range)->toHaveKey('max'); expect($range['min']->getTimestamp())->toBe($startTime); expect($range['max']->getTimestamp())->toBe($endTime); }); it('throws exception for invalid batch count', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); expect(fn () => $generator->generateBatch(0)) ->toThrow(InvalidArgumentException::class, 'Count must be positive'); expect(fn () => $generator->generateBatch(10001)) ->toThrow(InvalidArgumentException::class, 'Batch size cannot exceed 10000'); }); it('throws exception for invalid sequence count', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); expect(fn () => $generator->generateSequence(0)) ->toThrow(InvalidArgumentException::class, 'Count must be positive'); expect(fn () => $generator->generateSequence(1001)) ->toThrow(InvalidArgumentException::class, 'Sequence size cannot exceed 1000'); }); it('throws exception for negative interval', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); expect(fn () => $generator->generateSequence(5, -1)) ->toThrow(InvalidArgumentException::class, 'Interval must be non-negative'); }); it('throws exception for timestamp before epoch', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); $beforeEpoch = Ksuid::EPOCH - 1; expect(fn () => $generator->generateAt($beforeEpoch)) ->toThrow(InvalidArgumentException::class, 'Timestamp cannot be before KSUID epoch'); }); it('throws exception for prefix too long', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); $longPrefix = str_repeat('x', Ksuid::PAYLOAD_BYTES); // Full payload size expect(fn () => $generator->generateWithPrefix($longPrefix)) ->toThrow(InvalidArgumentException::class, 'Prefix cannot exceed 15 bytes'); }); it('throws exception for invalid time range', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); $startTime = time(); $endTime = time() - 3600; // Before start time expect(fn () => $generator->generateTimeRange($startTime, $endTime)) ->toThrow(InvalidArgumentException::class, 'Start timestamp must be before end timestamp'); }); it('creates generator using factory method', function () { $randomGen = new SecureRandomGenerator(); $generator = KsuidGenerator::create($randomGen); expect($generator)->toBeInstanceOf(KsuidGenerator::class); expect($generator->generate())->toBeInstanceOf(Ksuid::class); }); it('generates sortable KSUIDs by timestamp', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); $older = $generator->generateAt(time() - 3600); $newer = $generator->generateAt(time()); // Lexicographic comparison should match timestamp order expect($older->compare($newer))->toBeLessThan(0); expect($older->toString() < $newer->toString())->toBeTrue(); }); it('generates unique KSUIDs across multiple calls', function () { $generator = new KsuidGenerator(new SecureRandomGenerator()); $ksuids = []; $count = 1000; for ($i = 0; $i < $count; $i++) { $ksuids[] = $generator->generate()->toString(); } $uniqueKsuids = array_unique($ksuids); expect($uniqueKsuids)->toHaveCount($count); });