From c594b2bce1788860cf4c52f957c650c7dcd3c994 Mon Sep 17 00:00:00 2001 From: Tim Younger Date: Mon, 11 Apr 2016 08:39:38 -0700 Subject: [PATCH] add extended SQS and SNS clients that store the original message in S3 and publish a claim check replacement message onto the message bus. the extended SQS client is able to fetch messages from S3 when it encounters a claim check sent directly to SQS or through SNS. --- .env.example | 5 + .gitignore | 6 + .travis.yml | 21 ++ README.md | 61 ++++- behat.yml.dist | 5 + build.xml | 87 +++++++ composer.json | 46 ++++ .../ContextTrait/AwsConfigContextTrait.php | 28 +++ .../ContextTrait/DotEnvContextTrait.php | 32 +++ .../Bootstrap/ContextTrait/S3ContextTrait.php | 68 ++++++ .../ContextTrait/SnsContextTrait.php | 172 +++++++++++++ .../ContextTrait/SqsContextTrait.php | 185 ++++++++++++++ features/Bootstrap/S3DataStoreContext.php | 89 +++++++ features/fixtures/terraform/.gitignore | 2 + features/fixtures/terraform/fixtures.tf | 88 +++++++ .../terraform/variables_override.tf.dist | 25 ++ .../s3-data-store/sns-claim-check.feature | 17 ++ .../s3-data-store/sqs-claim-check.feature | 12 + phpunit.xml.dist | 17 ++ src/ClaimCheck/ClaimCheck.php | 46 ++++ src/ClaimCheck/ClaimCheckFactory.php | 27 ++ src/ClaimCheck/ClaimCheckFactoryInterface.php | 12 + .../Exception/ExceptionInterface.php | 8 + .../Exception/InvalidArgumentException.php | 8 + src/ClaimCheck/Exception/RuntimeException.php | 8 + .../Serializer/ClaimCheckJsonSerializer.php | 47 ++++ .../Serializer/ClaimCheckSerializerChain.php | 76 ++++++ .../ClaimCheckSerializerInterface.php | 23 ++ .../Serializer/JsonSnsMessageSerializer.php | 53 ++++ src/ClaimCheck/Sns/SnsExtendedClient.php | 85 +++++++ .../Sns/SnsExtendedClientConfiguration.php | 113 +++++++++ src/ClaimCheck/Sqs/SqsExtendedClient.php | 231 ++++++++++++++++++ .../Sqs/SqsExtendedClientConfiguration.php | 132 ++++++++++ tests/ClaimCheckFactoryTest.php | 18 ++ tests/ClaimCheckTest.php | 29 +++ .../ClaimCheckJsonSerializerTest.php | 80 ++++++ .../ClaimCheckSerializerChainTest.php | 119 +++++++++ .../JsonSnsMessageSerializerTest.php | 72 ++++++ .../SnsExtendedClientConfigurationTest.php | 93 +++++++ tests/Sns/SnsExtendedClientTest.php | 64 +++++ .../SqsExtendedClientConfigurationTest.php | 102 ++++++++ tests/Sqs/SqsExtendedClientTest.php | 175 +++++++++++++ 42 files changed, 2586 insertions(+), 1 deletion(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 behat.yml.dist create mode 100644 build.xml create mode 100644 composer.json create mode 100644 features/Bootstrap/ContextTrait/AwsConfigContextTrait.php create mode 100644 features/Bootstrap/ContextTrait/DotEnvContextTrait.php create mode 100644 features/Bootstrap/ContextTrait/S3ContextTrait.php create mode 100644 features/Bootstrap/ContextTrait/SnsContextTrait.php create mode 100644 features/Bootstrap/ContextTrait/SqsContextTrait.php create mode 100644 features/Bootstrap/S3DataStoreContext.php create mode 100644 features/fixtures/terraform/.gitignore create mode 100644 features/fixtures/terraform/fixtures.tf create mode 100644 features/fixtures/terraform/variables_override.tf.dist create mode 100644 features/s3-data-store/sns-claim-check.feature create mode 100644 features/s3-data-store/sqs-claim-check.feature create mode 100644 phpunit.xml.dist create mode 100644 src/ClaimCheck/ClaimCheck.php create mode 100644 src/ClaimCheck/ClaimCheckFactory.php create mode 100644 src/ClaimCheck/ClaimCheckFactoryInterface.php create mode 100644 src/ClaimCheck/Exception/ExceptionInterface.php create mode 100644 src/ClaimCheck/Exception/InvalidArgumentException.php create mode 100644 src/ClaimCheck/Exception/RuntimeException.php create mode 100644 src/ClaimCheck/Serializer/ClaimCheckJsonSerializer.php create mode 100644 src/ClaimCheck/Serializer/ClaimCheckSerializerChain.php create mode 100644 src/ClaimCheck/Serializer/ClaimCheckSerializerInterface.php create mode 100644 src/ClaimCheck/Serializer/JsonSnsMessageSerializer.php create mode 100644 src/ClaimCheck/Sns/SnsExtendedClient.php create mode 100644 src/ClaimCheck/Sns/SnsExtendedClientConfiguration.php create mode 100644 src/ClaimCheck/Sqs/SqsExtendedClient.php create mode 100644 src/ClaimCheck/Sqs/SqsExtendedClientConfiguration.php create mode 100644 tests/ClaimCheckFactoryTest.php create mode 100644 tests/ClaimCheckTest.php create mode 100644 tests/Serializer/ClaimCheckJsonSerializerTest.php create mode 100644 tests/Serializer/ClaimCheckSerializerChainTest.php create mode 100644 tests/Serializer/JsonSnsMessageSerializerTest.php create mode 100644 tests/Sns/SnsExtendedClientConfigurationTest.php create mode 100644 tests/Sns/SnsExtendedClientTest.php create mode 100644 tests/Sqs/SqsExtendedClientConfigurationTest.php create mode 100644 tests/Sqs/SqsExtendedClientTest.php 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); + } +}