simpleJob = new class { public function handle(): string { return 'executed'; } }; $this->complexJob = new class { public function __construct( public string $id = 'test-123', public array $data = ['key' => 'value'] ) {} public function process(): array { return $this->data; } }; }); describe('Basic Construction', function () { it('can be created with minimal parameters', function () { $payload = JobPayload::create($this->simpleJob); expect($payload->job)->toBe($this->simpleJob); expect($payload->priority->isNormal())->toBeTrue(); expect($payload->delay->toSeconds())->toBe(0); expect($payload->timeout)->toBeNull(); expect($payload->retryStrategy)->toBeNull(); expect($payload->metadata)->not->toBeNull(); }); it('accepts all configuration parameters', function () { $priority = QueuePriority::high(); $delay = Duration::fromMinutes(5); $timeout = Duration::fromSeconds(30); $retryStrategy = new ExponentialBackoffStrategy(maxAttempts: 3); $metadata = JobMetadata::create(['user_id' => 123]); $payload = JobPayload::create( $this->complexJob, $priority, $delay, $timeout, $retryStrategy, $metadata ); expect($payload->job)->toBe($this->complexJob); expect($payload->priority)->toBe($priority); expect($payload->delay)->toBe($delay); expect($payload->timeout)->toBe($timeout); expect($payload->retryStrategy)->toBe($retryStrategy); expect($payload->metadata)->toBe($metadata); }); it('is immutable - readonly properties cannot be changed', function () { $payload = JobPayload::create($this->simpleJob); // This would cause a PHP error if attempted: // $payload->job = new stdClass(); // Fatal error: Cannot modify readonly property // $payload->priority = QueuePriority::high(); // Fatal error: Cannot modify readonly property // Test that the properties are indeed readonly $reflection = new ReflectionClass($payload); foreach (['job', 'priority', 'delay', 'timeout', 'retryStrategy', 'metadata'] as $prop) { $property = $reflection->getProperty($prop); expect($property->isReadOnly())->toBeTrue("Property {$prop} should be readonly"); } }); }); describe('Factory Methods', function () { it('creates immediate jobs with high priority and no delay', function () { $payload = JobPayload::immediate($this->simpleJob); expect($payload->priority->isHigh())->toBeTrue(); expect($payload->delay->toSeconds())->toBe(0); expect($payload->isReady())->toBeTrue(); }); it('creates delayed jobs with specified delay', function () { $delay = Duration::fromMinutes(15); $payload = JobPayload::delayed($this->simpleJob, $delay); expect($payload->delay)->toBe($delay); expect($payload->isDelayed())->toBeTrue(); expect($payload->isReady())->toBeFalse(); }); it('creates critical jobs with critical priority and short timeout', function () { $payload = JobPayload::critical($this->simpleJob); expect($payload->priority->isCritical())->toBeTrue(); expect($payload->delay->toSeconds())->toBe(0); expect($payload->timeout->toSeconds())->toBe(30); expect($payload->hasTimeout())->toBeTrue(); }); it('creates background jobs with low priority and retry strategy', function () { $payload = JobPayload::background($this->simpleJob); expect($payload->priority->isLow())->toBeTrue(); expect($payload->timeout->toMinutes())->toBe(30); expect($payload->hasRetryStrategy())->toBeTrue(); expect($payload->retryStrategy->getMaxAttempts())->toBe(5); }); }); describe('Immutable Transformations', function () { beforeEach(function () { $this->originalPayload = JobPayload::create( $this->simpleJob, QueuePriority::normal(), Duration::zero() ); }); it('withPriority() creates new instance with different priority', function () { $newPriority = QueuePriority::high(); $newPayload = $this->originalPayload->withPriority($newPriority); expect($newPayload)->not->toBe($this->originalPayload); expect($newPayload->priority)->toBe($newPriority); expect($newPayload->job)->toBe($this->originalPayload->job); expect($newPayload->delay)->toBe($this->originalPayload->delay); // Original should be unchanged expect($this->originalPayload->priority->isNormal())->toBeTrue(); }); it('withDelay() creates new instance with different delay', function () { $newDelay = Duration::fromMinutes(10); $newPayload = $this->originalPayload->withDelay($newDelay); expect($newPayload)->not->toBe($this->originalPayload); expect($newPayload->delay)->toBe($newDelay); expect($newPayload->job)->toBe($this->originalPayload->job); expect($newPayload->priority)->toBe($this->originalPayload->priority); // Original should be unchanged expect($this->originalPayload->delay->toSeconds())->toBe(0); }); it('withTimeout() creates new instance with timeout', function () { $timeout = Duration::fromSeconds(45); $newPayload = $this->originalPayload->withTimeout($timeout); expect($newPayload)->not->toBe($this->originalPayload); expect($newPayload->timeout)->toBe($timeout); expect($newPayload->hasTimeout())->toBeTrue(); // Original should be unchanged expect($this->originalPayload->timeout)->toBeNull(); }); it('withRetryStrategy() creates new instance with retry strategy', function () { $retryStrategy = new FixedDelayStrategy(Duration::fromSeconds(30), 3); $newPayload = $this->originalPayload->withRetryStrategy($retryStrategy); expect($newPayload)->not->toBe($this->originalPayload); expect($newPayload->retryStrategy)->toBe($retryStrategy); expect($newPayload->hasRetryStrategy())->toBeTrue(); // Original should be unchanged expect($this->originalPayload->retryStrategy)->toBeNull(); }); it('withMetadata() creates new instance with metadata', function () { $metadata = JobMetadata::create(['source' => 'test', 'version' => '1.0']); $newPayload = $this->originalPayload->withMetadata($metadata); expect($newPayload)->not->toBe($this->originalPayload); expect($newPayload->metadata)->toBe($metadata); // Original should be unchanged expect($this->originalPayload->metadata)->not->toBe($metadata); }); it('can chain multiple transformations', function () { $finalPayload = $this->originalPayload ->withPriority(QueuePriority::critical()) ->withDelay(Duration::fromSeconds(30)) ->withTimeout(Duration::fromMinutes(5)) ->withRetryStrategy(new ExponentialBackoffStrategy(maxAttempts: 3)); expect($finalPayload->priority->isCritical())->toBeTrue(); expect($finalPayload->delay->toSeconds())->toBe(30); expect($finalPayload->timeout->toMinutes())->toBe(5); expect($finalPayload->hasRetryStrategy())->toBeTrue(); // Original should be completely unchanged expect($this->originalPayload->priority->isNormal())->toBeTrue(); expect($this->originalPayload->delay->toSeconds())->toBe(0); expect($this->originalPayload->timeout)->toBeNull(); expect($this->originalPayload->retryStrategy)->toBeNull(); }); }); describe('Status Checking Methods', function () { it('isReady() returns true for jobs with no delay', function () { $payload = JobPayload::create($this->simpleJob, delay: Duration::zero()); expect($payload->isReady())->toBeTrue(); $immediate = JobPayload::immediate($this->simpleJob); expect($immediate->isReady())->toBeTrue(); }); it('isReady() returns false for delayed jobs', function () { $payload = JobPayload::delayed($this->simpleJob, Duration::fromSeconds(30)); expect($payload->isReady())->toBeFalse(); }); it('isDelayed() returns true for jobs with delay', function () { $payload = JobPayload::delayed($this->simpleJob, Duration::fromMinutes(1)); expect($payload->isDelayed())->toBeTrue(); }); it('isDelayed() returns false for immediate jobs', function () { $payload = JobPayload::immediate($this->simpleJob); expect($payload->isDelayed())->toBeFalse(); }); it('hasRetryStrategy() reflects retry strategy presence', function () { $withoutRetry = JobPayload::create($this->simpleJob); expect($withoutRetry->hasRetryStrategy())->toBeFalse(); $withRetry = JobPayload::background($this->simpleJob); expect($withRetry->hasRetryStrategy())->toBeTrue(); }); it('hasTimeout() reflects timeout presence', function () { $withoutTimeout = JobPayload::create($this->simpleJob); expect($withoutTimeout->hasTimeout())->toBeFalse(); $withTimeout = JobPayload::critical($this->simpleJob); expect($withTimeout->hasTimeout())->toBeTrue(); }); }); describe('Time Calculations', function () { it('getAvailableTime() returns current time for immediate jobs', function () { $payload = JobPayload::immediate($this->simpleJob); $available = $payload->getAvailableTime(); $now = time(); expect($available)->toBeGreaterThanOrEqual($now - 1); expect($available)->toBeLessThanOrEqual($now + 1); }); it('getAvailableTime() returns future time for delayed jobs', function () { $delay = Duration::fromSeconds(300); // 5 minutes $payload = JobPayload::delayed($this->simpleJob, $delay); $available = $payload->getAvailableTime(); $expected = time() + 300; expect($available)->toBeGreaterThanOrEqual($expected - 1); expect($available)->toBeLessThanOrEqual($expected + 1); }); }); describe('Serialization and Array Conversion', function () { it('can serialize job objects', function () { $payload = JobPayload::create($this->complexJob); $serialized = $payload->serialize(); expect($serialized)->toBeString(); expect(strlen($serialized))->toBeGreaterThan(0); // Should be able to unserialize $unserialized = unserialize($serialized); expect($unserialized)->toBeInstanceOf(get_class($this->complexJob)); expect($unserialized->id)->toBe('test-123'); }); it('getJobClass() returns correct class name', function () { $payload = JobPayload::create($this->complexJob); $className = $payload->getJobClass(); expect($className)->toBeString(); expect($className)->toContain('class@anonymous'); }); it('toArray() provides comprehensive job information', function () { $retryStrategy = new ExponentialBackoffStrategy(maxAttempts: 5); $payload = JobPayload::create( $this->complexJob, QueuePriority::high(), Duration::fromSeconds(120), Duration::fromMinutes(10), $retryStrategy, JobMetadata::create(['source' => 'api']) ); $array = $payload->toArray(); expect($array)->toHaveKey('job_class'); expect($array)->toHaveKey('priority'); expect($array)->toHaveKey('priority_value'); expect($array)->toHaveKey('delay_seconds'); expect($array)->toHaveKey('timeout_seconds'); expect($array)->toHaveKey('has_retry_strategy'); expect($array)->toHaveKey('max_attempts'); expect($array)->toHaveKey('available_at'); expect($array)->toHaveKey('metadata'); expect($array['priority'])->toBe('high'); expect($array['priority_value'])->toBe(100); expect($array['delay_seconds'])->toBe(120); expect($array['timeout_seconds'])->toBe(600); expect($array['has_retry_strategy'])->toBeTrue(); expect($array['max_attempts'])->toBe(5); expect($array['available_at'])->toBeInt(); expect($array['metadata'])->toBeArray(); }); it('toArray() handles null values correctly', function () { $payload = JobPayload::create($this->simpleJob); $array = $payload->toArray(); expect($array['timeout_seconds'])->toBeNull(); expect($array['has_retry_strategy'])->toBeFalse(); expect($array['max_attempts'])->toBeNull(); }); }); describe('Edge Cases and Error Handling', function () { it('maintains object reference integrity', function () { $job = new stdClass(); $job->property = 'value'; $payload = JobPayload::create($job); // Same object reference should be maintained expect($payload->job)->toBe($job); expect($payload->job->property)->toBe('value'); // Modifying original object should affect payload job (reference) $job->property = 'modified'; expect($payload->job->property)->toBe('modified'); }); it('handles complex job objects with dependencies', function () { $complexJob = new class { public array $config; public \DateTime $created; public function __construct() { $this->config = ['timeout' => 30, 'retries' => 3]; $this->created = new \DateTime(); } public function getData(): array { return [ 'config' => $this->config, 'created' => $this->created->format('Y-m-d H:i:s') ]; } }; $payload = JobPayload::create($complexJob); expect($payload->job->getData())->toBeArray(); expect($payload->job->config['timeout'])->toBe(30); }); it('preserves metadata across transformations', function () { $originalMetadata = JobMetadata::create(['initial' => 'data']); $payload = JobPayload::create($this->simpleJob, metadata: $originalMetadata); // Transform without changing metadata $newPayload = $payload->withPriority(QueuePriority::critical()); expect($newPayload->metadata)->toBe($originalMetadata); }); }); }); describe('JobPayload Integration Scenarios', function () { beforeEach(function () { $this->emailJob = new class { public function __construct( public string $to = 'test@example.com', public string $subject = 'Test Email', public string $body = 'Hello World' ) {} public function send(): bool { // Simulate email sending return true; } }; $this->reportJob = new class { public function __construct( public array $criteria = ['period' => 'monthly'], public string $format = 'pdf' ) {} public function generate(): string { return "Report generated with format: {$this->format}"; } }; }); it('handles email job scenarios', function () { // Immediate notification $urgent = JobPayload::immediate($this->emailJob); expect($urgent->priority->isHigh())->toBeTrue(); expect($urgent->isReady())->toBeTrue(); // Delayed newsletter $newsletter = JobPayload::delayed($this->emailJob, Duration::fromHours(2)); expect($newsletter->isDelayed())->toBeTrue(); // Critical alert with timeout $alert = JobPayload::critical($this->emailJob); expect($alert->priority->isCritical())->toBeTrue(); expect($alert->hasTimeout())->toBeTrue(); }); it('handles report generation scenarios', function () { // Background monthly report $monthlyReport = JobPayload::background($this->reportJob); expect($monthlyReport->priority->isLow())->toBeTrue(); expect($monthlyReport->hasRetryStrategy())->toBeTrue(); // Add custom metadata for tracking $metadata = JobMetadata::create([ 'user_id' => 123, 'report_type' => 'financial', 'department' => 'accounting' ]); $customReport = $monthlyReport->withMetadata($metadata); expect($customReport->metadata->get('user_id'))->toBe(123); expect($customReport->metadata->get('report_type'))->toBe('financial'); }); it('handles job priority escalation scenarios', function () { // Start as normal priority $job = JobPayload::create($this->emailJob, QueuePriority::normal()); expect($job->priority->isNormal())->toBeTrue(); // Escalate to high priority (customer complaint) $escalated = $job->withPriority(QueuePriority::high()); expect($escalated->priority->isHigh())->toBeTrue(); // Further escalate to critical (system outage notification) $critical = $escalated->withPriority(QueuePriority::critical()); expect($critical->priority->isCritical())->toBeTrue(); // Original should remain unchanged expect($job->priority->isNormal())->toBeTrue(); }); it('demonstrates retry strategy configuration', function () { // Simple retry for transient failures $simpleRetry = new FixedDelayStrategy(Duration::fromSeconds(30), 3); $payload = JobPayload::create($this->emailJob)->withRetryStrategy($simpleRetry); expect($payload->retryStrategy->getMaxAttempts())->toBe(3); // Exponential backoff for rate limiting $exponentialRetry = new ExponentialBackoffStrategy(maxAttempts: 5); $rateLimitedPayload = $payload->withRetryStrategy($exponentialRetry); expect($rateLimitedPayload->retryStrategy->getMaxAttempts())->toBe(5); expect($rateLimitedPayload->retryStrategy)->toBeInstanceOf(ExponentialBackoffStrategy::class); }); });