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:
241
tests/Unit/Framework/Ksuid/KsuidGeneratorTest.php
Normal file
241
tests/Unit/Framework/Ksuid/KsuidGeneratorTest.php
Normal file
@@ -0,0 +1,241 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Ksuid\Ksuid;
|
||||
use App\Framework\Ksuid\KsuidGenerator;
|
||||
use App\Framework\Random\SecureRandomGenerator;
|
||||
|
||||
it('generates KSUID with current timestamp', function () {
|
||||
$generator = new KsuidGenerator(new SecureRandomGenerator());
|
||||
$ksuid = $generator->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);
|
||||
});
|
||||
177
tests/Unit/Framework/Ksuid/KsuidTest.php
Normal file
177
tests/Unit/Framework/Ksuid/KsuidTest.php
Normal file
@@ -0,0 +1,177 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Framework\Ksuid\Ksuid;
|
||||
|
||||
it('creates KSUID from string', function () {
|
||||
$value = '2SwcbqZrBNGd67ZJYmPKx42wKZj';
|
||||
$ksuid = Ksuid::fromString($value);
|
||||
|
||||
expect($ksuid->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');
|
||||
});
|
||||
Reference in New Issue
Block a user