diff --git a/.env.example b/.env.example
new file mode 100644
index 0000000..8ea2ef0
--- /dev/null
+++ b/.env.example
@@ -0,0 +1,5 @@
+AWS_REGION="us-west-2"
+AWS_VERSION="latest"
+AWS_ACCOUNT="MyAwsAccount"
+AWS_KEY="foo"
+AWS_SECRET="bar"
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..70d60c1
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,6 @@
+vendor
+.env
+behat.yml
+composer.lock
+coverage.clover
+phpunit.xml
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..9efbb98
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,21 @@
+language: php
+php:
+ - 5.5
+ - 5.6
+ - 7.0
+ - hhvm
+env:
+ - COMPOSER_OPTS=""
+ - COMPOSER_OPTS="--prefer-lowest"
+matrix:
+ allow_failures:
+ - php: hhvm
+ fast_finish: true
+before_script:
+ - composer self-update
+ - composer install --no-interaction
+script:
+ - vendor/bin/phing
+after_script:
+ - wget https://scrutinizer-ci.com/ocular.phar
+ - php ocular.phar code-coverage:upload --format=php-clover coverage.clover
diff --git a/README.md b/README.md
index e968773..d48c1cd 100644
--- a/README.md
+++ b/README.md
@@ -1 +1,60 @@
-# aws-php-claim-check-sdk
\ No newline at end of file
+[![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/abacaphiliac/aws-sdk-php-claim-check/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/abacaphiliac/aws-sdk-php-claim-check/?branch=master)
+[![Code Coverage](https://scrutinizer-ci.com/g/abacaphiliac/aws-sdk-php-claim-check/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/abacaphiliac/aws-sdk-php-claim-check/?branch=master)
+[![Build Status](https://travis-ci.org/abacaphiliac/aws-sdk-php-claim-check.svg?branch=master)](https://travis-ci.org/abacaphiliac/aws-sdk-php-claim-check)
+
+# abacaphiliac/aws-php-claim-check-sdk
+
+An implementation of the
+[Claim Check](http://www.enterpriseintegrationpatterns.com/patterns/messaging/StoreInLibrary.html)
+Enterprise Integration Pattern, utilizing Amazon Web Services. AWS recommends usage of the Claim Check pattern on the
+[SQS Compliance FAQ](https://aws.amazon.com/sqs/faqs/#Compliance).
+
+![StoreInLibrary](http://www.enterpriseintegrationpatterns.com/img/StoreInLibrary.gif "StoreInLibrary")
+
+This library provides extended SNS and SQS clients to "Check Luggage", uses S3 as the "Data Store",
+and uses the extended SQS client as the "Data Enricher".
+
+This package aims to be a compatible port of
+Amazon's [Extended Client Library](https://github.com/awslabs/amazon-sqs-java-extended-client-lib).
+Messages stored via this PHP package should be able to be received by the Java package.
+
+# AWS [Extended Client Library](https://github.com/awslabs/amazon-sqs-java-extended-client-lib) Compatibility
+## Similarities
+* Claim Check structure is identical, i.e. keys are `s3BucketName` and `s3Key`,
+meaning you can publish to SQS+S3 via this PHP lib and read via the AWS Extended Client Library java lib.
+
+## Differences
+* Messages published from SNS to SQS contain a nested Claim Check message structure,
+so the Java SDK is not be able to natively consume messages published to SNS (really???).
+* Usage of the pattern is not configurable by message size, nor can the pattern be disabled in this lib.
+Use the wrapped clients if you do not want to use Claim Check.
+* The AWS Extended Client Library will always delete the message from S3 when the message is deleted from SQS.
+This is not acceptable when messages are published to SNS and fanned-out to multiple subscribers
+(e.g. multiple SQS queues). This package allows you to disable deletion from S3 in the SQS extended client
+configuration.
+
+# Installation
+```
+composer require abacaphiliac/aws-php-claim-check-sdk
+```
+
+## Contributing
+```
+composer install && vendor/bin/phing
+```
+
+This library attempts to comply with [PSR-1][], [PSR-2][], and [PSR-4][]. If
+you notice compliance oversights, please send a patch via pull request.
+
+[PSR-1]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-1-basic-coding-standard.md
+[PSR-2]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md
+[PSR-4]: https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-4-autoloader.md
+
+# Tasks
+- [x] Add SQS check-in.
+- [x] Add SQS check-out.
+- [x] Add SNS check-in.
+- [ ] Use WireMock to stub API responses and add feature tests to CI.
+- [ ] Add async SQS check-in.
+- [ ] Add async SQS check-out.
+- [ ] Add async SNS check-in.
diff --git a/behat.yml.dist b/behat.yml.dist
new file mode 100644
index 0000000..623cc52
--- /dev/null
+++ b/behat.yml.dist
@@ -0,0 +1,5 @@
+default:
+ suites:
+ s3-data-store:
+ paths: [ %paths.base%/features/s3-data-store ]
+ contexts: [ AbacaphiliacFeature\AwsSdk\ClaimCheck\Bootstrap\S3DataStoreContext ]
diff --git a/build.xml b/build.xml
new file mode 100644
index 0000000..d6d4951
--- /dev/null
+++ b/build.xml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/composer.json b/composer.json
new file mode 100644
index 0000000..0f8002d
--- /dev/null
+++ b/composer.json
@@ -0,0 +1,46 @@
+{
+ "name": "abacaphiliac/aws-sdk-php-claim-check",
+ "description": "Claim Check enterprise integration pattern, implemented via AWS PHP SDK.",
+ "minimum-stability": "stable",
+ "license": "proprietary",
+ "authors": [
+ {
+ "name": "Timothy Younger",
+ "email": "tim@webpt.com"
+ }
+ ],
+ "autoload": {
+ "psr-4": {
+ "Abacaphiliac\\AwsSdk\\ClaimCheck\\": "src/ClaimCheck"
+ }
+ },
+ "autoload-dev": {
+ "psr-4": {
+ "AbacaphiliacFeature\\AwsSdk\\ClaimCheck\\Bootstrap\\": "features/Bootstrap"
+ }
+ },
+ "require": {
+ "aws/aws-sdk-php": "^3",
+ "ramsey/uuid": "^3",
+ "zendframework/zend-json": "^2"
+ },
+ "require-dev": {
+ "phing/phing": "^2",
+ "phpunit/phpunit": "^5|^4",
+ "behat/behat": "^3",
+ "vlucas/phpdotenv": "^2",
+ "ircmaxell/random-lib": "^1",
+ "jakub-onderka/php-parallel-lint": "^0.9.2",
+ "squizlabs/php_codesniffer": "^2.6"
+ },
+ "keywords": [
+ "aws",
+ "sdk",
+ "php",
+ "claim check",
+ "enterprise integration pattern",
+ "sqs",
+ "sns",
+ "s3"
+ ]
+}
diff --git a/features/Bootstrap/ContextTrait/AwsConfigContextTrait.php b/features/Bootstrap/ContextTrait/AwsConfigContextTrait.php
new file mode 100644
index 0000000..78bd4dd
--- /dev/null
+++ b/features/Bootstrap/ContextTrait/AwsConfigContextTrait.php
@@ -0,0 +1,28 @@
+overloadEnvironmentVariables();
+
+ return [
+ 'region' => getenv('AWS_REGION') ?: 'us-west-2',
+ 'version' => getenv('AWS_VERSION') ?: 'latest',
+ 'credentials' => [
+ 'key' => getenv('AWS_KEY') ?: 'foo',
+ 'secret' => getenv('AWS_SECRET') ?: 'bar',
+ ],
+ ];
+ }
+}
diff --git a/features/Bootstrap/ContextTrait/DotEnvContextTrait.php b/features/Bootstrap/ContextTrait/DotEnvContextTrait.php
new file mode 100644
index 0000000..09fca74
--- /dev/null
+++ b/features/Bootstrap/ContextTrait/DotEnvContextTrait.php
@@ -0,0 +1,32 @@
+createDotEnv()->load();
+ }
+
+ /**
+ * @return mixed[]
+ */
+ public function overloadEnvironmentVariables()
+ {
+ return $this->createDotEnv()->overload();
+ }
+
+ /**
+ * @return Dotenv
+ */
+ private function createDotEnv()
+ {
+ return new Dotenv(__DIR__ . '/../../../', '.env');
+ }
+}
diff --git a/features/Bootstrap/ContextTrait/S3ContextTrait.php b/features/Bootstrap/ContextTrait/S3ContextTrait.php
new file mode 100644
index 0000000..bac243a
--- /dev/null
+++ b/features/Bootstrap/ContextTrait/S3ContextTrait.php
@@ -0,0 +1,68 @@
+s3Client) {
+ $config = $this->getAwsServiceConfig();
+
+ $this->s3Client = new S3Client($config);
+ }
+
+ return $this->s3Client;
+ }
+
+ /**
+ * @Given /^a data store named "([^"]*)"$/
+ * @param string $name
+ * @return string
+ * @throws \InvalidArgumentException
+ * @throws \Aws\S3\Exception\S3Exception
+ * @throws \Exception
+ */
+ public function getS3BucketFixture($name)
+ {
+ $this->getS3Client()->headBucket(array(
+ 'Bucket' => $name,
+ ));
+
+ $this->bucketName = $name;
+ }
+
+ /**
+ * @return string
+ */
+ public function getBucketName()
+ {
+ return $this->bucketName;
+ }
+
+ /**
+ * @return ClaimCheckFactory
+ */
+ public function createClaimCheckFactory()
+ {
+ return new ClaimCheckFactory($this->bucketName);
+ }
+}
diff --git a/features/Bootstrap/ContextTrait/SnsContextTrait.php b/features/Bootstrap/ContextTrait/SnsContextTrait.php
new file mode 100644
index 0000000..cb9ab57
--- /dev/null
+++ b/features/Bootstrap/ContextTrait/SnsContextTrait.php
@@ -0,0 +1,172 @@
+snsClient) {
+ $config = $this->getAwsServiceConfig();
+ $client = new SnsClient($config);
+ $this->snsClient = new SnsExtendedClient($client, $this->createSnsExtendedClientConfiguration());
+ }
+
+ return $this->snsClient;
+ }
+
+ /**
+ * @return SnsExtendedClientConfiguration
+ * @throws ExceptionInterface
+ */
+ private function createSnsExtendedClientConfiguration()
+ {
+ $s3Client = $this->getS3Client();
+
+ $s3BucketName = $this->getBucketName();
+
+ return new SnsExtendedClientConfiguration($s3Client, $s3BucketName);
+ }
+
+ /**
+ * @Given /^a topic named "([^"]*)"$/
+ * @param string $name
+ * @return string
+ * @throws ExceptionInterface
+ * @throws \InvalidArgumentException
+ */
+ public function getTopicFixture($name)
+ {
+ $this->topicArn = $this->createTopicFixture($name);
+
+ return $this->topicArn;
+ }
+
+ /**
+ * @param string $name
+ * @return string
+ * @throws ExceptionInterface
+ * @throws \InvalidArgumentException
+ */
+ public function createTopicFixture($name)
+ {
+ $result = $this->getSnsClient()->createTopic([
+ 'Name' => $name,
+ ]);
+
+ return $result->get('TopicArn');
+ }
+
+ /**
+ * @Given /^a queue named "([^"]*)" is subscribed to a topic named "([^"]*)"$/
+ * @param string $queueName
+ * @param string $topicName
+ * @throws ExceptionInterface
+ * @throws \InvalidArgumentException
+ * @throws \Zend\Json\Exception\RuntimeException
+ */
+ public function aQueueNamedIsSubscribedToATopicNamed($queueName, $topicName)
+ {
+ $topicArn = $this->createTopicFixture($topicName);
+
+ $queueUrl = $this->getQueueUrl($queueName);
+
+ $queueArn = $this->getSqsClient()->getQueueArn($queueUrl);
+
+ // Subscribe SQS Queue to SNS Topic.
+ $this->getSnsClient()->subscribe([
+ 'TopicArn' => $topicArn,
+ 'Protocol' => 'sqs',
+ 'Endpoint' => $queueArn,
+ ]);
+ }
+
+ /**
+ * @When /^I send the message to a topic named "([^"]*)"$/
+ * @param string $name
+ * @return Result
+ * @throws ExceptionInterface
+ * @throws \InvalidArgumentException
+ */
+ public function iSendTheMessageToATopicNamed($name)
+ {
+ $sqsExtendedClientConfiguration = $this->getSqsExtendedClientConfiguration();
+
+ // Leaky abstraction???
+ // We probably shouldn't be able to change the behavior of the extended-client after it is created.
+ $sqsExtendedClientConfiguration->setDeleteFromS3(false);
+
+ $topicArn = $this->createTopicFixture($name);
+
+ $result = $this->getSnsClient()->publish([
+ 'TopicArn' => $topicArn,
+ 'Message' => $this->getMessage(),
+ ]);
+
+ return $result;
+ }
+
+}
diff --git a/features/Bootstrap/ContextTrait/SqsContextTrait.php b/features/Bootstrap/ContextTrait/SqsContextTrait.php
new file mode 100644
index 0000000..417b1c9
--- /dev/null
+++ b/features/Bootstrap/ContextTrait/SqsContextTrait.php
@@ -0,0 +1,185 @@
+sqsClient) {
+ $config = $this->getAwsServiceConfig();
+ $client = new SqsClient($config);
+ $this->sqsClient = new SqsExtendedClient($client, $this->getSqsExtendedClientConfiguration());
+ }
+
+ return $this->sqsClient;
+ }
+
+ /**
+ * @return SqsExtendedClientConfiguration
+ * @throws ExceptionInterface
+ */
+ public function getSqsExtendedClientConfiguration()
+ {
+ if (!$this->sqsExtendedClientConfiguration) {
+ $s3Client = $this->getS3Client();
+
+ $s3BucketName = $this->getBucketName();
+
+ $this->sqsExtendedClientConfiguration = new SqsExtendedClientConfiguration($s3Client, $s3BucketName);
+ }
+
+ return $this->sqsExtendedClientConfiguration;
+ }
+
+ /**
+ * @Given /^a queue named "([^"]*)"$/
+ * @param string $name
+ * @return string
+ * @throws ExceptionInterface
+ */
+ public function getQueueFixture($name)
+ {
+ $this->queueUrl = $this->getQueueUrl($name);
+
+ return $this->queueUrl;
+ }
+
+ /**
+ * @param string $name
+ * @return string|bool
+ * @throws ExceptionInterface
+ */
+ public function getQueueUrl($name)
+ {
+ if (array_key_exists($name, $this->queueUrls)) {
+ return $this->queueUrls[$name];
+ }
+
+ $result = $this->getSqsClient()->getQueueUrl([
+ 'QueueName' => $name,
+ ]);
+
+ $this->queueUrls[$name] = $result->get('QueueUrl');
+
+ return $this->queueUrls[$name];
+ }
+
+ /**
+ * @param string $name
+ * @return string
+ * @throws ExceptionInterface
+ */
+ public function getQueueArn($name)
+ {
+ $queueUrl = $this->getQueueUrl($name);
+
+ return $this->getSqsClient()->getQueueArn($queueUrl);
+ }
+
+ /**
+ * @When /^I send the message to a queue named "([^"]*)"$/
+ * @param string $name
+ * @return Result
+ * @throws ExceptionInterface
+ */
+ public function iSendTheMessageToQueue($name)
+ {
+ $sqsClient = $this->getSqsClient();
+ $queueUrl = $this->getQueueUrl($name);
+ $message = $this->getMessage();
+
+ return $sqsClient->sendMessage([
+ 'QueueUrl' => $queueUrl,
+ 'MessageBody' => $message,
+ ]);
+ }
+
+ /**
+ * @Then /^I can fetch the message from a queue named "([^"]*)"$/
+ * @param string $name
+ * @return Result
+ * @throws ExceptionInterface
+ * @throws \RuntimeException
+ * @throws \PHPUnit_Framework_AssertionFailedError
+ */
+ public function iCanFetchTheMessageFromQueue($name)
+ {
+ $expectedMessage = $this->getMessage();
+ \PHPUnit_Framework_Assert::assertNotEmpty($expectedMessage);
+
+ $sqsClient = $this->getSqsClient();
+
+ $queueUrl = $this->getQueueUrl($name);
+
+ $result = $sqsClient->receiveMessage([
+ 'QueueUrl' => $queueUrl,
+ 'MaxNumberOfMessages' => 1,
+ ]);
+
+ $actualMessage = $result->search('Messages[].Body|[0]');
+ \PHPUnit_Framework_Assert::assertNotEmpty($actualMessage);
+
+ \PHPUnit_Framework_Assert::assertEquals(
+ $expectedMessage,
+ $actualMessage,
+ 'Purge the queues before running the feature suite.'
+ );
+
+ $sqsClient->deleteMessage([
+ 'QueueUrl' => $queueUrl,
+ 'ReceiptHandle' => $result->search('Messages[].ReceiptHandle|[0]'),
+ ]);
+
+ return $result;
+ }
+}
diff --git a/features/Bootstrap/S3DataStoreContext.php b/features/Bootstrap/S3DataStoreContext.php
new file mode 100644
index 0000000..364b03e
--- /dev/null
+++ b/features/Bootstrap/S3DataStoreContext.php
@@ -0,0 +1,89 @@
+message = gmdate('c') . PHP_EOL . $this->getLargeMessage();
+ }
+
+ /**
+ * @Given /^I have a small message$/
+ */
+ public function iHaveASmallMessage()
+ {
+ $this->message = gmdate('c') . PHP_EOL . 'Hello, World!';
+ }
+
+ /**
+ * @Given /^I have a small message that contains PHI$/
+ */
+ public function iHaveASmallMessageThatContainsPhi()
+ {
+ throw new PendingException();
+ }
+
+ /**
+ * @return string
+ */
+ public function getLargeMessage()
+ {
+ if (!$this->largeMessage) {
+ $filename = sys_get_temp_dir() . DIRECTORY_SEPARATOR . md5(__METHOD__);
+ if (file_exists($filename)) {
+ // TODO Check file content length.
+
+ return file_get_contents($filename);
+ }
+
+ $factory = new Factory();
+ $generator = $factory->getLowStrengthGenerator();
+ $this->largeMessage = $generator->generateString($this->largeMessageLength);
+
+ file_put_contents($filename, $this->largeMessage);
+ }
+
+ return $this->largeMessage;
+ }
+
+ /**
+ * @return string
+ */
+ public function getMessage()
+ {
+ return $this->message;
+ }
+}
diff --git a/features/fixtures/terraform/.gitignore b/features/fixtures/terraform/.gitignore
new file mode 100644
index 0000000..87804ab
--- /dev/null
+++ b/features/fixtures/terraform/.gitignore
@@ -0,0 +1,2 @@
+*override.tf
+*.tfstate*
diff --git a/features/fixtures/terraform/fixtures.tf b/features/fixtures/terraform/fixtures.tf
new file mode 100644
index 0000000..80327c5
--- /dev/null
+++ b/features/fixtures/terraform/fixtures.tf
@@ -0,0 +1,88 @@
+
+# apply AWS credentials.
+provider "aws" {
+ access_key = "${var.aws_access_key}"
+ secret_key = "${var.aws_secret_key}"
+ region = "${var.aws_region}"
+}
+
+# create S3 bucket.
+resource "aws_s3_bucket" "php-sdk-claim-check" {
+ bucket = "php-sdk-claim-check"
+ acl = "private"
+ force_destroy = true
+}
+
+# create SNS topic.
+resource "aws_sns_topic" "ClaimCheckTopic" {
+ name = "ClaimCheckTopic"
+}
+
+# create SQS queue.
+resource "aws_sqs_queue" "ClaimCheckQueue" {
+ name = "ClaimCheckQueue"
+
+ # create Access Policy to allow SNS topic to broadcast message to this SQS queue.
+ policy = <
+
+
+
+ tests
+
+
+
+
+ src
+
+
+
diff --git a/src/ClaimCheck/ClaimCheck.php b/src/ClaimCheck/ClaimCheck.php
new file mode 100644
index 0000000..4ca7bdf
--- /dev/null
+++ b/src/ClaimCheck/ClaimCheck.php
@@ -0,0 +1,46 @@
+s3BucketName = $s3BucketName;
+
+ if (!$s3Key) {
+ $s3Key = Uuid::uuid4()->toString();
+ }
+
+ $this->s3Key = $s3Key;
+ }
+
+ /**
+ * @return string
+ */
+ public function getS3BucketName()
+ {
+ return $this->s3BucketName;
+ }
+
+ /**
+ * @return string
+ */
+ public function getS3Key()
+ {
+ return $this->s3Key;
+ }
+}
diff --git a/src/ClaimCheck/ClaimCheckFactory.php b/src/ClaimCheck/ClaimCheckFactory.php
new file mode 100644
index 0000000..accd393
--- /dev/null
+++ b/src/ClaimCheck/ClaimCheckFactory.php
@@ -0,0 +1,27 @@
+s3BucketName = $s3BucketName;
+ }
+
+ /**
+ * @param string $s3Key
+ * @return ClaimCheck
+ */
+ public function create($s3Key = null)
+ {
+ return new ClaimCheck($this->s3BucketName, $s3Key);
+ }
+}
diff --git a/src/ClaimCheck/ClaimCheckFactoryInterface.php b/src/ClaimCheck/ClaimCheckFactoryInterface.php
new file mode 100644
index 0000000..c84af02
--- /dev/null
+++ b/src/ClaimCheck/ClaimCheckFactoryInterface.php
@@ -0,0 +1,12 @@
+ $claimCheck->getS3BucketName(),
+ 's3Key' => $claimCheck->getS3Key(),
+ ));
+ }
+
+ /**
+ * @param string $encodedValue
+ * @return ClaimCheck
+ * @throws ExceptionInterface
+ */
+ public function unserialize($encodedValue)
+ {
+ try {
+ $data = Json::decode($encodedValue, Json::TYPE_ARRAY);
+ } catch (RuntimeException $e) {
+ throw new InvalidArgumentException($e->getMessage(), 0, $e);
+ }
+
+ foreach (array('s3BucketName', 's3Key') as $param) {
+ if (!array_key_exists($param, $data) || !$data[$param]) {
+ throw new InvalidArgumentException(sprintf('Param %s is required and cannot be empty.', $param));
+ }
+ }
+
+ return new ClaimCheck($data['s3BucketName'], $data['s3Key']);
+ }
+}
diff --git a/src/ClaimCheck/Serializer/ClaimCheckSerializerChain.php b/src/ClaimCheck/Serializer/ClaimCheckSerializerChain.php
new file mode 100644
index 0000000..cb6e09f
--- /dev/null
+++ b/src/ClaimCheck/Serializer/ClaimCheckSerializerChain.php
@@ -0,0 +1,76 @@
+setSerializers($serializers);
+ }
+
+ /**
+ * @param ClaimCheck $claimCheck
+ * @return string
+ * @throws ExceptionInterface
+ */
+ public function serialize(ClaimCheck $claimCheck)
+ {
+ foreach ($this->serializers as $serializer) {
+ try {
+ return $serializer->serialize($claimCheck);
+ } catch (ExceptionInterface $e) {
+ continue;
+ }
+ }
+
+ throw new InvalidArgumentException('Failed to serialize Claim Check.');
+ }
+
+ /**
+ * @param string $encodedValue
+ * @return ClaimCheck
+ * @throws ExceptionInterface
+ */
+ public function unserialize($encodedValue)
+ {
+ foreach ($this->serializers as $serializer) {
+ try {
+ return $serializer->unserialize($encodedValue);
+ } catch (ExceptionInterface $e) {
+ continue;
+ }
+ }
+
+ throw new InvalidArgumentException('Failed to hydrate Claim Check from message: ' . $encodedValue);
+ }
+
+ /**
+ * @param ClaimCheckSerializerInterface[] $serializers
+ */
+ private function setSerializers(array $serializers)
+ {
+ $this->serializers = array();
+
+ array_map(array($this, 'addSerializer'), $serializers);
+ }
+
+ /**
+ * @param ClaimCheckSerializerInterface $serializer
+ */
+ private function addSerializer(ClaimCheckSerializerInterface $serializer)
+ {
+ $this->serializers[] = $serializer;
+ }
+}
diff --git a/src/ClaimCheck/Serializer/ClaimCheckSerializerInterface.php b/src/ClaimCheck/Serializer/ClaimCheckSerializerInterface.php
new file mode 100644
index 0000000..1e39a26
--- /dev/null
+++ b/src/ClaimCheck/Serializer/ClaimCheckSerializerInterface.php
@@ -0,0 +1,23 @@
+serializer = new ClaimCheckJsonSerializer();
+ }
+
+ /**
+ * @param ClaimCheck $claimCheck
+ * @return string
+ * @throws ExceptionInterface
+ */
+ public function serialize(ClaimCheck $claimCheck)
+ {
+ return $this->serializer->serialize($claimCheck);
+ }
+
+ /**
+ * @param string $encodedValue
+ * @return ClaimCheck
+ * @throws ExceptionInterface
+ */
+ public function unserialize($encodedValue)
+ {
+ try {
+ $data = Json::decode($encodedValue, Json::TYPE_ARRAY);
+ } catch (RuntimeException $e) {
+ throw new InvalidArgumentException($e->getMessage(), 0, $e);
+ }
+
+ if (!array_key_exists('Message', $data)) {
+ throw new InvalidArgumentException('Message is required and cannot be empty.');
+ }
+
+ return $this->serializer->unserialize($data['Message']);
+ }
+}
diff --git a/src/ClaimCheck/Sns/SnsExtendedClient.php b/src/ClaimCheck/Sns/SnsExtendedClient.php
new file mode 100644
index 0000000..5f0775c
--- /dev/null
+++ b/src/ClaimCheck/Sns/SnsExtendedClient.php
@@ -0,0 +1,85 @@
+snsClient = $snsClient;
+ $this->configuration = $configuration;
+ }
+
+ /**
+ * @param string $name
+ * @param mixed[] $args
+ * @return Result|Promise
+ * @throws ExceptionInterface
+ */
+ public function __call($name, array $args)
+ {
+ $params = array_key_exists(0, $args) ? $args[0] : [];
+
+ if (strcasecmp($name, 'publish') === 0) {
+ return $this->publishClaimCheck($params);
+ }
+
+ return $this->snsClient->{$name}($params);
+ }
+
+ /**
+ * @param mixed[] $args
+ * @return Result
+ * @throws ExceptionInterface
+ */
+ private function publishClaimCheck(array $args = [])
+ {
+ $claimCheckSerializer = $this->configuration->getClaimCheckSerializer();
+
+ $message = array_key_exists('Message', $args) ? $args['Message'] : '';
+
+ $claimCheck = $this->storeMessageInS3($message);
+
+ $args['Message'] = $claimCheckSerializer->serialize($claimCheck);
+
+ return $this->snsClient->publish($args);
+ }
+
+ /**
+ * @param string $message
+ * @return ClaimCheck
+ */
+ private function storeMessageInS3($message)
+ {
+ $s3Client = $this->configuration->getS3Client();
+ $s3BucketName = $this->configuration->getS3BucketName();
+ $claimCheckFactory = $this->configuration->getClaimCheckFactory();
+
+ $claimCheck = $claimCheckFactory->create();
+
+ $s3Client->putObject([
+ 'Bucket' => $s3BucketName,
+ 'Key' => $claimCheck->getS3Key(),
+ 'Body' => $message,
+ ]);
+
+ return $claimCheck;
+ }
+}
diff --git a/src/ClaimCheck/Sns/SnsExtendedClientConfiguration.php b/src/ClaimCheck/Sns/SnsExtendedClientConfiguration.php
new file mode 100644
index 0000000..76fbd13
--- /dev/null
+++ b/src/ClaimCheck/Sns/SnsExtendedClientConfiguration.php
@@ -0,0 +1,113 @@
+s3Client = $s3Client;
+ $this->s3BucketName = $s3BucketName;
+ $this->claimCheckFactory = new ClaimCheckFactory($s3BucketName);
+ $this->claimCheckSerializer = new ClaimCheckSerializerChain(array(
+ new ClaimCheckJsonSerializer(),
+ new JsonSnsMessageSerializer(),
+ ));
+ }
+
+ /**
+ * @return S3Client
+ */
+ public function getS3Client()
+ {
+ return $this->s3Client;
+ }
+
+ /**
+ * @param S3Client $s3Client
+ */
+ public function setS3Client($s3Client)
+ {
+ $this->s3Client = $s3Client;
+ }
+
+ /**
+ * @return string
+ */
+ public function getS3BucketName()
+ {
+ return $this->s3BucketName;
+ }
+
+ /**
+ * @param string $s3BucketName
+ */
+ public function setS3BucketName($s3BucketName)
+ {
+ $this->s3BucketName = $s3BucketName;
+ }
+
+ /**
+ * @return ClaimCheckFactoryInterface
+ */
+ public function getClaimCheckFactory()
+ {
+ return $this->claimCheckFactory;
+ }
+
+ /**
+ * @param ClaimCheckFactoryInterface $claimCheckFactory
+ */
+ public function setClaimCheckFactory($claimCheckFactory)
+ {
+ $this->claimCheckFactory = $claimCheckFactory;
+ }
+
+ /**
+ * @return ClaimCheckSerializerInterface
+ */
+ public function getClaimCheckSerializer()
+ {
+ return $this->claimCheckSerializer;
+ }
+
+ /**
+ * @param ClaimCheckSerializerInterface $claimCheckSerializer
+ */
+ public function setClaimCheckSerializer(ClaimCheckSerializerInterface $claimCheckSerializer)
+ {
+ $this->claimCheckSerializer = $claimCheckSerializer;
+ }
+}
diff --git a/src/ClaimCheck/Sqs/SqsExtendedClient.php b/src/ClaimCheck/Sqs/SqsExtendedClient.php
new file mode 100644
index 0000000..c483a14
--- /dev/null
+++ b/src/ClaimCheck/Sqs/SqsExtendedClient.php
@@ -0,0 +1,231 @@
+sqsClient = $sqsClient;
+ $this->configuration = $configuration;
+ }
+
+ /**
+ * @param string $name
+ * @param mixed[] $args
+ * @return Result|Promise
+ * @throws ExceptionInterface
+ */
+ public function __call($name, array $args)
+ {
+ $params = array_key_exists(0, $args) ? $args[0] : [];
+
+ switch ($name) {
+ case 'sendMessage':
+ $result = $this->sendSqsMessage($params);
+ break;
+ case 'receiveMessage':
+ $result = $this->receiveSqsMessage($params);
+ break;
+ case 'deleteMessage':
+ $result = $this->deleteSqsMessage($params);
+ break;
+ default:
+ $result = $this->sqsClient->{$name}($params);
+ break;
+ }
+
+ return $result;
+ }
+
+ /**
+ * @param mixed[] $args
+ * @return Result
+ * @throws ExceptionInterface
+ */
+ private function sendSqsMessage(array $args = [])
+ {
+ $claimCheckSerializer = $this->configuration->getClaimCheckSerializer();
+
+ $message = array_key_exists('MessageBody', $args) ? $args['MessageBody'] : '';
+
+ $claimCheck = $this->storeMessageInS3($message);
+
+ $args['MessageBody'] = $claimCheckSerializer->serialize($claimCheck);
+
+ return $this->sqsClient->sendMessage($args);
+ }
+
+ /**
+ * @param mixed[] $args
+ * @return Result
+ * @throws ExceptionInterface
+ */
+ private function receiveSqsMessage(array $args = [])
+ {
+ $result = $this->sqsClient->receiveMessage($args);
+
+ $messages = array();
+
+ foreach ($result->search('Messages[]') as $i => $message) {
+ $messages[$i] = $this->decodeSqsMessage($message);
+ }
+
+ $result->offsetSet('Messages', $messages);
+
+ return $result;
+ }
+
+ /**
+ * @param mixed[] $args
+ * @return Result
+ * @throws ExceptionInterface
+ */
+ private function deleteSqsMessage(array $args = [])
+ {
+ if (array_key_exists('ReceiptHandle', $args) && $this->configuration->getDeleteFromS3()) {
+ // Split receipt handle into S3 and SQS information.
+ $decodedReceiptHandle = json_decode($args['ReceiptHandle'], true);
+ if (json_last_error() === JSON_ERROR_NONE) {
+ $s3Client = $this->configuration->getS3Client();
+
+ // Delete from S3.
+ $s3Client->deleteObject(array(
+ 'Bucket' => $decodedReceiptHandle['s3_bucket_name'],
+ 'Key' => $decodedReceiptHandle['s3_key'],
+ ));
+
+ // Adjust SQS args.
+ $args['ReceiptHandle'] = $decodedReceiptHandle['original_receipt_handle'];
+ }
+ }
+
+ return $this->sqsClient->deleteMessage($args);
+ }
+
+ /**
+ * @param mixed[] $message
+ * @return string|bool
+ * @throws ExceptionInterface
+ */
+ private function decodeSqsMessage(array $message)
+ {
+ if (!array_key_exists('Body', $message)) {
+ // Unknown message body. Skip processing.
+ return $message;
+ }
+
+ try {
+ $claimCheck = $this->configuration->getClaimCheckSerializer()->unserialize($message['Body']);
+ } catch (ExceptionInterface $e) {
+ // Unknown message body. Skip processing.
+ return $message;
+ }
+
+ if (!$claimCheck instanceof ClaimCheck) {
+ // Unknown message body. Skip processing.
+ return $message;
+ }
+
+ try {
+ $message['Body'] = $this->fetchClaimCheckFromS3($claimCheck);
+ } catch (ExceptionInterface $e) {
+ // Unknown message body. Skip processing.
+ return $message;
+ }
+
+ if (array_key_exists('ReceiptHandle', $message) && $this->configuration->getDeleteFromS3()) {
+ // Prepend S3 information to receipt handle.
+ $message['ReceiptHandle'] = $this->embedS3PointerInReceiptHandle(
+ $message['ReceiptHandle'],
+ $claimCheck->getS3BucketName(),
+ $claimCheck->getS3Key()
+ );
+ }
+
+ return $message;
+ }
+
+ /**
+ * @param string $message
+ * @return ClaimCheck
+ */
+ private function storeMessageInS3($message)
+ {
+ $s3Client = $this->configuration->getS3Client();
+ $s3BucketName = $this->configuration->getS3BucketName();
+ $claimCheckFactory = $this->configuration->getClaimCheckFactory();
+
+ $claimCheck = $claimCheckFactory->create();
+
+ $s3Client->putObject([
+ 'Bucket' => $s3BucketName,
+ 'Key' => $claimCheck->getS3Key(),
+ 'Body' => $message,
+ ]);
+
+ return $claimCheck;
+ }
+
+ /**
+ * @param ClaimCheck $claimCheck
+ * @return string
+ * @throws ExceptionInterface
+ */
+ private function fetchClaimCheckFromS3(ClaimCheck $claimCheck)
+ {
+ $s3Client = $this->configuration->getS3Client();
+
+ $result = $s3Client->getObject([
+ 'Bucket' => $claimCheck->getS3BucketName(),
+ 'Key' => $claimCheck->getS3Key(),
+ ]);
+
+ $body = $result->get('Body');
+
+ // Unpack the message.
+ if ($body instanceof StreamInterface) {
+ try {
+ return $body->getContents();
+ } catch (\RuntimeException $e) {
+ throw new RuntimeException($e->getMessage(), 0, $e);
+ }
+ }
+
+ return $body;
+ }
+
+ /**
+ * @param string $receiptHandle
+ * @param string $s3MsgBucketName
+ * @param string $s3MsgKey
+ * @return string
+ */
+ private function embedS3PointerInReceiptHandle($receiptHandle, $s3MsgBucketName, $s3MsgKey)
+ {
+ return json_encode(array(
+ 'original_receipt_handle' => $receiptHandle,
+ 's3_bucket_name' => $s3MsgBucketName,
+ 's3_key' => $s3MsgKey,
+ ));
+ }
+}
diff --git a/src/ClaimCheck/Sqs/SqsExtendedClientConfiguration.php b/src/ClaimCheck/Sqs/SqsExtendedClientConfiguration.php
new file mode 100644
index 0000000..5181e69
--- /dev/null
+++ b/src/ClaimCheck/Sqs/SqsExtendedClientConfiguration.php
@@ -0,0 +1,132 @@
+s3Client = $s3Client;
+ $this->s3BucketName = $s3BucketName;
+ $this->claimCheckFactory = new ClaimCheckFactory($s3BucketName);
+ $this->claimCheckSerializer = new ClaimCheckSerializerChain(array(
+ new ClaimCheckJsonSerializer(),
+ new JsonSnsMessageSerializer(),
+ ));
+ }
+
+ /**
+ * @return S3Client
+ */
+ public function getS3Client()
+ {
+ return $this->s3Client;
+ }
+
+ /**
+ * @param S3Client $s3Client
+ */
+ public function setS3Client($s3Client)
+ {
+ $this->s3Client = $s3Client;
+ }
+
+ /**
+ * @return string
+ */
+ public function getS3BucketName()
+ {
+ return $this->s3BucketName;
+ }
+
+ /**
+ * @param string $s3BucketName
+ */
+ public function setS3BucketName($s3BucketName)
+ {
+ $this->s3BucketName = $s3BucketName;
+ }
+
+ /**
+ * @return ClaimCheckFactoryInterface
+ */
+ public function getClaimCheckFactory()
+ {
+ return $this->claimCheckFactory;
+ }
+
+ /**
+ * @param ClaimCheckFactoryInterface $claimCheckFactory
+ */
+ public function setClaimCheckFactory($claimCheckFactory)
+ {
+ $this->claimCheckFactory = $claimCheckFactory;
+ }
+
+ /**
+ * @return ClaimCheckSerializerInterface
+ */
+ public function getClaimCheckSerializer()
+ {
+ return $this->claimCheckSerializer;
+ }
+
+ /**
+ * @param ClaimCheckSerializerInterface $claimCheckSerializer
+ */
+ public function setClaimCheckSerializer(ClaimCheckSerializerInterface $claimCheckSerializer)
+ {
+ $this->claimCheckSerializer = $claimCheckSerializer;
+ }
+
+ /**
+ * @return boolean
+ */
+ public function getDeleteFromS3()
+ {
+ return $this->deleteFromS3;
+ }
+
+ /**
+ * @param boolean $deleteFromS3
+ */
+ public function setDeleteFromS3($deleteFromS3)
+ {
+ $this->deleteFromS3 = $deleteFromS3;
+ }
+}
diff --git a/tests/ClaimCheckFactoryTest.php b/tests/ClaimCheckFactoryTest.php
new file mode 100644
index 0000000..c6d01c6
--- /dev/null
+++ b/tests/ClaimCheckFactoryTest.php
@@ -0,0 +1,18 @@
+create();
+
+ self::assertEquals($expected, $actual->getS3BucketName());
+ self::assertNotEmpty($actual->getS3Key());
+ }
+}
diff --git a/tests/ClaimCheckTest.php b/tests/ClaimCheckTest.php
new file mode 100644
index 0000000..bab88db
--- /dev/null
+++ b/tests/ClaimCheckTest.php
@@ -0,0 +1,29 @@
+getS3BucketName());
+ }
+
+ public function testGetDefaultKey()
+ {
+ $sut = new ClaimCheck('MyBucket');
+
+ self::assertNotEmpty($sut->getS3Key());
+ }
+
+ public function testGetKey()
+ {
+ $sut = new ClaimCheck('MyBucket', $expected = 'MyKey');
+
+ self::assertEquals($expected, $sut->getS3Key());
+ }
+}
diff --git a/tests/Serializer/ClaimCheckJsonSerializerTest.php b/tests/Serializer/ClaimCheckJsonSerializerTest.php
new file mode 100644
index 0000000..594756e
--- /dev/null
+++ b/tests/Serializer/ClaimCheckJsonSerializerTest.php
@@ -0,0 +1,80 @@
+sut = new ClaimCheckJsonSerializer();
+ }
+
+ public function testSerialize()
+ {
+ $claimCheck = new ClaimCheck('MyBucket', 'MyKey');
+
+ $encodedValue = $this->sut->serialize($claimCheck);
+
+ $actual = Json::decode($encodedValue, Json::TYPE_ARRAY);
+
+ self::assertArrayHasKey('s3BucketName', $actual);
+ self::assertEquals('MyBucket', $actual['s3BucketName']);
+ self::assertArrayHasKey('s3Key', $actual);
+ self::assertEquals('MyKey', $actual['s3Key']);
+ }
+
+ public function testUnserialize()
+ {
+ $encodedValue = Json::encode([
+ 's3BucketName' => 'MyBucket',
+ 's3Key' => 'MyKey',
+ ]);
+
+ $actual = $this->sut->unserialize($encodedValue);
+
+ self::assertEquals('MyBucket', $actual->getS3BucketName());
+ self::assertEquals('MyKey', $actual->getS3Key());
+ }
+
+ /**
+ * @throws ExceptionInterface
+ * @expectedException \Abacaphiliac\AwsSdk\ClaimCheck\Exception\ExceptionInterface
+ */
+ public function testUnserializeInvalidJson()
+ {
+ $this->sut->unserialize('StuffAndThings');
+ }
+
+ /**
+ * @throws ExceptionInterface
+ * @expectedException \Abacaphiliac\AwsSdk\ClaimCheck\Exception\ExceptionInterface
+ */
+ public function testUnserializeClaimCheckMissingBucketName()
+ {
+ $this->sut->unserialize(Json::encode([
+ 's3Key' => 'MyKey',
+ ]));
+ }
+
+ /**
+ * @throws ExceptionInterface
+ * @expectedException \Abacaphiliac\AwsSdk\ClaimCheck\Exception\ExceptionInterface
+ */
+ public function testUnserializeClaimCheckMissingKey()
+ {
+ $this->sut->unserialize(Json::encode([
+ 's3BucketName' => 'MyBucket',
+ ]));
+ }
+}
diff --git a/tests/Serializer/ClaimCheckSerializerChainTest.php b/tests/Serializer/ClaimCheckSerializerChainTest.php
new file mode 100644
index 0000000..6951f9b
--- /dev/null
+++ b/tests/Serializer/ClaimCheckSerializerChainTest.php
@@ -0,0 +1,119 @@
+sut = new ClaimCheckSerializerChain([
+ $this->firstSerializer = $this->getMock(ClaimCheckSerializerInterface::class),
+ $this->secondSerializer = $this->getMock(ClaimCheckSerializerInterface::class),
+ ]);
+ }
+
+ public function testSerializeViaFirstSerializer()
+ {
+ $claimCheck = new ClaimCheck('MyBucket', 'MyKey');
+
+ $this->firstSerializer->method('serialize')
+ ->willReturn($expected = 'Serialized!');
+
+ $this->secondSerializer->method('serialize')
+ ->willThrowException(new InvalidArgumentException());
+
+ $actual = $this->sut->serialize($claimCheck);
+
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testSerializeViaSecondSerializer()
+ {
+ $claimCheck = new ClaimCheck('MyBucket', 'MyKey');
+
+ $this->firstSerializer->method('serialize')
+ ->willThrowException(new InvalidArgumentException());
+
+ $this->secondSerializer->method('serialize')
+ ->willReturn($expected = 'Serialized!');
+
+ $actual = $this->sut->serialize($claimCheck);
+
+ self::assertEquals($expected, $actual);
+ }
+
+ /**
+ * @throws ExceptionInterface
+ * @expectedException \Abacaphiliac\AwsSdk\ClaimCheck\Exception\ExceptionInterface
+ */
+ public function testNotSerialize()
+ {
+ $claimCheck = new ClaimCheck('MyBucket', 'MyKey');
+
+ $this->firstSerializer->method('serialize')
+ ->willThrowException(new InvalidArgumentException());
+
+ $this->secondSerializer->method('serialize')
+ ->willThrowException(new InvalidArgumentException());
+
+ $this->sut->serialize($claimCheck);
+ }
+
+ public function testUnserializeViaFirstSerializer()
+ {
+ $this->firstSerializer->method('unserialize')
+ ->willReturn($expected = new ClaimCheck('MyBucket', 'MyKey'));
+
+ $this->secondSerializer->method('unserialize')
+ ->willThrowException(new InvalidArgumentException());
+
+ $actual = $this->sut->unserialize('Serialized!');
+
+ self::assertEquals($expected, $actual);
+ }
+
+ public function testUnserializeViaSecondSerializer()
+ {
+ $this->firstSerializer->method('unserialize')
+ ->willThrowException(new InvalidArgumentException());
+
+ $this->secondSerializer->method('unserialize')
+ ->willReturn($expected = new ClaimCheck('MyBucket', 'MyKey'));
+
+ $actual = $this->sut->unserialize('Serialized!');
+
+ self::assertEquals($expected, $actual);
+ }
+
+ /**
+ * @throws ExceptionInterface
+ * @expectedException \Abacaphiliac\AwsSdk\ClaimCheck\Exception\ExceptionInterface
+ */
+ public function testNotUnserialize()
+ {
+ $this->firstSerializer->method('unserialize')
+ ->willThrowException(new InvalidArgumentException());
+
+ $this->secondSerializer->method('unserialize')
+ ->willThrowException(new InvalidArgumentException());
+
+ $this->sut->unserialize('Serialized!');
+ }
+}
diff --git a/tests/Serializer/JsonSnsMessageSerializerTest.php b/tests/Serializer/JsonSnsMessageSerializerTest.php
new file mode 100644
index 0000000..4baff32
--- /dev/null
+++ b/tests/Serializer/JsonSnsMessageSerializerTest.php
@@ -0,0 +1,72 @@
+sut = new JsonSnsMessageSerializer();
+ }
+
+ public function testSerialize()
+ {
+ $claimCheck = new ClaimCheck('MyBucket', 'MyKey');
+
+ $encodedValue = $this->sut->serialize($claimCheck);
+
+ $actual = Json::decode($encodedValue, Json::TYPE_ARRAY);
+
+ self::assertArrayHasKey('s3BucketName', $actual);
+ self::assertEquals('MyBucket', $actual['s3BucketName']);
+ self::assertArrayHasKey('s3Key', $actual);
+ self::assertEquals('MyKey', $actual['s3Key']);
+ }
+
+ public function testUnserialize()
+ {
+ $encodedValue = Json::encode([
+ 'Message' => Json::encode([
+ 's3BucketName' => 'MyBucket',
+ 's3Key' => 'MyKey',
+ ]),
+ ]);
+
+ $actual = $this->sut->unserialize($encodedValue);
+
+ self::assertEquals('MyBucket', $actual->getS3BucketName());
+ self::assertEquals('MyKey', $actual->getS3Key());
+ }
+
+ /**
+ * @throws ExceptionInterface
+ * @expectedException \Abacaphiliac\AwsSdk\ClaimCheck\Exception\ExceptionInterface
+ */
+ public function testUnserializeInvalidJson()
+ {
+ $this->sut->unserialize('StuffAndThings');
+ }
+
+ /**
+ * @throws ExceptionInterface
+ * @expectedException \Abacaphiliac\AwsSdk\ClaimCheck\Exception\ExceptionInterface
+ */
+ public function testUnserializeClaimCheckMissingBucketName()
+ {
+ $this->sut->unserialize(Json::encode([
+ 's3BucketName' => 'MyBucketName',
+ 's3Key' => 'MyKey',
+ ]));
+ }
+}
diff --git a/tests/Sns/SnsExtendedClientConfigurationTest.php b/tests/Sns/SnsExtendedClientConfigurationTest.php
new file mode 100644
index 0000000..ffa64b9
--- /dev/null
+++ b/tests/Sns/SnsExtendedClientConfigurationTest.php
@@ -0,0 +1,93 @@
+s3Client = $this->getMockBuilder(S3Client::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->sut = new SnsExtendedClientConfiguration($this->s3Client, $this->s3BucketName);
+ }
+
+ public function testGetS3Client()
+ {
+ self::assertSame($this->s3Client, $this->sut->getS3Client());
+
+ /** @var S3Client $expected */
+ $expected = $this->getMockBuilder(S3Client::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->sut->setS3Client($expected);
+
+ self::assertSame($expected, $this->sut->getS3Client());
+ }
+
+ public function testGetS3BucketName()
+ {
+ self::assertEquals($this->s3BucketName, $this->sut->getS3BucketName());
+
+ $this->sut->setS3BucketName($expected = 'AnotherBucket');
+
+ self::assertSame($expected, $this->sut->getS3BucketName());
+ }
+
+ public function testGetClaimCheckFactory()
+ {
+ self::assertInstanceOf(ClaimCheckFactoryInterface::class, $this->sut->getClaimCheckFactory());
+
+ $this->sut->setClaimCheckFactory($expected = new ClaimCheckFactory('AnotherBucket'));
+
+ self::assertSame($expected, $this->sut->getClaimCheckFactory());
+ }
+
+ public function testGetClaimCheckSerializer()
+ {
+ self::assertInstanceOf(ClaimCheckSerializerInterface::class, $this->sut->getClaimCheckSerializer());
+
+ $this->sut->setClaimCheckSerializer($expected = new ClaimCheckSerializerChain());
+
+ self::assertSame($expected, $this->sut->getClaimCheckSerializer());
+ }
+
+ /**
+ * @throws ExceptionInterface
+ * @expectedException \Abacaphiliac\AwsSdk\ClaimCheck\Exception\ExceptionInterface
+ */
+ public function testNullIsInvalidBucketName()
+ {
+ new SnsExtendedClientConfiguration($this->s3Client, null);
+ }
+
+ /**
+ * @throws ExceptionInterface
+ * @expectedException \Abacaphiliac\AwsSdk\ClaimCheck\Exception\ExceptionInterface
+ */
+ public function testEmptyStringIsInvalidBucketName()
+ {
+ new SnsExtendedClientConfiguration($this->s3Client, '');
+ }
+}
diff --git a/tests/Sns/SnsExtendedClientTest.php b/tests/Sns/SnsExtendedClientTest.php
new file mode 100644
index 0000000..3d06906
--- /dev/null
+++ b/tests/Sns/SnsExtendedClientTest.php
@@ -0,0 +1,64 @@
+snsClient = $this->getMockBuilder(SnsClient::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->s3Client = $this->getMockBuilder(S3Client::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configuration = new SnsExtendedClientConfiguration($this->s3Client, $this->s3BucketName);
+
+ $this->sut = new SnsExtendedClient($this->snsClient, $this->configuration);
+ }
+
+ public function testPublishAlteredMessage()
+ {
+ $args = [
+ 'Message' => $originalMessage = 'MyOriginalMessage',
+ ];
+
+ $this->snsClient->method('__call')->with('publish')
+ ->willReturnCallback(function ($name, array $args) use ($originalMessage) {
+ $params = array_key_exists(0, $args) ? $args[0] : [];
+
+ \PHPUnit_Framework_Assert::assertNotEquals($originalMessage, $params['Message']);
+
+ return new Result();
+ });
+
+ $actual = $this->sut->publish($args);
+
+ self::assertInstanceOf(Result::class, $actual);
+ }
+}
diff --git a/tests/Sqs/SqsExtendedClientConfigurationTest.php b/tests/Sqs/SqsExtendedClientConfigurationTest.php
new file mode 100644
index 0000000..70e49f8
--- /dev/null
+++ b/tests/Sqs/SqsExtendedClientConfigurationTest.php
@@ -0,0 +1,102 @@
+s3Client = $this->getMockBuilder(S3Client::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->sut = new SqsExtendedClientConfiguration($this->s3Client, $this->s3BucketName);
+ }
+
+ public function testGetS3Client()
+ {
+ self::assertSame($this->s3Client, $this->sut->getS3Client());
+
+ /** @var S3Client $expected */
+ $expected = $this->getMockBuilder(S3Client::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->sut->setS3Client($expected);
+
+ self::assertSame($expected, $this->sut->getS3Client());
+ }
+
+ public function testGetS3BucketName()
+ {
+ self::assertEquals($this->s3BucketName, $this->sut->getS3BucketName());
+
+ $this->sut->setS3BucketName($expected = 'AnotherBucket');
+
+ self::assertSame($expected, $this->sut->getS3BucketName());
+ }
+
+ public function testGetClaimCheckFactory()
+ {
+ self::assertInstanceOf(ClaimCheckFactoryInterface::class, $this->sut->getClaimCheckFactory());
+
+ $this->sut->setClaimCheckFactory($expected = new ClaimCheckFactory('AnotherBucket'));
+
+ self::assertSame($expected, $this->sut->getClaimCheckFactory());
+ }
+
+ public function testGetClaimCheckSerializer()
+ {
+ self::assertInstanceOf(ClaimCheckSerializerInterface::class, $this->sut->getClaimCheckSerializer());
+
+ $this->sut->setClaimCheckSerializer($expected = new ClaimCheckSerializerChain());
+
+ self::assertSame($expected, $this->sut->getClaimCheckSerializer());
+ }
+
+ public function testDeleteFromS3()
+ {
+ self::assertTrue($this->sut->getDeleteFromS3());
+
+ $this->sut->setDeleteFromS3(false);
+
+ self::assertFalse($this->sut->getDeleteFromS3());
+ }
+
+ /**
+ * @throws ExceptionInterface
+ * @expectedException \Abacaphiliac\AwsSdk\ClaimCheck\Exception\ExceptionInterface
+ */
+ public function testNullIsInvalidBucketName()
+ {
+ new SqsExtendedClientConfiguration($this->s3Client, null);
+ }
+
+ /**
+ * @throws ExceptionInterface
+ * @expectedException \Abacaphiliac\AwsSdk\ClaimCheck\Exception\ExceptionInterface
+ */
+ public function testEmptyStringIsInvalidBucketName()
+ {
+ new SqsExtendedClientConfiguration($this->s3Client, '');
+ }
+}
diff --git a/tests/Sqs/SqsExtendedClientTest.php b/tests/Sqs/SqsExtendedClientTest.php
new file mode 100644
index 0000000..ab51f1f
--- /dev/null
+++ b/tests/Sqs/SqsExtendedClientTest.php
@@ -0,0 +1,175 @@
+sqsClient = $this->getMockBuilder(SqsClient::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->s3Client = $this->getMockBuilder(S3Client::class)
+ ->disableOriginalConstructor()
+ ->getMock();
+
+ $this->configuration = new SqsExtendedClientConfiguration($this->s3Client, $this->s3BucketName);
+
+ $this->sut = new SqsExtendedClient($this->sqsClient, $this->configuration);
+ }
+
+ public function testSendAlteredMessage()
+ {
+ $args = [
+ 'MessageBody' => $originalMessage = 'MyOriginalMessage',
+ ];
+
+ $this->sqsClient->method('__call')->with('sendMessage')
+ ->willReturnCallback(function ($name, array $args) use ($originalMessage) {
+ $params = array_key_exists(0, $args) ? $args[0] : [];
+
+ \PHPUnit_Framework_Assert::assertNotEquals($originalMessage, $params['MessageBody']);
+
+ return new Result();
+ });
+
+ $actual = $this->sut->sendMessage($args);
+
+ self::assertInstanceOf(Result::class, $actual);
+ }
+
+ public function testReceiveUnalteredMessage()
+ {
+ $expected = 'MyOriginalMessage';
+
+ $this->sqsClient->method('__call')->with('receiveMessage')
+ ->willReturnCallback(function ($name, array $args) use ($expected) {
+ return new Result([
+ 'Messages' => [
+ [
+ 'Body' => $expected,
+ ],
+ ],
+ ]);
+ });
+
+ $actual = $this->sut->receiveMessage();
+
+ self::assertInstanceOf(Result::class, $actual);
+
+ self::assertEquals($expected, $actual->search('Messages[].Body|[0]'));
+ }
+
+ public function testReceiveAlteredMessage()
+ {
+ $originalMessage = 'MyOriginalMessage';
+
+ $this->sqsClient->method('__call')->with('receiveMessage')
+ ->willReturnCallback(function ($name, array $args) {
+ $factory = new ClaimCheckFactory('MyBucket');
+ $serializer = new ClaimCheckJsonSerializer();
+ return new Result([
+ 'Messages' => [
+ [
+ 'Body' => $serializer->serialize($factory->create('MyKey')),
+ ],
+ ],
+ ]);
+ });
+
+ $this->s3Client->method('__call')->with('getObject')
+ ->willReturnCallback(function ($name, array $args) use ($originalMessage) {
+ return new Result([
+ 'Body' => $originalMessage,
+ ]);
+ });
+
+ $actual = $this->sut->receiveMessage();
+
+ self::assertInstanceOf(Result::class, $actual);
+
+ self::assertEquals($originalMessage, $actual->search('Messages[].Body|[0]'));
+ }
+
+ public function testReceiveAlteredReceiptHandle()
+ {
+ $this->sqsClient->method('__call')->with('receiveMessage')
+ ->willReturnCallback(function ($name, array $args) {
+ $factory = new ClaimCheckFactory('MyBucket');
+ $serializer = new ClaimCheckJsonSerializer();
+ return new Result([
+ 'Messages' => [
+ [
+ 'Body' => $serializer->serialize($factory->create('MyKey')),
+ 'ReceiptHandle' => 'MyReceiptHandle',
+ ],
+ ],
+ ]);
+ });
+
+ $this->s3Client->method('__call')->with('getObject')
+ ->willReturnCallback(function ($name, array $args) {
+ return new Result([
+ 'Body' => 'MyOriginalMessage',
+ ]);
+ });
+
+ $actual = $this->sut->receiveMessage();
+
+ self::assertInstanceOf(Result::class, $actual);
+
+ self::assertContains('MyReceiptHandle', $actual->search('Messages[].ReceiptHandle|[0]'));
+ self::assertContains('MyBucket', $actual->search('Messages[].ReceiptHandle|[0]'));
+ self::assertContains('MyKey', $actual->search('Messages[].ReceiptHandle|[0]'));
+ }
+
+ public function testDeleteFromS3()
+ {
+ $args = [
+ 'ReceiptHandle' => $modifiedReceiptHandle = json_encode([
+ 's3_bucket_name' => 'MyBucket',
+ 's3_key' => 'MyKey',
+ 'original_receipt_handle' => 'MyReceiptHandle',
+ ]),
+ ];
+
+ $this->sqsClient->method('__call')->with('deleteMessage')
+ ->willReturnCallback(function ($name, array $args) use ($modifiedReceiptHandle) {
+ $params = array_key_exists(0, $args) ? $args[0] : [];
+
+ \PHPUnit_Framework_Assert::assertNotEquals($modifiedReceiptHandle, $params['ReceiptHandle']);
+
+ return new Result();
+ });
+
+ $actual = $this->sut->deleteMessage($args);
+
+ self::assertInstanceOf(Result::class, $actual);
+ }
+}