diff --git a/src/TestSuite/Constraint/Queue/JobCount.php b/src/TestSuite/Constraint/Queue/JobCount.php new file mode 100644 index 0000000..913c9fc --- /dev/null +++ b/src/TestSuite/Constraint/Queue/JobCount.php @@ -0,0 +1,40 @@ +getJobs(); + + foreach ($jobs as $job) { + if ($job['jobClass'] === $jobClass) { + return true; + } + } + + return false; + } + + /** + * Assertion message + * + * @return string + */ + public function toString(): string + { + if ($this->at !== null) { + return sprintf('job #%d was queued', $this->at); + } + + return 'job was queued'; + } +} diff --git a/src/TestSuite/Constraint/Queue/JobQueuedTimes.php b/src/TestSuite/Constraint/Queue/JobQueuedTimes.php new file mode 100644 index 0000000..9937308 --- /dev/null +++ b/src/TestSuite/Constraint/Queue/JobQueuedTimes.php @@ -0,0 +1,58 @@ +times = $times; + } + + /** + * Checks if job was queued the expected number of times + * + * @param mixed $other Job class name + * @return bool + */ + public function matches(mixed $other): bool + { + $jobClass = $other; + $jobs = TestQueueClient::getQueuedJobsByClass($jobClass); + $actualCount = count($jobs); + + return $actualCount === $this->times; + } + + /** + * Assertion message + * + * @return string + */ + public function toString(): string + { + return sprintf('job was queued %d times', $this->times); + } +} diff --git a/src/TestSuite/Constraint/Queue/NoJobQueued.php b/src/TestSuite/Constraint/Queue/NoJobQueued.php new file mode 100644 index 0000000..d698b31 --- /dev/null +++ b/src/TestSuite/Constraint/Queue/NoJobQueued.php @@ -0,0 +1,37 @@ +at = $at; + } + + /** + * Get the jobs or job to check + * + * @return array> + */ + protected function getJobs(): array + { + $jobs = TestQueueClient::getQueuedJobs(); + + if ($this->at !== null) { + if (!isset($jobs[$this->at])) { + return []; + } + + return [$jobs[$this->at]]; + } + + return $jobs; + } +} diff --git a/src/TestSuite/QueueTrait.php b/src/TestSuite/QueueTrait.php new file mode 100644 index 0000000..8d8686c --- /dev/null +++ b/src/TestSuite/QueueTrait.php @@ -0,0 +1,299 @@ + 'value']); + * + * $this->assertJobQueued('MyJob'); + * $this->assertJobQueuedWith('MyJob', ['data' => 'value']); + * } + * } + * ``` + */ +trait QueueTrait +{ + /** + * Setup test queue client + * + * Replaces all queue configs with test transport + * to capture jobs instead of queuing them. + * + * @return void + */ + #[Before] + public function setupTestQueueClient(): void + { + TestQueueClient::clearQueuedJobs(); + + if (!QueueManager::configured()) { + QueueManager::setConfig('default', [ + 'url' => 'null:', + ]); + } + + TestQueueClient::replaceAllClients(); + } + + /** + * Cleanup queued jobs + * + * Clears all captured jobs after each test. + * + * @return void + */ + #[After] + public function cleanupQueueTrait(): void + { + TestQueueClient::clearQueuedJobs(); + } + + /** + * Assert a job was queued + * + * @param string $jobClass Job class name + * @param string $message Optional assertion message + * @return void + */ + public function assertJobQueued(string $jobClass, string $message = ''): void + { + $this->assertThat($jobClass, new JobQueued(), $message); + } + + /** + * Assert a job was not queued + * + * @param string $jobClass Job class name + * @param string $message Optional assertion message + * @return void + */ + public function assertJobNotQueued(string $jobClass, string $message = ''): void + { + $jobs = TestQueueClient::getQueuedJobsByClass($jobClass); + $this->assertEmpty( + $jobs, + $message ?: "Job {$jobClass} was queued unexpectedly", + ); + } + + /** + * Assert no jobs were queued + * + * @param string $message Optional assertion message + * @return void + */ + public function assertNoJobsQueued(string $message = ''): void + { + $this->assertThat(null, new NoJobQueued(), $message); + } + + /** + * Assert a specific count of jobs were queued + * + * @param int $count Expected job count + * @param string $message Optional assertion message + * @return void + */ + public function assertJobCount(int $count, string $message = ''): void + { + $this->assertThat($count, new JobCount(), $message); + } + + /** + * Assert a job was queued with specific data + * + * @param string $jobClass Job class name + * @param array $data Expected data + * @param string $message Optional assertion message + * @return void + */ + public function assertJobQueuedWith( + string $jobClass, + array $data, + string $message = '', + ): void { + $jobs = TestQueueClient::getQueuedJobsByClass($jobClass); + $this->assertNotEmpty($jobs, "Job {$jobClass} was not queued"); + + $found = false; + foreach ($jobs as $job) { + if ($job['data'] === $data) { + $found = true; + break; + } + } + + $this->assertTrue( + $found, + $message ?: "Job {$jobClass} was not queued with expected data", + ); + } + + /** + * Assert a job was queued to a specific queue + * + * @param string $queue Queue name + * @param string $jobClass Job class name + * @param string $message Optional assertion message + * @return void + */ + public function assertJobQueuedToQueue( + string $queue, + string $jobClass, + string $message = '', + ): void { + $jobs = TestQueueClient::getQueuedJobsByQueue($queue); + $found = false; + foreach ($jobs as $job) { + if ($job['jobClass'] === $jobClass) { + $found = true; + break; + } + } + + $this->assertTrue( + $found, + $message ?: "Job {$jobClass} was not queued to queue {$queue}", + ); + } + + /** + * Assert a job was queued with delay + * + * @param string $jobClass Job class name + * @param int $delay Expected delay in seconds + * @param string $message Optional assertion message + * @return void + */ + public function assertJobQueuedWithDelay( + string $jobClass, + int $delay, + string $message = '', + ): void { + $jobs = TestQueueClient::getQueuedJobsByClass($jobClass); + $this->assertNotEmpty($jobs, "Job {$jobClass} was not queued"); + + $found = false; + foreach ($jobs as $job) { + if (($job['options']['delay'] ?? null) === $delay) { + $found = true; + break; + } + } + + $this->assertTrue( + $found, + $message ?: "Job {$jobClass} was not queued with delay {$delay}", + ); + } + + /** + * Assert a job was queued with priority + * + * @param string $jobClass Job class name + * @param string|int $priority Expected priority + * @param string $message Optional assertion message + * @return void + */ + public function assertJobQueuedWithPriority( + string $jobClass, + int|string $priority, + string $message = '', + ): void { + $jobs = TestQueueClient::getQueuedJobsByClass($jobClass); + $this->assertNotEmpty($jobs, "Job {$jobClass} was not queued"); + + $found = false; + foreach ($jobs as $job) { + if (($job['options']['priority'] ?? null) === $priority) { + $found = true; + break; + } + } + + $this->assertTrue( + $found, + $message ?: "Job {$jobClass} was not queued with priority {$priority}", + ); + } + + /** + * Assert a job was queued a specific number of times + * + * @param string $jobClass Job class name + * @param int $times Expected number of times + * @param string $message Optional assertion message + * @return void + */ + public function assertJobQueuedTimes(string $jobClass, int $times, string $message = ''): void + { + $this->assertThat($jobClass, new JobQueuedTimes($times), $message); + } + + /** + * Get all queued jobs + * + * @return array> + */ + public function getQueuedJobs(): array + { + return TestQueueClient::getQueuedJobs(); + } + + /** + * Get queued jobs by class + * + * @param string $jobClass Job class name + * @return array> + */ + public function getQueuedJobsByClass(string $jobClass): array + { + return TestQueueClient::getQueuedJobsByClass($jobClass); + } + + /** + * Get queued jobs by queue name + * + * @param string $queue Queue name + * @return array> + */ + public function getQueuedJobsByQueue(string $queue): array + { + return TestQueueClient::getQueuedJobsByQueue($queue); + } + + /** + * Get queued jobs by config name + * + * @param string $config Config name + * @return array> + */ + public function getQueuedJobsByConfig(string $config): array + { + return TestQueueClient::getQueuedJobsByConfig($config); + } +} diff --git a/src/TestSuite/TestQueueClient.php b/src/TestSuite/TestQueueClient.php new file mode 100644 index 0000000..aa3e554 --- /dev/null +++ b/src/TestSuite/TestQueueClient.php @@ -0,0 +1,251 @@ + 'value']); + * + * // Make assertions + * $jobs = TestQueueClient::getQueuedJobs(); + * ``` + */ +class TestQueueClient +{ + /** + * Captured queued jobs + * + * @var array> + */ + protected static array $queuedJobs = []; + + /** + * Transport registration flag + * + * @var bool + */ + protected static bool $registered = false; + + /** + * Replace all queue clients with test transport + * + * Similar to TestEmailTransport::replaceAllTransports() + * + * @return void + */ + public static function replaceAllClients(): void + { + if (!static::$registered) { + Resources::addConnection( + Transport\TestConnectionFactory::class, + ['test'], + [], + 'cakephp/queue-testsuite', + ); + ClientResources::addDriver( + Transport\TestDriver::class, + ['test'], + [], + ['cakephp/queue-testsuite'], + ); + static::$registered = true; + } + + $configured = QueueManager::configured(); + + foreach ($configured as $configName) { + $config = QueueManager::getConfig($configName); + if ($config === null) { + continue; + } + + $config['url'] = 'test:'; + QueueManager::drop($configName); + QueueManager::setConfig($configName, $config); + } + } + + /** + * Capture message from transport + * + * Called by TestProducer when a message is sent. + * + * @param \Interop\Queue\Destination $destination Destination + * @param \Interop\Queue\Message $message Message + * @param int|null $deliveryDelay Delivery delay from producer (in milliseconds) + * @param int|null $timeToLive Time to live from producer (in milliseconds) + * @param int|null $producerPriority Priority from producer + * @return void + */ + public static function captureMessage( + Destination $destination, + Message $message, + ?int $deliveryDelay = null, + ?int $timeToLive = null, + ?int $producerPriority = null, + ): void { + $body = static::extractMessageBody($message); + $classData = $body['class'] ?? null; + $jobClass = is_array($classData) ? $classData[0] : null; + $method = is_array($classData) && isset($classData[1]) ? $classData[1] : 'execute'; + $data = $body['data'] ?? ($body['args'][0] ?? []); + + $requeueOptions = $body['requeueOptions'] ?? []; + $configName = $requeueOptions['config'] ?? 'default'; + + $queueName = 'default'; + if ($destination instanceof Queue) { + $queueName = $destination->getQueueName(); + } elseif ($destination instanceof Topic) { + $queueName = $destination->getTopicName(); + } + $queueName = $requeueOptions['queue'] ?? $queueName; + + $properties = $message->getProperties(); + $delay = $properties['enqueue.delay'] ?? null; + $expires = $properties['enqueue.expire'] ?? null; + $priority = $properties['enqueue.priority'] ?? + $producerPriority ?? + $requeueOptions['priority'] ?? + null; + + if ($delay !== null) { + $delay = (int)$delay; + } elseif ($deliveryDelay !== null) { + $delay = (int)($deliveryDelay / 1000); + } + + if ($expires !== null) { + $expires = (int)$expires; + } elseif ($timeToLive !== null) { + $expires = (int)($timeToLive / 1000); + } + + static::$queuedJobs[] = [ + 'jobClass' => $jobClass, + 'method' => $method, + 'data' => $data, + 'options' => [ + 'config' => $configName, + 'queue' => $queueName, + 'delay' => $delay, + 'expires' => $expires, + 'priority' => $priority, + ], + 'timestamp' => time(), + ]; + } + + /** + * Extract message body + * + * @param \Interop\Queue\Message $message Message + * @return array + */ + protected static function extractMessageBody(Message $message): array + { + $body = $message->getBody(); + $decoded = json_decode($body, true); + if (is_array($decoded)) { + return $decoded; + } + + return []; + } + + /** + * Get all queued jobs + * + * @return array> + */ + public static function getQueuedJobs(): array + { + return static::$queuedJobs; + } + + /** + * Get queued jobs by job class + * + * @param string $jobClass Job class name + * @return array> + */ + public static function getQueuedJobsByClass(string $jobClass): array + { + $filtered = array_filter(static::$queuedJobs, function ($job) use ($jobClass) { + return $job['jobClass'] === $jobClass; + }); + + return array_values($filtered); + } + + /** + * Get queued jobs by queue name + * + * @param string $queue Queue name + * @return array> + */ + public static function getQueuedJobsByQueue(string $queue): array + { + $filtered = array_filter(static::$queuedJobs, function ($job) use ($queue) { + return ($job['options']['queue'] ?? 'default') === $queue; + }); + + return array_values($filtered); + } + + /** + * Get queued jobs by config name + * + * @param string $config Config name + * @return array> + */ + public static function getQueuedJobsByConfig(string $config): array + { + $filtered = array_filter(static::$queuedJobs, function ($job) use ($config) { + return ($job['options']['config'] ?? 'default') === $config; + }); + + return array_values($filtered); + } + + /** + * Clear all queued jobs + * + * @return void + */ + public static function clearQueuedJobs(): void + { + static::$queuedJobs = []; + } + + /** + * Get count of queued jobs + * + * @return int + */ + public static function getQueuedJobCount(): int + { + return count(static::$queuedJobs); + } +} diff --git a/src/TestSuite/Transport/TestConnectionFactory.php b/src/TestSuite/Transport/TestConnectionFactory.php new file mode 100644 index 0000000..847623f --- /dev/null +++ b/src/TestSuite/Transport/TestConnectionFactory.php @@ -0,0 +1,34 @@ +queue = $queue; + } + + /** + * Get queue + * + * @return \Interop\Queue\Queue + */ + public function getQueue(): Queue + { + return $this->queue; + } + + /** + * Receive message + * + * @param int|null $timeout Timeout in milliseconds + * @return \Interop\Queue\Message|null + */ + public function receive(?int $timeout = null): ?Message + { + return null; + } + + /** + * Receive no wait + * + * @return \Interop\Queue\Message|null + */ + public function receiveNoWait(): ?Message + { + return null; + } + + /** + * Acknowledge message + * + * @param \Interop\Queue\Message $message Message + * @return void + */ + public function acknowledge(Message $message): void + { + } + + /** + * Reject message + * + * @param \Interop\Queue\Message $message Message + * @param bool $requeue Requeue flag + * @return void + */ + public function reject(Message $message, bool $requeue = false): void + { + } +} diff --git a/src/TestSuite/Transport/TestContext.php b/src/TestSuite/Transport/TestContext.php new file mode 100644 index 0000000..d0b1781 --- /dev/null +++ b/src/TestSuite/Transport/TestContext.php @@ -0,0 +1,135 @@ + $properties Message properties + * @param array $headers Message headers + * @return \Interop\Queue\Message + */ + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message + { + return new TestMessage($body, $properties, $headers); + } + + /** + * Create queue + * + * @param string $name Queue name + * @return \Interop\Queue\Queue + */ + public function createQueue(string $name): Queue + { + return new TestDestination($name); + } + + /** + * Create topic + * + * @param string $name Topic name + * @return \Interop\Queue\Topic + */ + public function createTopic(string $name): Topic + { + return new TestDestination($name); + } + + /** + * Create producer + * + * @return \Interop\Queue\Producer + */ + public function createProducer(): Producer + { + if ($this->producer === null) { + $this->producer = new TestProducer(); + } + + return $this->producer; + } + + /** + * Create consumer + * + * @param \Interop\Queue\Destination $destination Destination + * @return \Interop\Queue\Consumer + */ + public function createConsumer(Destination $destination): Consumer + { + if ($destination instanceof Queue) { + return new TestConsumer($destination); + } + + $queueName = $destination instanceof Topic + ? $destination->getTopicName() + : 'default'; + + return new TestConsumer(new TestDestination($queueName)); + } + + /** + * Create subscription consumer + * + * @return \Interop\Queue\SubscriptionConsumer + */ + public function createSubscriptionConsumer(): SubscriptionConsumer + { + return new TestSubscriptionConsumer(); + } + + /** + * Create temporary queue + * + * @return \Interop\Queue\Queue + */ + public function createTemporaryQueue(): Queue + { + return new TestDestination('temp_' . uniqid()); + } + + /** + * Purge queue + * + * @param \Interop\Queue\Queue $queue Queue to purge + * @return void + */ + public function purgeQueue(Queue $queue): void + { + } + + /** + * Close context + * + * @return void + */ + public function close(): void + { + } +} diff --git a/src/TestSuite/Transport/TestDestination.php b/src/TestSuite/Transport/TestDestination.php new file mode 100644 index 0000000..47618de --- /dev/null +++ b/src/TestSuite/Transport/TestDestination.php @@ -0,0 +1,62 @@ +name = $name; + } + + /** + * Get queue name + * + * @return string + */ + public function getQueueName(): string + { + return $this->name; + } + + /** + * Get topic name + * + * @return string + */ + public function getTopicName(): string + { + return $this->name; + } + + /** + * Get destination name (for compatibility) + * + * @return string + */ + public function getDestinationName(): string + { + return $this->name; + } +} diff --git a/src/TestSuite/Transport/TestDriver.php b/src/TestSuite/Transport/TestDriver.php new file mode 100644 index 0000000..b70bb2c --- /dev/null +++ b/src/TestSuite/Transport/TestDriver.php @@ -0,0 +1,30 @@ + + */ + protected array $properties; + + /** + * Message headers + * + * @var array + */ + protected array $headers; + + /** + * Constructor + * + * @param string $body Message body + * @param array $properties Properties + * @param array $headers Headers + */ + public function __construct(string $body = '', array $properties = [], array $headers = []) + { + $this->body = $body; + $this->properties = $properties; + $this->headers = $headers; + } + + /** + * Get body + * + * @return string + */ + public function getBody(): string + { + return $this->body; + } + + /** + * Set body + * + * @param string $body Body + * @return void + */ + public function setBody(string $body): void + { + $this->body = $body; + } + + /** + * Set property + * + * @param string $name Property name + * @param mixed $value Property value + * @return void + */ + public function setProperty(string $name, mixed $value): void + { + $this->properties[$name] = $value; + } + + /** + * Get property + * + * @param string $name Property name + * @param mixed $default Default value + * @return mixed + */ + public function getProperty(string $name, mixed $default = null): mixed + { + return $this->properties[$name] ?? $default; + } + + /** + * Set header + * + * @param string $name Header name + * @param mixed $value Header value + * @return void + */ + public function setHeader(string $name, mixed $value): void + { + $this->headers[$name] = $value; + } + + /** + * Get header + * + * @param string $name Header name + * @param mixed $default Default value + * @return mixed + */ + public function getHeader(string $name, mixed $default = null): mixed + { + return $this->headers[$name] ?? $default; + } + + /** + * Get properties + * + * @return array + */ + public function getProperties(): array + { + return $this->properties; + } + + /** + * Set properties + * + * @param array $properties Properties + * @return void + */ + public function setProperties(array $properties): void + { + $this->properties = $properties; + } + + /** + * Get headers + * + * @return array + */ + public function getHeaders(): array + { + return $this->headers; + } + + /** + * Set headers + * + * @param array $headers Headers + * @return void + */ + public function setHeaders(array $headers): void + { + $this->headers = $headers; + } + + /** + * Get redelivered flag + * + * @return bool + */ + public function isRedelivered(): bool + { + return false; + } + + /** + * Set redelivered flag + * + * @param bool $redelivered Redelivered + * @return void + */ + public function setRedelivered(bool $redelivered): void + { + } + + /** + * Get correlation ID + * + * @return string|null + */ + public function getCorrelationId(): ?string + { + return $this->getHeader('correlation_id'); + } + + /** + * Set correlation ID + * + * @param string|null $correlationId Correlation ID + * @return void + */ + public function setCorrelationId(?string $correlationId = null): void + { + $this->setHeader('correlation_id', $correlationId); + } + + /** + * Get message ID + * + * @return string|null + */ + public function getMessageId(): ?string + { + return $this->getHeader('message_id'); + } + + /** + * Set message ID + * + * @param string|null $messageId Message ID + * @return void + */ + public function setMessageId(?string $messageId = null): void + { + $this->setHeader('message_id', $messageId); + } + + /** + * Get timestamp + * + * @return int|null + */ + public function getTimestamp(): ?int + { + return $this->getHeader('timestamp'); + } + + /** + * Set timestamp + * + * @param int|null $timestamp Timestamp + * @return void + */ + public function setTimestamp(?int $timestamp = null): void + { + $this->setHeader('timestamp', $timestamp); + } + + /** + * Get reply to + * + * @return string|null + */ + public function getReplyTo(): ?string + { + return $this->getHeader('reply_to'); + } + + /** + * Set reply to + * + * @param string|null $replyTo Reply to + * @return void + */ + public function setReplyTo(?string $replyTo = null): void + { + $this->setHeader('reply_to', $replyTo); + } +} diff --git a/src/TestSuite/Transport/TestProducer.php b/src/TestSuite/Transport/TestProducer.php new file mode 100644 index 0000000..f019089 --- /dev/null +++ b/src/TestSuite/Transport/TestProducer.php @@ -0,0 +1,125 @@ +deliveryDelay, + $this->timeToLive, + $this->priority, + ); + } + + /** + * Set delivery delay + * + * @param int|null $deliveryDelay Delay in milliseconds + * @return \Interop\Queue\Producer + */ + public function setDeliveryDelay(?int $deliveryDelay = null): Producer + { + $this->deliveryDelay = $deliveryDelay; + + return $this; + } + + /** + * Get delivery delay + * + * @return int|null + */ + public function getDeliveryDelay(): ?int + { + return $this->deliveryDelay; + } + + /** + * Set priority + * + * @param int|null $priority Priority + * @return \Interop\Queue\Producer + */ + public function setPriority(?int $priority = null): Producer + { + $this->priority = $priority; + + return $this; + } + + /** + * Get priority + * + * @return int|null + */ + public function getPriority(): ?int + { + return $this->priority; + } + + /** + * Set time to live + * + * @param int|null $timeToLive Time to live in milliseconds + * @return \Interop\Queue\Producer + */ + public function setTimeToLive(?int $timeToLive = null): Producer + { + $this->timeToLive = $timeToLive; + + return $this; + } + + /** + * Get time to live + * + * @return int|null + */ + public function getTimeToLive(): ?int + { + return $this->timeToLive; + } +} diff --git a/src/TestSuite/Transport/TestSubscriptionConsumer.php b/src/TestSuite/Transport/TestSubscriptionConsumer.php new file mode 100644 index 0000000..075d93e --- /dev/null +++ b/src/TestSuite/Transport/TestSubscriptionConsumer.php @@ -0,0 +1,55 @@ + 'null:', + ]); + } + + TestQueueClient::replaceAllClients(); + + $config = QueueManager::getConfig('default'); + $url = $config['url']; + $transport = is_array($url) ? $url['transport'] : $url; + $this->assertEquals('test:', $transport); + } + + /** + * Test assertJobQueued + * + * @return void + */ + public function testAssertJobQueued(): void + { + QueueManager::push(LogToDebugJob::class, []); + + $this->assertJobQueued(LogToDebugJob::class); + } + + /** + * Test assertJobQueued fails when job not queued + * + * @return void + */ + public function testAssertJobQueuedFailsWhenJobNotQueued(): void + { + $this->expectException(AssertionFailedError::class); + + $this->assertJobQueued(LogToDebugJob::class); + } + + /** + * Test assertJobNotQueued + * + * @return void + */ + public function testAssertJobNotQueued(): void + { + $this->assertJobNotQueued(LogToDebugJob::class); + } + + /** + * Test assertJobNotQueued fails when job is queued + * + * @return void + */ + public function testAssertJobNotQueuedFailsWhenJobQueued(): void + { + QueueManager::push(LogToDebugJob::class, []); + + $this->expectException(AssertionFailedError::class); + + $this->assertJobNotQueued(LogToDebugJob::class); + } + + /** + * Test assertNoJobsQueued + * + * @return void + */ + public function testAssertNoJobsQueued(): void + { + $this->assertNoJobsQueued(); + } + + /** + * Test assertNoJobsQueued fails when jobs queued + * + * @return void + */ + public function testAssertNoJobsQueuedFailsWhenJobsQueued(): void + { + QueueManager::push(LogToDebugJob::class, []); + + $this->expectException(AssertionFailedError::class); + + $this->assertNoJobsQueued(); + } + + /** + * Test assertJobCount + * + * @return void + */ + public function testAssertJobCount(): void + { + QueueManager::push(LogToDebugJob::class, []); + QueueManager::push(LogToDebugJob::class, []); + + $this->assertJobCount(2); + } + + /** + * Test assertJobCount fails when count mismatch + * + * @return void + */ + public function testAssertJobCountFailsWhenCountMismatch(): void + { + QueueManager::push(LogToDebugJob::class, []); + + $this->expectException(AssertionFailedError::class); + + $this->assertJobCount(2); + } + + /** + * Test assertJobQueuedWith + * + * @return void + */ + public function testAssertJobQueuedWith(): void + { + QueueManager::push(LogToDebugJob::class, ['key' => 'value', 'id' => 123]); + + $this->assertJobQueuedWith(LogToDebugJob::class, ['key' => 'value', 'id' => 123]); + } + + /** + * Test assertJobQueuedWith fails when data mismatch + * + * @return void + */ + public function testAssertJobQueuedWithFailsWhenDataMismatch(): void + { + QueueManager::push(LogToDebugJob::class, ['key' => 'value']); + + $this->expectException(AssertionFailedError::class); + + $this->assertJobQueuedWith(LogToDebugJob::class, ['key' => 'different']); + } + + /** + * Test assertJobQueuedToQueue + * + * @return void + */ + public function testAssertJobQueuedToQueue(): void + { + QueueManager::push(LogToDebugJob::class, [], ['queue' => 'high-priority']); + + $this->assertJobQueuedToQueue('high-priority', LogToDebugJob::class); + } + + /** + * Test assertJobQueuedToQueue fails when queue mismatch + * + * @return void + */ + public function testAssertJobQueuedToQueueFailsWhenQueueMismatch(): void + { + QueueManager::push(LogToDebugJob::class, [], ['queue' => 'low-priority']); + + $this->expectException(AssertionFailedError::class); + + $this->assertJobQueuedToQueue('high-priority', LogToDebugJob::class); + } + + /** + * Test assertJobQueuedWithDelay + * + * @return void + */ + public function testAssertJobQueuedWithDelay(): void + { + QueueManager::push(LogToDebugJob::class, [], ['delay' => 60]); + + $this->assertJobQueuedWithDelay(LogToDebugJob::class, 60); + } + + /** + * Test assertJobQueuedWithDelay fails when delay mismatch + * + * @return void + */ + public function testAssertJobQueuedWithDelayFailsWhenDelayMismatch(): void + { + QueueManager::push(LogToDebugJob::class, [], ['delay' => 30]); + + $this->expectException(AssertionFailedError::class); + + $this->assertJobQueuedWithDelay(LogToDebugJob::class, 60); + } + + /** + * Test assertJobQueuedWithPriority + * + * @return void + */ + public function testAssertJobQueuedWithPriority(): void + { + QueueManager::push(LogToDebugJob::class, [], ['priority' => MessagePriority::HIGH]); + + $this->assertJobQueuedWithPriority(LogToDebugJob::class, MessagePriority::HIGH); + } + + /** + * Test assertJobQueuedWithPriority fails when priority mismatch + * + * @return void + */ + public function testAssertJobQueuedWithPriorityFailsWhenPriorityMismatch(): void + { + QueueManager::push(LogToDebugJob::class, [], ['priority' => MessagePriority::LOW]); + + $this->expectException(AssertionFailedError::class); + + $this->assertJobQueuedWithPriority(LogToDebugJob::class, MessagePriority::HIGH); + } + + /** + * Test assertJobQueuedTimes + * + * @return void + */ + public function testAssertJobQueuedTimes(): void + { + QueueManager::push(LogToDebugJob::class, []); + QueueManager::push(LogToDebugJob::class, []); + QueueManager::push(LogToDebugJob::class, []); + + $this->assertJobQueuedTimes(LogToDebugJob::class, 3); + } + + /** + * Test assertJobQueuedTimes fails when count mismatch + * + * @return void + */ + public function testAssertJobQueuedTimesFailsWhenCountMismatch(): void + { + QueueManager::push(LogToDebugJob::class, []); + + $this->expectException(AssertionFailedError::class); + + $this->assertJobQueuedTimes(LogToDebugJob::class, 2); + } + + /** + * Test getQueuedJobs + * + * @return void + */ + public function testGetQueuedJobs(): void + { + QueueManager::push(LogToDebugJob::class, ['data' => 'value']); + + $jobs = $this->getQueuedJobs(); + + $this->assertCount(1, $jobs); + $this->assertEquals(LogToDebugJob::class, $jobs[0]['jobClass']); + $this->assertEquals('execute', $jobs[0]['method']); + $this->assertEquals(['data' => 'value'], $jobs[0]['data']); + $this->assertEquals('default', $jobs[0]['options']['queue']); + $this->assertEquals('default', $jobs[0]['options']['config']); + } + + /** + * Test getQueuedJobsByClass + * + * @return void + */ + public function testGetQueuedJobsByClass(): void + { + QueueManager::push(LogToDebugJob::class, ['job' => 1]); + QueueManager::push(LogToDebugJob::class, ['job' => 2]); + + $this->assertJobQueued(LogToDebugJob::class); + $this->assertJobQueuedTimes(LogToDebugJob::class, 2); + + $jobs = $this->getQueuedJobsByClass(LogToDebugJob::class); + + $this->assertCount(2, $jobs); + } + + /** + * Test getQueuedJobsByQueue + * + * @return void + */ + public function testGetQueuedJobsByQueue(): void + { + QueueManager::push(LogToDebugJob::class, [], ['queue' => 'high-priority']); + QueueManager::push(LogToDebugJob::class, [], ['queue' => 'low-priority']); + QueueManager::push(LogToDebugJob::class, [], ['queue' => 'high-priority']); + + $highPriorityJobs = $this->getQueuedJobsByQueue('high-priority'); + $lowPriorityJobs = $this->getQueuedJobsByQueue('low-priority'); + + $this->assertCount(2, $highPriorityJobs); + $this->assertCount(1, $lowPriorityJobs); + } + + /** + * Test getQueuedJobsByConfig + * + * @return void + */ + public function testGetQueuedJobsByConfig(): void + { + QueueManager::setConfig('test', ['url' => 'null:']); + TestQueueClient::replaceAllClients(); + + QueueManager::push(LogToDebugJob::class, [], ['config' => 'default']); + QueueManager::push(LogToDebugJob::class, [], ['config' => 'test']); + QueueManager::push(LogToDebugJob::class, [], ['config' => 'default']); + + $defaultJobs = $this->getQueuedJobsByConfig('default'); + $testJobs = $this->getQueuedJobsByConfig('test'); + + $this->assertCount(2, $defaultJobs); + $this->assertCount(1, $testJobs); + } + + /** + * Test clearQueuedJobs + * + * @return void + */ + public function testClearQueuedJobs(): void + { + QueueManager::push(LogToDebugJob::class, []); + + $this->assertCount(1, $this->getQueuedJobs()); + + TestQueueClient::clearQueuedJobs(); + + $this->assertCount(0, $this->getQueuedJobs()); + } + + /** + * Test job captured with delay + * + * @return void + */ + public function testJobCapturedWithDelay(): void + { + QueueManager::push(LogToDebugJob::class, [], ['delay' => 60]); + + $jobs = $this->getQueuedJobs(); + + $this->assertCount(1, $jobs); + $this->assertEquals(60, $jobs[0]['options']['delay']); + } + + /** + * Test job captured with priority + * + * @return void + */ + public function testJobCapturedWithPriority(): void + { + QueueManager::push(LogToDebugJob::class, [], ['priority' => MessagePriority::HIGH]); + + $jobs = $this->getQueuedJobs(); + + $this->assertCount(1, $jobs); + $this->assertEquals(MessagePriority::HIGH, $jobs[0]['options']['priority']); + } + + /** + * Test job captured with expires + * + * @return void + */ + public function testJobCapturedWithExpires(): void + { + QueueManager::push(LogToDebugJob::class, [], ['expires' => 3600]); + + $jobs = $this->getQueuedJobs(); + + $this->assertCount(1, $jobs); + $this->assertEquals(3600, $jobs[0]['options']['expires']); + } + + /** + * Test getQueuedJobCount + * + * @return void + */ + public function testGetQueuedJobCount(): void + { + $this->assertEquals(0, TestQueueClient::getQueuedJobCount()); + + QueueManager::push(LogToDebugJob::class, []); + $this->assertEquals(1, TestQueueClient::getQueuedJobCount()); + + QueueManager::push(LogToDebugJob::class, []); + $this->assertEquals(2, TestQueueClient::getQueuedJobCount()); + } + + /** + * Test replaceAllClients with multiple configs + * + * @return void + */ + public function testReplaceAllClientsWithMultipleConfigs(): void + { + QueueManager::setConfig('test1', ['url' => 'null:']); + QueueManager::setConfig('test2', ['url' => 'null:']); + + TestQueueClient::replaceAllClients(); + + $config1 = QueueManager::getConfig('test1'); + $config2 = QueueManager::getConfig('test2'); + + $this->assertNotNull($config1); + $this->assertNotNull($config2); + + $url1 = $config1['url']; + $url2 = $config2['url']; + $transport1 = is_array($url1) ? $url1['transport'] : $url1; + $transport2 = is_array($url2) ? $url2['transport'] : $url2; + + $this->assertEquals('test:', $transport1); + $this->assertEquals('test:', $transport2); + } + + /** + * Test replaceAllClients handles null config gracefully + * + * @return void + */ + public function testReplaceAllClientsHandlesNullConfig(): void + { + QueueManager::setConfig('test-null', ['url' => 'null:']); + QueueManager::drop('test-null'); + + TestQueueClient::replaceAllClients(); + + $this->assertNull(QueueManager::getConfig('test-null')); + } + + /** + * Test job captured with custom method + * + * @return void + */ + public function testJobCapturedWithCustomMethod(): void + { + $body = json_encode([ + 'class' => [LogToDebugJob::class, 'customMethod'], + 'data' => ['test' => 'value'], + ]); + + $context = new TestContext(); + $destination = $context->createQueue('default'); + $message = $context->createMessage($body); + + TestQueueClient::captureMessage($destination, $message); + + $jobs = $this->getQueuedJobs(); + $this->assertCount(1, $jobs); + $this->assertEquals(LogToDebugJob::class, $jobs[0]['jobClass']); + $this->assertEquals('customMethod', $jobs[0]['method']); + } + + /** + * Test job captured with topic destination + * + * @return void + */ + public function testJobCapturedWithTopicDestination(): void + { + $body = json_encode([ + 'class' => [LogToDebugJob::class], + 'data' => [], + ]); + + $context = new TestContext(); + $destination = $context->createTopic('test-topic'); + $message = $context->createMessage($body); + + TestQueueClient::captureMessage($destination, $message); + + $jobs = $this->getQueuedJobs(); + $this->assertCount(1, $jobs); + $this->assertEquals('test-topic', $jobs[0]['options']['queue']); + } + + /** + * Test job captured with requeue options + * + * @return void + */ + public function testJobCapturedWithRequeueOptions(): void + { + $body = json_encode([ + 'class' => [LogToDebugJob::class], + 'data' => [], + 'requeueOptions' => [ + 'config' => 'custom-config', + 'queue' => 'custom-queue', + 'priority' => 'high', + ], + ]); + + $context = new TestContext(); + $destination = $context->createQueue('default'); + $message = $context->createMessage($body); + + TestQueueClient::captureMessage($destination, $message); + + $jobs = $this->getQueuedJobs(); + $this->assertCount(1, $jobs); + $this->assertEquals('custom-config', $jobs[0]['options']['config']); + $this->assertEquals('custom-queue', $jobs[0]['options']['queue']); + $this->assertEquals('high', $jobs[0]['options']['priority']); + } + + /** + * Test job captured with message properties for delay and expires + * + * @return void + */ + public function testJobCapturedWithMessageProperties(): void + { + $body = json_encode([ + 'class' => [LogToDebugJob::class], + 'data' => [], + ]); + + $context = new TestContext(); + $destination = $context->createQueue('default'); + $message = $context->createMessage($body, [ + 'enqueue.delay' => '5', + 'enqueue.expire' => '10', + 'enqueue.priority' => '5', + ]); + + TestQueueClient::captureMessage($destination, $message, 3000, 6000, 3); + + $jobs = $this->getQueuedJobs(); + $this->assertCount(1, $jobs); + $this->assertEquals(5, $jobs[0]['options']['delay']); + $this->assertEquals(10, $jobs[0]['options']['expires']); + $this->assertEquals(5, $jobs[0]['options']['priority']); + } + + /** + * Test job captured with delivery delay and time to live from producer + * + * @return void + */ + public function testJobCapturedWithProducerDelayAndTtl(): void + { + $body = json_encode([ + 'class' => [LogToDebugJob::class], + 'data' => [], + ]); + + $context = new TestContext(); + $destination = $context->createQueue('default'); + $message = $context->createMessage($body); + + TestQueueClient::captureMessage($destination, $message, 5000, 10000, 3); + + $jobs = $this->getQueuedJobs(); + $this->assertCount(1, $jobs); + $this->assertEquals(5, $jobs[0]['options']['delay']); + $this->assertEquals(10, $jobs[0]['options']['expires']); + $this->assertEquals(3, $jobs[0]['options']['priority']); + } + + /** + * Test extractMessageBody with args fallback + * + * @return void + */ + public function testExtractMessageBodyWithArgs(): void + { + $body = json_encode([ + 'class' => [LogToDebugJob::class], + 'args' => [['test' => 'value']], + ]); + + $context = new TestContext(); + $destination = $context->createQueue('default'); + $message = $context->createMessage($body); + + TestQueueClient::captureMessage($destination, $message); + + $jobs = $this->getQueuedJobs(); + $this->assertEquals(['test' => 'value'], $jobs[0]['data']); + } + + /** + * Test extractMessageBody with invalid JSON + * + * @return void + */ + public function testExtractMessageBodyWithInvalidJson(): void + { + $context = new TestContext(); + $destination = $context->createQueue('default'); + $message = $context->createMessage('invalid json'); + + TestQueueClient::captureMessage($destination, $message); + + $jobs = $this->getQueuedJobs(); + $this->assertNull($jobs[0]['jobClass']); + } + + /** + * Test createConsumer with other destination + * + * @return void + */ + public function testCreateConsumerWithOtherDestination(): void + { + $context = new TestContext(); + $destination = new TestDestination('test'); + + $consumer = $context->createConsumer($destination); + + $this->assertInstanceOf(TestConsumer::class, $consumer); + } + + /** + * Test createConsumer with Topic destination (not Queue) + * + * @return void + */ + public function testCreateConsumerWithTopicOnlyDestination(): void + { + $context = new TestContext(); + $topic = $this->createMock(Topic::class); + $topic->method('getTopicName')->willReturn('test-topic'); + + $consumer = $context->createConsumer($topic); + + $this->assertInstanceOf(TestConsumer::class, $consumer); + } + + /** + * Test QueueConstraintBase with at parameter + * + * @return void + */ + public function testQueueConstraintBaseWithAt(): void + { + QueueManager::push(LogToDebugJob::class, []); + QueueManager::push(LogToDebugJob::class, []); + + $constraint = new JobQueued(0); + $this->assertTrue($constraint->matches(LogToDebugJob::class)); + $this->assertEquals('job #0 was queued', $constraint->toString()); + + $constraint = new JobQueued(99); + $this->assertFalse($constraint->matches(LogToDebugJob::class)); + } +} diff --git a/tests/TestCase/TestSuite/Transport/TransportTest.php b/tests/TestCase/TestSuite/Transport/TransportTest.php new file mode 100644 index 0000000..1a750ff --- /dev/null +++ b/tests/TestCase/TestSuite/Transport/TransportTest.php @@ -0,0 +1,147 @@ +createContext(); + $this->assertInstanceOf(TestContext::class, $context); + $factory->close(); + } + + public function testTestContext(): void + { + $context = new TestContext(); + + $message = $context->createMessage('body', ['prop' => 'value'], ['header' => 'value']); + $this->assertInstanceOf(TestMessage::class, $message); + $this->assertEquals('body', $message->getBody()); + + $queue = $context->createQueue('test-queue'); + $this->assertInstanceOf(TestDestination::class, $queue); + $this->assertEquals('test-queue', $queue->getQueueName()); + + $topic = $context->createTopic('test-topic'); + $this->assertInstanceOf(TestDestination::class, $topic); + $this->assertEquals('test-topic', $topic->getTopicName()); + + $producer = $context->createProducer(); + $this->assertInstanceOf(TestProducer::class, $producer); + + $consumer = $context->createConsumer($queue); + $this->assertInstanceOf(TestConsumer::class, $consumer); + + $topicConsumer = $context->createConsumer($topic); + $this->assertInstanceOf(TestConsumer::class, $topicConsumer); + + $subscriptionConsumer = $context->createSubscriptionConsumer(); + $this->assertInstanceOf(TestSubscriptionConsumer::class, $subscriptionConsumer); + + $tempQueue = $context->createTemporaryQueue(); + $this->assertInstanceOf(TestDestination::class, $tempQueue); + $this->assertStringStartsWith('temp_', $tempQueue->getQueueName()); + + $context->purgeQueue($queue); + $context->close(); + } + + public function testTestMessage(): void + { + $message = new TestMessage('body', ['prop' => 'value'], ['header' => 'value']); + + $this->assertEquals('body', $message->getBody()); + $message->setBody('new-body'); + $this->assertEquals('new-body', $message->getBody()); + + $this->assertEquals('value', $message->getProperty('prop')); + $this->assertEquals('default', $message->getProperty('missing', 'default')); + $message->setProperty('new-prop', 'new-value'); + $this->assertEquals('new-value', $message->getProperty('new-prop')); + + $this->assertEquals('value', $message->getHeader('header')); + $this->assertEquals('default', $message->getHeader('missing', 'default')); + $message->setHeader('new-header', 'new-value'); + $this->assertEquals('new-value', $message->getHeader('new-header')); + + $message->setProperties(['a' => 'b']); + $this->assertEquals(['a' => 'b'], $message->getProperties()); + + $message->setHeaders(['x' => 'y']); + $this->assertEquals(['x' => 'y'], $message->getHeaders()); + + $this->assertFalse($message->isRedelivered()); + + $message->setCorrelationId('corr-123'); + $this->assertEquals('corr-123', $message->getCorrelationId()); + + $message->setMessageId('msg-123'); + $this->assertEquals('msg-123', $message->getMessageId()); + + $message->setTimestamp(1234567890); + $this->assertEquals(1234567890, $message->getTimestamp()); + + $message->setReplyTo('reply-to'); + $this->assertEquals('reply-to', $message->getReplyTo()); + } + + public function testTestProducer(): void + { + $producer = new TestProducer(); + $destination = new TestDestination('test-queue'); + $message = new TestMessage('body'); + + $producer->setDeliveryDelay(1000); + $this->assertEquals(1000, $producer->getDeliveryDelay()); + + $producer->setPriority(5); + $this->assertEquals(5, $producer->getPriority()); + + $producer->setTimeToLive(2000); + $this->assertEquals(2000, $producer->getTimeToLive()); + + $producer->send($destination, $message); + } + + public function testTestConsumer(): void + { + $subscriptionConsumer = new TestSubscriptionConsumer(); + $queue = new TestDestination('test-queue'); + $consumer = new TestConsumer($queue); + + $this->assertEquals($queue, $consumer->getQueue()); + $this->assertNull($consumer->receive()); + $this->assertNull($consumer->receiveNoWait()); + + $message = new TestMessage('body'); + $consumer->acknowledge($message); + $consumer->reject($message, true); + + $subscriptionConsumer->consume(1000); + $subscriptionConsumer->subscribe($consumer, function () { + }); + $subscriptionConsumer->unsubscribe($consumer); + $subscriptionConsumer->unsubscribeAll(); + } + + public function testTestDestination(): void + { + $destination = new TestDestination('test-name'); + + $this->assertEquals('test-name', $destination->getQueueName()); + $this->assertEquals('test-name', $destination->getTopicName()); + $this->assertEquals('test-name', $destination->getDestinationName()); + } +}