diff --git a/composer.json b/composer.json index ff9db427d..5bdae58bc 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,7 @@ "php-http/guzzle6-adapter": "^1.1", "php-http/client-common": "^1.7@dev", "richardfullmer/rabbitmq-management-api": "^2.0", + "microsoft/azure-storage-queue": "^1.1", "predis/predis": "^1.1", "thruway/pawl-transport": "^0.5.0", "voryx/thruway": "^0.5.3", @@ -62,6 +63,7 @@ "Enqueue\\AmqpTools\\": "pkg/amqp-tools/", "Enqueue\\AsyncEventDispatcher\\": "pkg/async-event-dispatcher/", "Enqueue\\AsyncCommand\\": "pkg/async-command/", + "Enqueue\\AzureStorage\\": "pkg/azure-storage/", "Enqueue\\Dbal\\": "pkg/dbal/", "Enqueue\\Bundle\\": "pkg/enqueue-bundle/", "Enqueue\\Fs\\": "pkg/fs/", diff --git a/docs/transport/azure.md b/docs/transport/azure.md new file mode 100644 index 000000000..87df687e6 --- /dev/null +++ b/docs/transport/azure.md @@ -0,0 +1,124 @@ +

Supporting Enqueue

+ +Enqueue is an MIT-licensed open source project with its ongoing development made possible entirely by the support of community and our customers. If you'd like to join them, please consider: + +- [Become a sponsor](https://www.patreon.com/makasim) +- [Become our client](http://forma-pro.com/) + +--- + +# Azure Storage transport + +The transport uses [Azure Storage](https://docs.microsoft.com/en-us/azure/storage/queues/storage-dotnet-how-to-use-queues) as a message broker. +It creates a collection (a queue or topic) there. It's a FIFO system (First In First Out). + +* [Installation](#installation) +* [Create context](#create-context) +* [Send message to topic](#send-message-to-topic) +* [Send message to queue](#send-message-to-queue) +* [Send expiration message](#send-expiration-message) +* [Consume message](#consume-message) +* [Delete queue (purge messages)](#delete-queue-purge-messages) +* [Delete topic (purge messages)](#delete-topic-purge-messages) + +## Installation + +* With composer: + +```bash +$ composer require enqueue/azure-storage +``` + +## Create context + +```php +;AccountKey='); + +$context = $factory->createContext(); + +``` + +## Send message to topic + +```php +createTopic('aTopic'); +$message = $context->createMessage('Hello world!'); + +$context->createProducer()->send($fooTopic, $message); +``` + +## Send message to queue + +```php +createQueue('aQueue'); +$message = $context->createMessage('Hello world!'); + +$context->createProducer()->send($fooQueue, $message); +``` + +## Send expiration message + +```php +createMessage('Hello world!'); + +$context->createProducer() + ->setTimeToLive(60000) // 60 sec + ->send($fooQueue, $message) +; +``` + +## Consume message: + +```php +createQueue('aQueue'); +$consumer = $context->createConsumer($fooQueue); + +$message = $consumer->receiveNoWait(); + +// process a message + +$consumer->acknowledge($message); +//$consumer->reject($message); +``` + +## Delete queue (purge messages): + +```php +createQueue('aQueue'); + +$context->deleteQueue($fooQueue); +``` + +## Delete topic (purge messages): + +```php +createTopic('aTopic'); + +$context->deleteTopic($fooTopic); +``` + +[back to index](../index.md) \ No newline at end of file diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f1c8f205f..83667abfd 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -105,6 +105,10 @@ pkg/dsn/Tests + + pkg/azure-storage/Tests + + pkg/wamp/Tests diff --git a/pkg/azure-storage/AzureStorageConnectionFactory.php b/pkg/azure-storage/AzureStorageConnectionFactory.php new file mode 100644 index 000000000..e57d3a6ae --- /dev/null +++ b/pkg/azure-storage/AzureStorageConnectionFactory.php @@ -0,0 +1,28 @@ +connectionString = $connectionString; + } + + public function createContext(): Context + { + $client = QueueRestProxy::createQueueService($this->connectionString); + + return new AzureStorageContext($client); + } +} \ No newline at end of file diff --git a/pkg/azure-storage/AzureStorageConsumer.php b/pkg/azure-storage/AzureStorageConsumer.php new file mode 100644 index 000000000..027560b96 --- /dev/null +++ b/pkg/azure-storage/AzureStorageConsumer.php @@ -0,0 +1,101 @@ +client = $client; + $this->queue = $queue; + $this->context = $context; + } + + /** + * @inheritdoc + */ + public function getQueue(): Queue + { + return $this->queue; + } + + /** + * @inheritdoc + */ + public function receiveNoWait(): ?Message + { + $options = new ListMessagesOptions(); + $options->setNumberOfMessages(1); + $options->setVisibilityTimeoutInSeconds($this->visibilityTimeout); + + $listMessagesResult = $this->client->listMessages($this->queue->getQueueName(), $options); + $messages = $listMessagesResult->getQueueMessages(); + + if($messages) { + $message = $messages[0]; + + $formattedMessage = new AzureStorageMessage(); + $formattedMessage->setMessageId($message->getMessageId()); + $formattedMessage->setBody($message->getMessageText()); + $formattedMessage->setTimestamp($message->getInsertionDate()->getTimestamp()); + $formattedMessage->setRedelivered($message->getDequeueCount() > 1); + + $formattedMessage->setHeaders([ + 'dequeue_count' => $message->getDequeueCount(), + 'expiration_date' => $message->getExpirationDate(), + 'pop_peceipt' => $message->getExpirationDate(), + 'next_time_visible' => $message->getTimeNextVisible(), + ]); + + return $formattedMessage; + } else { + return null; + } + } + + /** + * @inheritdoc + */ + public function acknowledge(Message $message): void + { + InvalidMessageException::assertMessageInstanceOf($message, AzureStorageMessage::class); + + $this->client->deleteMessage($this->queue->getQueueName(), $message->getMessageId(), $message->getHeader('pop_receipt')); + } + + /** + * @inheritdoc + */ + public function reject(Message $message, bool $requeue = false): void + { + InvalidMessageException::assertMessageInstanceOf($message, AzureStorageMessage::class); + + if (true === $requeue) { + $producer = $this->context->createProducer(); + $producer->send($this->queue, $message); + } else { + $this->acknowledge($message); + } + } +} \ No newline at end of file diff --git a/pkg/azure-storage/AzureStorageContext.php b/pkg/azure-storage/AzureStorageContext.php new file mode 100644 index 000000000..ac45674f4 --- /dev/null +++ b/pkg/azure-storage/AzureStorageContext.php @@ -0,0 +1,115 @@ +client = $client; + } + + public function createMessage(string $body = '', array $properties = [], array $headers = []): Message + { + $message = new AzureStorageMessage(); + $message->setBody($body); + $message->setProperties($properties); + $message->setHeaders($headers); + return $message; + } + + public function createTopic(string $topicName): Topic + { + return new AzureStorageDestination($topicName); + } + + public function createQueue(string $queueName): Queue + { + return new AzureStorageDestination($queueName); + } + + + /** + * @param AzureStorageDestination $queue + */ + public function deleteQueue(Queue $queue): void + { + InvalidDestinationException::assertDestinationInstanceOf($queue, AzureStorageDestination::class); + + $this->client->deleteQueue($queue); + } + + /** + * @param AzureStorageDestination $topic + */ + public function deleteTopic(Topic $topic): void + { + InvalidDestinationException::assertDestinationInstanceOf($topic, AzureStorageDestination::class); + + $this->client->deleteQueue($topic); + } + + /** + * @inheritdoc + */ + public function createTemporaryQueue(): Queue + { + throw new TemporaryQueueNotSupportedException(); + } + + public function createProducer(): Producer + { + return new AzureStorageProducer($this->client); + } + + /** + * @param AzureStorageDestination $destination + * @return Consumer + * @throws InvalidDestinationException + */ + public function createConsumer(Destination $destination): Consumer + { + InvalidDestinationException::assertDestinationInstanceOf($destination, AzureStorageDestination::class); + + return new AzureStorageConsumer($this->client, $destination, $this); + } + + /** + * @inheritdoc + */ + public function createSubscriptionConsumer(): SubscriptionConsumer + { + throw new SubscriptionConsumerNotSupportedException(); + } + + /** + * @inheritdoc + */ + public function purgeQueue(Queue $queue): void + { + throw new PurgeQueueNotSupportedException(); + } + + public function close(): void + {} +} \ No newline at end of file diff --git a/pkg/azure-storage/AzureStorageDestination.php b/pkg/azure-storage/AzureStorageDestination.php new file mode 100644 index 000000000..de84fa5ca --- /dev/null +++ b/pkg/azure-storage/AzureStorageDestination.php @@ -0,0 +1,35 @@ +name = $name; + } + + public function getName(): string + { + return $this->name; + } + + public function getQueueName(): string + { + return $this->name; + } + + public function getTopicName(): string + { + return $this->name; + } +} \ No newline at end of file diff --git a/pkg/azure-storage/AzureStorageMessage.php b/pkg/azure-storage/AzureStorageMessage.php new file mode 100644 index 000000000..31a5ddc0d --- /dev/null +++ b/pkg/azure-storage/AzureStorageMessage.php @@ -0,0 +1,33 @@ +body = $body; + $this->properties = $properties; + $this->headers = $headers; + + $this->redelivered = false; + $this->visibilityTimeout = 0; + } + + public function getVisibilityTimeout() + { + return $this->visibilityTimeout; + } + + public function setVisibilityTimeout($visibilityTimeout) + { + $this->visibilityTimeout = $visibilityTimeout; + return $this; + } +} \ No newline at end of file diff --git a/pkg/azure-storage/AzureStorageProducer.php b/pkg/azure-storage/AzureStorageProducer.php new file mode 100644 index 000000000..57468bb35 --- /dev/null +++ b/pkg/azure-storage/AzureStorageProducer.php @@ -0,0 +1,113 @@ +client = $client; + } + + /** + * @var AzureStorageDestination $destination + * @var AzureStorageMessage $message + * @throws InvalidDestinationException if a client uses this method with an invalid destination + * @throws InvalidMessageException if an invalid message is specified + */ + public function send(Destination $destination, Message $message): void + { + InvalidDestinationException::assertDestinationInstanceOf($destination, AzureStorageDestination::class); + InvalidMessageException::assertMessageInstanceOf($message, AzureStorageMessage::class); + + $options = new CreateMessageOptions(); + $options->setTimeToLiveInSeconds(intval($this->timeToLive / 1000)); + $options->setVisibilityTimeoutInSeconds($message->getVisibilityTimeout()); + + $result = $this->client->createMessage($destination->getName(), $message->getBody()); + $resultMessage = $result->getQueueMessage(); + + $message->setMessageId($resultMessage->getMessageId()); + $message->setTimestamp($resultMessage->getInsertionDate()->getTimestamp()); + $message->setRedelivered($resultMessage->getDequeueCount() > 1); + + $message->setHeaders([ + 'dequeueCount' => $resultMessage->getDequeueCount(), + 'expirationDate' => $resultMessage->getExpirationDate(), + 'popReceipt' => $resultMessage->getExpirationDate(), + 'nextTimeVisible' => $resultMessage->getTimeNextVisible(), + ]); + } + + /** + * @inheritdoc + */ + public function setDeliveryDelay(int $deliveryDelay = null): Producer + { + if (null !== $deliveryDelay) + throw new DeliveryDelayNotSupportedException(); + } + + /** + * @inheritdoc + */ + public function getDeliveryDelay(): ?int + { + return null; + } + + /** + * @inheritdoc + */ + public function setPriority(int $priority = null): Producer + { + if (null !== $priority) + throw new PriorityNotSupportedException(); + } + + /** + * @inheritdoc + */ + public function getPriority(): ?int + { + return null; + } + + /** + * @var integer + */ + protected $timeToLive; + + /** + * @inheritdoc + */ + public function setTimeToLive(int $timeToLive = null): Producer + { + $this->timeToLive = $timeToLive; + return $this; + } + + /** + * @inheritdoc + */ + public function getTimeToLive(): ?int + { + return $this->timeToLive; + } +} \ No newline at end of file diff --git a/pkg/azure-storage/LICENSE b/pkg/azure-storage/LICENSE new file mode 100644 index 000000000..d9736f8bf --- /dev/null +++ b/pkg/azure-storage/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) +Copyright (c) 2017 Kotliar Maksym + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished +to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/pkg/azure-storage/Tests/AzureStorageConnectionFactoryTest.php b/pkg/azure-storage/Tests/AzureStorageConnectionFactoryTest.php new file mode 100644 index 000000000..a5991a97f --- /dev/null +++ b/pkg/azure-storage/Tests/AzureStorageConnectionFactoryTest.php @@ -0,0 +1,20 @@ +assertClassImplements(ConnectionFactory::class, AzureStorageConnectionFactory::class); + } + +} diff --git a/pkg/azure-storage/Tests/AzureStorageConsumerTest.php b/pkg/azure-storage/Tests/AzureStorageConsumerTest.php new file mode 100644 index 000000000..36317c41f --- /dev/null +++ b/pkg/azure-storage/Tests/AzureStorageConsumerTest.php @@ -0,0 +1,213 @@ +assertClassImplements(Consumer::class, AzureStorageConsumer::class); + } + + public function testCouldBeConstructedWithContextAndDestinationAndPreFetchCountAsArguments() + { + $restProxy = $this->createQueueRestProxyMock(); + new AzureStorageConsumer($restProxy, new AzureStorageDestination('aQueue'), new AzureStorageContext($restProxy)); + } + + public function testShouldReturnDestinationSetInConstructorOnGetQueue() + { + $destination = new AzureStorageDestination('aQueue'); + $restProxy = $this->createQueueRestProxyMock(); + $consumer = new AzureStorageConsumer($restProxy, $destination, new AzureStorageContext($restProxy)); + + $this->assertSame($destination, $consumer->getQueue()); + } + + public function testShouldAlwaysReturnNullOnReceiveNoWait() + { + $options = new ListMessagesOptions(); + $options->setNumberOfMessages(1); + + $listMessagesResultMock = $this->createMock(ListMessagesResult::class); + $listMessagesResultMock + ->expects($this->any()) + ->method('getQueueMessages') + ->willReturn([]) + ; + + $azureMock = $this->createQueueRestProxyMock(); + $azureMock + ->expects($this->any()) + ->method('listMessages') + ->with('aQueue', $options) + ->willReturn($listMessagesResultMock) + ; + + $consumer = new AzureStorageConsumer($azureMock, new AzureStorageDestination('aQueue'), new AzureStorageContext($azureMock)); + + $this->assertNull($consumer->receiveNoWait()); + $this->assertNull($consumer->receiveNoWait()); + $this->assertNull($consumer->receiveNoWait()); + } + + public function testShouldDoNothingOnAcknowledge() + { + $restProxy = $this->createQueueRestProxyMock(); + $consumer = new AzureStorageConsumer($restProxy, new AzureStorageDestination('aQueue'), new AzureStorageContext($restProxy)); + + $consumer->acknowledge(new AzureStorageMessage()); + } + + public function testShouldDoNothingOnReject() + { + $restProxy = $this->createQueueRestProxyMock(); + $consumer = new AzureStorageConsumer($restProxy, new AzureStorageDestination('aQueue'), new AzureStorageContext($restProxy)); + + $consumer->reject(new AzureStorageMessage()); + } + + public function testShouldQueueMsgAgainReject() + { + $messageMock = $this->createQueueMessageMock(); + + $options = new ListMessagesOptions(); + $options->setNumberOfMessages(1); + + $listMessagesResultMock = $this->createMock(ListMessagesResult::class); + $listMessagesResultMock + ->expects($this->any()) + ->method('getQueueMessages') + ->willReturn([$messageMock]) + ; + $createMessageResultMock = $this->createMock(CreateMessageResult::class); + $createMessageResultMock + ->expects($this->any()) + ->method('getQueueMessage') + ->willReturn($messageMock) + ; + + $azureMock = $this->createQueueRestProxyMock(); + $azureMock + ->expects($this->any()) + ->method('listMessages') + ->with('aQueue', $options) + ->willReturn($listMessagesResultMock) + ; + $azureMock + ->expects($this->any()) + ->method('createMessage') + ->with('aQueue', $messageMock->getMessageText()) + ->willReturn($createMessageResultMock) + ; + + $consumer = new AzureStorageConsumer($azureMock, new AzureStorageDestination('aQueue'), new AzureStorageContext($azureMock)); + + $receivedMessage = $consumer->receiveNoWait(); + + $consumer->reject($receivedMessage, true); + + $this->assertInstanceOf(AzureStorageMessage::class, $receivedMessage); + $this->assertSame('aBody', $receivedMessage->getBody()); + } + + public function testShouldReturnMsgOnReceiveNoWait() + { + $messageMock = $this->createQueueMessageMock(); + + $options = new ListMessagesOptions(); + $options->setNumberOfMessages(1); + + $listMessagesResultMock = $this->createMock(ListMessagesResult::class); + $listMessagesResultMock + ->expects($this->any()) + ->method('getQueueMessages') + ->willReturn([$messageMock]) + ; + + $azureMock = $this->createQueueRestProxyMock(); + $azureMock + ->expects($this->any()) + ->method('listMessages') + ->with('aQueue', $options) + ->willReturn($listMessagesResultMock) + ; + + $consumer = new AzureStorageConsumer($azureMock, new AzureStorageDestination('aQueue'), new AzureStorageContext($azureMock)); + + $receivedMessage = $consumer->receiveNoWait(); + $this->assertInstanceOf(AzureStorageMessage::class, $receivedMessage); + $this->assertSame('aBody', $receivedMessage->getBody()); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|QueueRestProxy + */ + private function createQueueRestProxyMock() + { + return $this->createMock(QueueRestProxy::class); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|QueueMessage + */ + private function createQueueMessageMock() + { + $insertionDateMock = $this->createMock(\DateTime::class); + $insertionDateMock + ->expects($this->any()) + ->method('getTimestamp') + ->willReturn(1542809366); + + $messageMock = $this->createMock(QueueMessage::class); + $messageMock + ->expects($this->any()) + ->method('getMessageId') + ->willReturn('any'); + $messageMock + ->expects($this->any()) + ->method('getMessageText') + ->willReturn('aBody'); + $messageMock + ->expects($this->any()) + ->method('getInsertionDate') + ->willReturn($insertionDateMock); + $messageMock + ->expects($this->any()) + ->method('getDequeueCount') + ->willReturn('any'); + $messageMock + ->expects($this->any()) + ->method('getDequeueCount') + ->willReturn('any'); + $messageMock + ->expects($this->any()) + ->method('getExpirationDate') + ->willReturn('any'); + $messageMock + ->expects($this->any()) + ->method('getExpirationDate') + ->willReturn('any'); + $messageMock + ->expects($this->any()) + ->method('getTimeNextVisible') + ->willReturn('any'); + return $messageMock; + } +} diff --git a/pkg/azure-storage/Tests/AzureStorageContextTest.php b/pkg/azure-storage/Tests/AzureStorageContextTest.php new file mode 100644 index 000000000..12bcae0f1 --- /dev/null +++ b/pkg/azure-storage/Tests/AzureStorageContextTest.php @@ -0,0 +1,164 @@ +assertClassImplements(Context::class, AzureStorageContext::class); + } + + public function testShouldAllowCreateEmptyMessage() + { + $context = new AzureStorageContext($this->createQueueRestProxyMock()); + + $message = $context->createMessage(); + + $this->assertInstanceOf(Message::class, $message); + + $this->assertSame('', $message->getBody()); + $this->assertSame([], $message->getProperties()); + $this->assertSame([], $message->getHeaders()); + } + + public function testShouldAllowCreateCustomMessage() + { + $context = new AzureStorageContext($this->createQueueRestProxyMock()); + + $message = $context->createMessage('theBody', ['aProp' => 'aPropVal'], ['aHeader' => 'aHeaderVal']); + + $this->assertInstanceOf(Message::class, $message); + + $this->assertSame('theBody', $message->getBody()); + $this->assertSame(['aProp' => 'aPropVal'], $message->getProperties()); + $this->assertSame(['aHeader' => 'aHeaderVal'], $message->getHeaders()); + } + + public function testShouldCreateQueue() + { + $context = new AzureStorageContext($this->createQueueRestProxyMock()); + + $queue = $context->createQueue('aQueue'); + + $this->assertInstanceOf(AzureStorageDestination::class, $queue); + $this->assertSame('aQueue', $queue->getQueueName()); + } + + public function testShouldAllowCreateTopic() + { + $context = new AzureStorageContext($this->createQueueRestProxyMock()); + + $topic = $context->createTopic('aTopic'); + + $this->assertInstanceOf(AzureStorageDestination::class, $topic); + $this->assertSame('aTopic', $topic->getTopicName()); + } + + public function testThrowNotImplementedOnCreateTmpQueueCall() + { + $context = new AzureStorageContext($this->createQueueRestProxyMock()); + + $this->expectException(TemporaryQueueNotSupportedException::class); + + $context->createTemporaryQueue(); + } + + public function testShouldCreateProducer() + { + $context = new AzureStorageContext($this->createQueueRestProxyMock()); + + $producer = $context->createProducer(); + + $this->assertInstanceOf(AzureStorageProducer::class, $producer); + } + + public function testShouldThrowIfNotAzureStorageDestinationGivenOnCreateConsumer() + { + $context = new AzureStorageContext($this->createQueueRestProxyMock()); + + $this->expectException(InvalidDestinationException::class); + + $consumer = $context->createConsumer(new NullQueue('aQueue')); + + $this->assertInstanceOf(AzureStorageConsumer::class, $consumer); + } + + public function testShouldCreateConsumer() + { + $context = new AzureStorageContext($this->createQueueRestProxyMock()); + + $queue = $context->createQueue('aQueue'); + + $consumer = $context->createConsumer($queue); + + $this->assertInstanceOf(AzureStorageConsumer::class, $consumer); + } + + public function testThrowIfNotAzureStorageDestinationGivenOnDeleteQueue() + { + $context = new AzureStorageContext($this->createQueueRestProxyMock()); + + $this->expectException(InvalidDestinationException::class); + $context->deleteQueue(new NullQueue('aQueue')); + } + + public function testShouldAllowDeleteQueue() + { + $context = new AzureStorageContext($this->createQueueRestProxyMock()); + + $queue = $context->createQueue('aQueueName'); + + $context->deleteQueue($queue); + } + + public function testThrowIfNotAzureStorageDestinationGivenOnDeleteTopic() + { + $context = new AzureStorageContext($this->createQueueRestProxyMock()); + + $this->expectException(InvalidDestinationException::class); + $context->deleteTopic(new NullTopic('aTopic')); + } + + public function testShouldAllowDeleteTopic() + { + $context = new AzureStorageContext($this->createQueueRestProxyMock()); + + $topic = $context->createTopic('aTopicName'); + + $context->deleteQueue($topic); + } + + public function testShouldReturnNotSupportedSubscriptionConsumerInstance() + { + $context = new AzureStorageContext($this->createQueueRestProxyMock()); + + $this->expectException(SubscriptionConsumerNotSupportedException::class); + $this->assertInstanceOf(AzureStorageConsumer::class, $context->createSubscriptionConsumer()); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|QueueRestProxy + */ + private function createQueueRestProxyMock() + { + return $this->createMock(QueueRestProxy::class); + } +} diff --git a/pkg/azure-storage/Tests/AzureStorageDestinationTest.php b/pkg/azure-storage/Tests/AzureStorageDestinationTest.php new file mode 100644 index 000000000..551c6890d --- /dev/null +++ b/pkg/azure-storage/Tests/AzureStorageDestinationTest.php @@ -0,0 +1,29 @@ +assertClassImplements(Topic::class, AzureStorageDestination::class); + $this->assertClassImplements(Queue::class, AzureStorageDestination::class); + } + + public function testShouldReturnNameSetInConstructor() + { + $destination = new AzureStorageDestination('aDestinationName'); + + $this->assertSame('aDestinationName', $destination->getName()); + $this->assertSame('aDestinationName', $destination->getQueueName()); + $this->assertSame('aDestinationName', $destination->getTopicName()); + } +} diff --git a/pkg/azure-storage/Tests/AzureStorageMessageTest.php b/pkg/azure-storage/Tests/AzureStorageMessageTest.php new file mode 100644 index 000000000..252bb5277 --- /dev/null +++ b/pkg/azure-storage/Tests/AzureStorageMessageTest.php @@ -0,0 +1,70 @@ +assertClassImplements(Message::class, AzureStorageMessage::class); + } + + public function testCouldConstructMessageWithoutArguments() + { + $message = new AzureStorageMessage(); + + $this->assertSame('', $message->getBody()); + $this->assertSame([], $message->getProperties()); + $this->assertSame([], $message->getHeaders()); + } + + public function testCouldBeConstructedWithOptionalArguments() + { + $message = new AzureStorageMessage('theBody', ['barProp' => 'barPropVal'], ['fooHeader' => 'fooHeaderVal']); + + $this->assertSame('theBody', $message->getBody()); + $this->assertSame(['barProp' => 'barPropVal'], $message->getProperties()); + $this->assertSame(['fooHeader' => 'fooHeaderVal'], $message->getHeaders()); + } + + public function testShouldSetCorrelationIdAsHeader() + { + $message = new AzureStorageMessage(); + $message->setCorrelationId('the-correlation-id'); + + $this->assertSame(['correlation_id' => 'the-correlation-id'], $message->getHeaders()); + } + + public function testCouldSetMessageIdAsHeader() + { + $message = new AzureStorageMessage(); + $message->setMessageId('the-message-id'); + + $this->assertSame(['message_id' => 'the-message-id'], $message->getHeaders()); + } + + public function testCouldSetTimestampAsHeader() + { + $message = new AzureStorageMessage(); + $message->setTimestamp(12345); + + $this->assertSame(['timestamp' => 12345], $message->getHeaders()); + } + + public function testShouldSetReplyToAsHeader() + { + $message = new AzureStorageMessage(); + $message->setReplyTo('theQueueName'); + + $this->assertSame(['reply_to' => 'theQueueName'], $message->getHeaders()); + } + +} \ No newline at end of file diff --git a/pkg/azure-storage/Tests/AzureStorageProducerTest.php b/pkg/azure-storage/Tests/AzureStorageProducerTest.php new file mode 100644 index 000000000..5bcbebabb --- /dev/null +++ b/pkg/azure-storage/Tests/AzureStorageProducerTest.php @@ -0,0 +1,134 @@ +assertClassImplements(Producer::class, AzureStorageProducer::class); + } + + public function testCouldBeConstructedWithQueueRestProxy() + { + $producer = new AzureStorageProducer($this->createQueueRestProxyMock()); + + $this->assertInstanceOf(AzureStorageProducer::class, $producer); + } + + public function testThrowIfDestinationNotAzureStorageDestinationOnSend() + { + $producer = new AzureStorageProducer($this->createQueueRestProxyMock()); + + $this->expectException(InvalidDestinationException::class); + $this->expectExceptionMessage('The destination must be an instance of Enqueue\AzureStorage\AzureStorageDestination but got Enqueue\Null\NullQueue.'); + $producer->send(new NullQueue('aQueue'), new AzureStorageMessage()); + } + + public function testThrowIfMessageNotAzureStorageMessageOnSend() + { + $producer = new AzureStorageProducer($this->createQueueRestProxyMock()); + + $this->expectException(InvalidMessageException::class); + $this->expectExceptionMessage('The message must be an instance of Enqueue\AzureStorage\AzureStorageMessage but it is Enqueue\Null\NullMessage.'); + $producer->send(new AzureStorageDestination('aQueue'), new NullMessage()); + } + + public function testShouldCallCreateMessageOnSend() + { + $destination = new AzureStorageDestination('aDestination'); + $message = new AzureStorageMessage(); + + $queueMessage = $this->createQueueMessageMock(); + + $createMessageResult = $this->createMock(CreateMessageResult::class); + $createMessageResult + ->expects($this->once()) + ->method('getQueueMessage') + ->willReturn($queueMessage); + + $queueRestProxy = $this->createQueueRestProxyMock(); + $queueRestProxy + ->expects($this->once()) + ->method('createMessage') + ->with('aDestination', $message->getBody()) + ->willReturn($createMessageResult) + ; + + $producer = new AzureStorageProducer($queueRestProxy); + + $producer->send($destination, $message); + } + + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|QueueRestProxy + */ + private function createQueueRestProxyMock() + { + return $this->createMock(QueueRestProxy::class); + } + + /** + * @return \PHPUnit_Framework_MockObject_MockObject|QueueMessage + */ + private function createQueueMessageMock() + { + $insertionDateMock = $this->createMock(\DateTime::class); + $insertionDateMock + ->expects($this->any()) + ->method('getTimestamp') + ->willReturn(1542809366); + + $messageMock = $this->createMock(QueueMessage::class); + $messageMock + ->expects($this->any()) + ->method('getMessageId') + ->willReturn('any'); + $messageMock + ->expects($this->any()) + ->method('getMessageText') + ->willReturn('aBody'); + $messageMock + ->expects($this->any()) + ->method('getInsertionDate') + ->willReturn($insertionDateMock); + $messageMock + ->expects($this->any()) + ->method('getDequeueCount') + ->willReturn('any'); + $messageMock + ->expects($this->any()) + ->method('getDequeueCount') + ->willReturn('any'); + $messageMock + ->expects($this->any()) + ->method('getExpirationDate') + ->willReturn('any'); + $messageMock + ->expects($this->any()) + ->method('getExpirationDate') + ->willReturn('any'); + $messageMock + ->expects($this->any()) + ->method('getTimeNextVisible') + ->willReturn('any'); + return $messageMock; + } +} diff --git a/pkg/azure-storage/composer.json b/pkg/azure-storage/composer.json new file mode 100644 index 000000000..9babd854c --- /dev/null +++ b/pkg/azure-storage/composer.json @@ -0,0 +1,38 @@ +{ + "name": "enqueue/azure-storage", + "type": "library", + "description": "Message Queue Azure Storage Transport", + "keywords": ["messaging", "queue", "azure", "storage"], + "homepage": "https://enqueue.forma-pro.com/", + "license": "MIT", + "require": { + "php": "^7.1.3", + "queue-interop/queue-interop": "0.7.x-dev", + "microsoft/azure-storage-queue": "^1.1" + }, + "require-dev": { + "phpunit/phpunit": "~5.4.0", + "enqueue/test": "0.9.x-dev", + "enqueue/null": "0.9.x-dev", + "queue-interop/queue-spec": "0.6.x-dev" + }, + "support": { + "email": "opensource@forma-pro.com", + "issues": "https://github.com/php-enqueue/enqueue-dev/issues", + "forum": "https://gitter.im/php-enqueue/Lobby", + "source": "https://github.com/php-enqueue/enqueue-dev", + "docs": "https://github.com/php-enqueue/enqueue-dev/blob/master/docs/index.md" + }, + "autoload": { + "psr-4": { "Enqueue\\AzureStorage\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "minimum-stability": "dev", + "extra": { + "branch-alias": { + "dev-master": "0.9.x-dev" + } + } +} diff --git a/pkg/azure-storage/phpunit.xml.dist b/pkg/azure-storage/phpunit.xml.dist new file mode 100644 index 000000000..7d6c5ed3e --- /dev/null +++ b/pkg/azure-storage/phpunit.xml.dist @@ -0,0 +1,30 @@ + + + + + + + ./Tests + + + + + + . + + ./vendor + ./Tests + + + +