toBeInstanceOf(JobId::class); expect($id2)->toBeInstanceOf(JobId::class); expect($id1->toString())->not->toBe($id2->toString()); }); it('can create from string', function () { $idString = 'job_test_123'; $jobId = JobId::fromString($idString); expect($jobId->toString())->toBe($idString); expect($jobId->getValue())->toBe($idString); }); it('can create from ULID object', function () { $ulid = Ulid::generate(); $jobId = JobId::fromUlid($ulid); expect($jobId->toString())->toBe($ulid->toString()); expect($jobId->toUlid()->toString())->toBe($ulid->toString()); }); it('rejects empty JobId', function () { expect(fn () => JobId::fromString('')) ->toThrow(\InvalidArgumentException::class, 'JobId cannot be empty'); }); it('validates JobId format correctly', function () { // Valid formats should work expect(fn () => JobId::fromString('job_12345'))->not->toThrow(); expect(fn () => JobId::fromString('01FXYZ0123456789ABCDEF1234'))->not->toThrow(); // ULID format expect(fn () => JobId::fromString('simple-id'))->not->toThrow(); // Any non-empty string is currently accepted expect(fn () => JobId::fromString('a'))->not->toThrow(); expect(fn () => JobId::fromString('very-long-job-identifier-12345'))->not->toThrow(); }); it('is readonly and immutable', function () { $jobId = JobId::fromString('test-job-123'); // Verify the class is readonly $reflection = new ReflectionClass($jobId); expect($reflection->isReadOnly())->toBeTrue(); // The value property should be readonly $valueProperty = $reflection->getProperty('value'); expect($valueProperty->isReadOnly())->toBeTrue(); }); }); describe('String Representation', function () { it('toString() returns the internal value', function () { $value = 'test-job-456'; $jobId = JobId::fromString($value); expect($jobId->toString())->toBe($value); }); it('getValue() is alias for toString()', function () { $value = 'another-test-job'; $jobId = JobId::fromString($value); expect($jobId->getValue())->toBe($jobId->toString()); expect($jobId->getValue())->toBe($value); }); it('__toString() magic method works', function () { $value = 'magic-method-test'; $jobId = JobId::fromString($value); expect((string) $jobId)->toBe($value); expect("Job ID: {$jobId}")->toBe("Job ID: {$value}"); }); it('jsonSerialize() returns string value', function () { $value = 'json-test-job'; $jobId = JobId::fromString($value); expect($jobId->jsonSerialize())->toBe($value); expect(json_encode($jobId))->toBe('"' . $value . '"'); }); }); describe('Equality and Comparison', function () { it('equals() compares JobIds correctly', function () { $id1 = JobId::fromString('same-id'); $id2 = JobId::fromString('same-id'); $id3 = JobId::fromString('different-id'); expect($id1->equals($id2))->toBeTrue(); expect($id1->equals($id3))->toBeFalse(); expect($id2->equals($id3))->toBeFalse(); }); it('isBefore() and isAfter() compare string values', function () { $idA = JobId::fromString('aaa'); $idB = JobId::fromString('bbb'); $idC = JobId::fromString('ccc'); expect($idA->isBefore($idB))->toBeTrue(); expect($idB->isBefore($idC))->toBeTrue(); expect($idA->isBefore($idC))->toBeTrue(); expect($idC->isAfter($idB))->toBeTrue(); expect($idB->isAfter($idA))->toBeTrue(); expect($idC->isAfter($idA))->toBeTrue(); expect($idB->isBefore($idA))->toBeFalse(); expect($idA->isAfter($idB))->toBeFalse(); }); it('comparison works with numeric-like strings', function () { $id1 = JobId::fromString('job_001'); $id2 = JobId::fromString('job_002'); $id10 = JobId::fromString('job_010'); expect($id1->isBefore($id2))->toBeTrue(); expect($id2->isBefore($id10))->toBeTrue(); expect($id10->isAfter($id1))->toBeTrue(); }); }); describe('ULID Integration', function () { it('can convert to ULID when valid format', function () { $originalUlid = Ulid::generate(); $jobId = JobId::fromUlid($originalUlid); $convertedUlid = $jobId->toUlid(); expect($convertedUlid->toString())->toBe($originalUlid->toString()); }); it('getTimestamp() extracts timestamp from ULID', function () { $ulid = Ulid::generate(); $jobId = JobId::fromUlid($ulid); $timestamp = $jobId->getTimestamp(); expect($timestamp)->toBeInstanceOf(\DateTimeImmutable::class); // Should be very recent $now = new \DateTimeImmutable(); $diff = $now->getTimestamp() - $timestamp->getTimestamp(); expect($diff)->toBeLessThan(5); // Within 5 seconds }); it('generateForQueue() creates ULID-based JobId', function () { $jobId = JobId::generateForQueue('email-queue'); expect($jobId)->toBeInstanceOf(JobId::class); expect(strlen($jobId->toString()))->toBe(26); // ULID length }); it('getTimePrefix() extracts time portion', function () { $jobId = JobId::generateForQueue('test-queue'); $timePrefix = $jobId->getTimePrefix(); expect($timePrefix)->toBeString(); expect(strlen($timePrefix))->toBe(10); }); it('getRandomSuffix() extracts random portion', function () { $jobId = JobId::generateForQueue('test-queue'); $randomSuffix = $jobId->getRandomSuffix(); expect($randomSuffix)->toBeString(); expect(strlen($randomSuffix))->toBe(16); }); }); describe('Edge Cases and Error Handling', function () { it('handles very long job IDs', function () { $longId = str_repeat('a', 1000); $jobId = JobId::fromString($longId); expect($jobId->toString())->toBe($longId); expect(strlen($jobId->toString()))->toBe(1000); }); it('handles special characters in job IDs', function () { $specialId = 'job-with_special.chars@123!'; $jobId = JobId::fromString($specialId); expect($jobId->toString())->toBe($specialId); }); it('handles unicode characters', function () { $unicodeId = 'job-测试-🚀-123'; $jobId = JobId::fromString($unicodeId); expect($jobId->toString())->toBe($unicodeId); }); it('toUlid() may fail for non-ULID format strings', function () { $nonUlidJobId = JobId::fromString('not-a-ulid-format'); // This should throw an exception since it's not a valid ULID expect(fn () => $nonUlidJobId->toUlid()) ->toThrow(); }); it('getTimestamp() may fail for non-ULID format', function () { $nonUlidJobId = JobId::fromString('simple-job-id'); // This should throw an exception since it's not a valid ULID expect(fn () => $nonUlidJobId->getTimestamp()) ->toThrow(); }); }); describe('Performance and Uniqueness', function () { it('generates unique IDs in rapid succession', function () { $ids = []; $count = 1000; $start = microtime(true); for ($i = 0; $i < $count; $i++) { $id = JobId::generate(); $ids[] = $id->toString(); } $elapsed = microtime(true) - $start; // All IDs should be unique $unique = array_unique($ids); expect(count($unique))->toBe($count); // Should generate quickly expect($elapsed)->toBeLessThan(1.0); // Within 1 second }); it('ULID-based IDs are time-ordered', function () { $ids = []; // Generate several IDs with small delays for ($i = 0; $i < 5; $i++) { $ids[] = JobId::generateForQueue('test'); if ($i < 4) { usleep(1000); // 1ms delay } } // Each ID should be "after" the previous one (time-ordered) for ($i = 1; $i < count($ids); $i++) { expect($ids[$i]->isAfter($ids[$i - 1]))->toBeTrue(); } }); it('maintains consistent string representation', function () { $jobId = JobId::fromString('consistent-test'); // Multiple calls should return the same result expect($jobId->toString())->toBe($jobId->toString()); expect($jobId->getValue())->toBe($jobId->toString()); expect((string) $jobId)->toBe($jobId->toString()); expect($jobId->jsonSerialize())->toBe($jobId->toString()); }); }); }); describe('JobId in Queue Context', function () { it('can be used as array keys', function () { $id1 = JobId::fromString('job-1'); $id2 = JobId::fromString('job-2'); $jobs = []; $jobs[$id1->toString()] = 'First Job'; $jobs[$id2->toString()] = 'Second Job'; expect($jobs[$id1->toString()])->toBe('First Job'); expect($jobs[$id2->toString()])->toBe('Second Job'); expect(count($jobs))->toBe(2); }); it('works with job tracking scenarios', function () { $processingJobs = []; $completedJobs = []; // Simulate job lifecycle $jobId = JobId::generate(); // Job starts processing $processingJobs[$jobId->toString()] = time(); expect(isset($processingJobs[$jobId->toString()]))->toBeTrue(); // Job completes $completedJobs[$jobId->toString()] = [ 'started_at' => $processingJobs[$jobId->toString()], 'completed_at' => time(), 'status' => 'success', ]; unset($processingJobs[$jobId->toString()]); expect(isset($processingJobs[$jobId->toString()]))->toBeFalse(); expect(isset($completedJobs[$jobId->toString()]))->toBeTrue(); expect($completedJobs[$jobId->toString()]['status'])->toBe('success'); }); it('demonstrates time-based job identification', function () { // Generate jobs for different queues $emailJobId = JobId::generateForQueue('email'); $reportJobId = JobId::generateForQueue('reports'); $backgroundJobId = JobId::generateForQueue('background'); // All should be unique expect($emailJobId->toString())->not->toBe($reportJobId->toString()); expect($reportJobId->toString())->not->toBe($backgroundJobId->toString()); expect($backgroundJobId->toString())->not->toBe($emailJobId->toString()); // All should have ULID format (26 characters) expect(strlen($emailJobId->toString()))->toBe(26); expect(strlen($reportJobId->toString()))->toBe(26); expect(strlen($backgroundJobId->toString()))->toBe(26); }); it('supports job priority scenarios', function () { // Generate jobs with time component $urgentJob = JobId::generateForQueue('urgent'); sleep(1); // Ensure different timestamp $normalJob = JobId::generateForQueue('normal'); // Later job should have later timestamp expect($normalJob->isAfter($urgentJob))->toBeTrue(); // Can use timestamps for ordering $urgentTime = $urgentJob->getTimestamp(); $normalTime = $normalJob->getTimestamp(); expect($normalTime > $urgentTime)->toBeTrue(); }); });