- Add comprehensive health check system with multiple endpoints - Add Prometheus metrics endpoint - Add production logging configurations (5 strategies) - Add complete deployment documentation suite: * QUICKSTART.md - 30-minute deployment guide * DEPLOYMENT_CHECKLIST.md - Printable verification checklist * DEPLOYMENT_WORKFLOW.md - Complete deployment lifecycle * PRODUCTION_DEPLOYMENT.md - Comprehensive technical reference * production-logging.md - Logging configuration guide * ANSIBLE_DEPLOYMENT.md - Infrastructure as Code automation * README.md - Navigation hub * DEPLOYMENT_SUMMARY.md - Executive summary - Add deployment scripts and automation - Add DEPLOYMENT_PLAN.md - Concrete plan for immediate deployment - Update README with production-ready features All production infrastructure is now complete and ready for deployment.
346 lines
12 KiB
PHP
346 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Framework\Queue\ValueObjects\JobId;
|
|
use App\Framework\Ulid\Ulid;
|
|
|
|
describe('JobId Value Object', function () {
|
|
|
|
describe('Creation and Validation', function () {
|
|
it('can generate unique JobIds', function () {
|
|
$id1 = JobId::generate();
|
|
$id2 = JobId::generate();
|
|
|
|
expect($id1)->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();
|
|
});
|
|
});
|