toString())->toBe($value); expect($ksuid->getValue())->toBe($value); }); it('creates KSUID from bytes', function () { $bytes = str_repeat("\x00", Ksuid::TOTAL_BYTES); $ksuid = Ksuid::fromBytes($bytes); expect($ksuid->getBytes())->toBe($bytes); expect(strlen($ksuid->toString()))->toBe(Ksuid::ENCODED_LENGTH); }); it('creates KSUID from timestamp and payload', function () { $timestamp = time(); $payload = random_bytes(Ksuid::PAYLOAD_BYTES); $ksuid = Ksuid::fromTimestampAndPayload($timestamp, $payload); expect($ksuid->getTimestamp())->toBe($timestamp); expect($ksuid->getPayload())->toBe($payload); }); it('validates KSUID length', function () { expect(fn () => Ksuid::fromString('toolong' . str_repeat('a', 30))) ->toThrow(InvalidArgumentException::class, 'KSUID must be exactly 27 characters long'); expect(fn () => Ksuid::fromString('tooshort')) ->toThrow(InvalidArgumentException::class, 'KSUID must be exactly 27 characters long'); }); it('validates KSUID characters', function () { expect(fn () => Ksuid::fromString(str_repeat('!', Ksuid::ENCODED_LENGTH))) ->toThrow(InvalidArgumentException::class, 'KSUID contains invalid characters'); }); it('validates payload size', function () { $timestamp = time(); $shortPayload = random_bytes(Ksuid::PAYLOAD_BYTES - 1); expect(fn () => Ksuid::fromTimestampAndPayload($timestamp, $shortPayload)) ->toThrow(InvalidArgumentException::class, 'Payload must be exactly 16 bytes'); }); it('validates timestamp before epoch', function () { $earlyTimestamp = Ksuid::EPOCH - 1; $payload = random_bytes(Ksuid::PAYLOAD_BYTES); expect(fn () => Ksuid::fromTimestampAndPayload($earlyTimestamp, $payload)) ->toThrow(InvalidArgumentException::class, 'Timestamp cannot be before KSUID epoch'); }); it('parses timestamp correctly', function () { $expectedTimestamp = 1609459200; // 2021-01-01 00:00:00 UTC $payload = random_bytes(Ksuid::PAYLOAD_BYTES); $ksuid = Ksuid::fromTimestampAndPayload($expectedTimestamp, $payload); expect($ksuid->getTimestamp())->toBe($expectedTimestamp); }); it('gets DateTime from timestamp', function () { $timestamp = 1609459200; // 2021-01-01 00:00:00 UTC $payload = random_bytes(Ksuid::PAYLOAD_BYTES); $ksuid = Ksuid::fromTimestampAndPayload($timestamp, $payload); $dateTime = $ksuid->getDateTime(); expect($dateTime->getTimestamp())->toBe($timestamp); expect($dateTime->format('Y-m-d H:i:s'))->toBe('2021-01-01 00:00:00'); }); it('checks equality between KSUIDs', function () { $value = '2SwcbqZrBNGd67ZJYmPKx42wKZj'; $ksuid1 = Ksuid::fromString($value); $ksuid2 = Ksuid::fromString($value); $ksuid3 = Ksuid::fromString('2SwcbqZrBNGd67ZJYmPKx42wKZk'); expect($ksuid1->equals($ksuid2))->toBeTrue(); expect($ksuid1->equals($ksuid3))->toBeFalse(); }); it('compares KSUIDs for sorting', function () { $value1 = '2SwcbqZrBNGd67ZJYmPKx42wKZj'; $value2 = '2SwcbqZrBNGd67ZJYmPKx42wKZk'; // Lexicographically greater $ksuid1 = Ksuid::fromString($value1); $ksuid2 = Ksuid::fromString($value2); expect($ksuid1->compare($ksuid2))->toBeLessThan(0); expect($ksuid2->compare($ksuid1))->toBeGreaterThan(0); expect($ksuid1->compare($ksuid1))->toBe(0); }); it('checks age comparisons', function () { $olderTimestamp = time() - 3600; // 1 hour ago $newerTimestamp = time(); $payload1 = random_bytes(Ksuid::PAYLOAD_BYTES); $payload2 = random_bytes(Ksuid::PAYLOAD_BYTES); $olderKsuid = Ksuid::fromTimestampAndPayload($olderTimestamp, $payload1); $newerKsuid = Ksuid::fromTimestampAndPayload($newerTimestamp, $payload2); expect($olderKsuid->isOlderThan($newerKsuid))->toBeTrue(); expect($newerKsuid->isNewerThan($olderKsuid))->toBeTrue(); expect($olderKsuid->isNewerThan($newerKsuid))->toBeFalse(); expect($newerKsuid->isOlderThan($olderKsuid))->toBeFalse(); }); it('calculates age in seconds', function () { $timestamp = time() - 100; // 100 seconds ago $payload = random_bytes(Ksuid::PAYLOAD_BYTES); $ksuid = Ksuid::fromTimestampAndPayload($timestamp, $payload); $age = $ksuid->getAgeInSeconds(); expect($age)->toBeGreaterThanOrEqual(99); expect($age)->toBeLessThanOrEqual(101); // Allow 1 second tolerance }); it('converts to string using magic method', function () { $value = '2SwcbqZrBNGd67ZJYmPKx42wKZj'; $ksuid = Ksuid::fromString($value); expect((string)$ksuid)->toBe($value); }); it('handles Base62 encoding/decoding correctly', function () { $originalBytes = random_bytes(Ksuid::TOTAL_BYTES); $ksuid = Ksuid::fromBytes($originalBytes); $decodedBytes = $ksuid->getBytes(); expect($decodedBytes)->toBe($originalBytes); }); it('handles zero timestamp correctly', function () { $epochTimestamp = Ksuid::EPOCH; // Exactly at epoch $payload = str_repeat("\x00", Ksuid::PAYLOAD_BYTES); $ksuid = Ksuid::fromTimestampAndPayload($epochTimestamp, $payload); expect($ksuid->getTimestamp())->toBe($epochTimestamp); // Should create the minimum possible KSUID $expectedEncoded = str_repeat('0', Ksuid::ENCODED_LENGTH); expect($ksuid->toString())->toBe($expectedEncoded); }); it('handles maximum values correctly', function () { $maxTimestamp = Ksuid::EPOCH + 0xFFFFFFFF; // Max 32-bit timestamp $maxPayload = str_repeat("\xFF", Ksuid::PAYLOAD_BYTES); $ksuid = Ksuid::fromTimestampAndPayload($maxTimestamp, $maxPayload); expect($ksuid->getTimestamp())->toBe($maxTimestamp); expect($ksuid->getPayload())->toBe($maxPayload); }); it('throws exception for empty KSUID', function () { expect(fn () => Ksuid::fromString('')) ->toThrow(InvalidArgumentException::class, 'KSUID cannot be empty'); }); it('throws exception for invalid bytes length', function () { expect(fn () => Ksuid::fromBytes('tooshort')) ->toThrow(InvalidArgumentException::class, 'KSUID bytes must be exactly 20 bytes'); });