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
+
+
+
+