diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index c020e78..1e816fa 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -9,8 +9,10 @@ jobs: strategy: matrix: php: [7.4, 7.3] - laravel: [8.*] + laravel: [6.*, 8.*] include: + - laravel: 6.* + testbench: 4.* - laravel: 8.* testbench: 6.* diff --git a/.gitignore b/.gitignore index 21a92ba..4d8c462 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ .idea .php_cs.cache .phpunit.result.cache +composer.lock diff --git a/README.md b/README.md index 494f28e..56f42b7 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Introduction +# Laravel SNS Handler This package provides an easy way of adding AWS SNS message handling to your Laravel application as a REST endpoint. The package can automatically confirm subscription requests and dispatches events when a message is received. ## How to create an SNS topic @@ -75,3 +75,14 @@ This package adds a route to your application for incoming SNS requests. Note th **Note: You will only be able to subscribe your endpoint if it can be reached from the AWS SNS service** +## Development +This package is expected to work with supported versions of Laravel, including LTS releases. During development, you should be sure to run tests and validate expected behaviors under different releases. Since we use the `orchestra/testbench` package, you can easily switch between installed Laravel framework releases using `composer`: + +```bash +# Laravel 6 +composer require --dev orchestra/testbench:^4.0 -W +# Laravel 8 +composer require --dev orchestra/testbench:^6.0 -W +``` + +New releases of Laravel should be added to the GitHub workflow matrix in `.github/workflows/run-tests.yml`. diff --git a/composer.json b/composer.json index 02aebac..1351584 100644 --- a/composer.json +++ b/composer.json @@ -16,12 +16,16 @@ "require": { "php": ">=7.3", "ext-json": "*", - "aws/aws-php-sns-message-validator": "^1.6" + "aws/aws-php-sns-message-validator": "^1.6", + "php-http/discovery": "^1.14", + "psr/http-factory": "^1.0", + "psr/http-client": "^1.0" }, "require-dev" : { "roave/security-advisories": "dev-latest", - "guzzlehttp/guzzle": "^7.2", - "orchestra/testbench": "^6.0" + "orchestra/testbench": "^6.0", + "http-interop/http-factory-guzzle": "^1.2", + "php-http/guzzle6-adapter": "^2.0" }, "autoload": { "psr-4": { diff --git a/config/sns-handler.php b/config/sns-handler.php index 8545b00..ca7cbb6 100644 --- a/config/sns-handler.php +++ b/config/sns-handler.php @@ -2,5 +2,10 @@ return [ 'validate-sns-messages' => env('VALIDATE_SNS_MESSAGES', true), - 'sns-class-map' => [] + 'confirmation-events' => [ + Nipwaayoni\SnsHandler\Events\SnsConfirmationRequestReceived::class => ['*'] + ], + 'message-events' => [ + Nipwaayoni\SnsHandler\Events\SnsMessageReceived::class => ['*'] + ], ]; \ No newline at end of file diff --git a/src/Listeners/SnsConfirmationRequestListener.php b/src/Listeners/SnsConfirmationRequestListener.php index 9ddc317..f717af5 100644 --- a/src/Listeners/SnsConfirmationRequestListener.php +++ b/src/Listeners/SnsConfirmationRequestListener.php @@ -3,25 +3,67 @@ namespace Nipwaayoni\SnsHandler\Listeners; -use Illuminate\Support\Facades\Http; +use Http\Discovery\HttpClientDiscovery; +use Http\Discovery\Psr17FactoryDiscovery; use Illuminate\Support\Facades\Log; use Nipwaayoni\SnsHandler\Events\SnsConfirmationRequestReceived; use Nipwaayoni\SnsHandler\SnsConfirmSubscriptionException; +use Nipwaayoni\SnsHandler\SnsException; +use Nipwaayoni\SnsHandler\SnsMessage; +use Psr\Http\Client\ClientExceptionInterface; +use Psr\Http\Client\ClientInterface; +use Psr\Http\Message\RequestFactoryInterface; +use Psr\Http\Message\ResponseInterface; class SnsConfirmationRequestListener { + /** + * @var ClientInterface + */ + private $client; + + /** + * @var RequestFactoryInterface + */ + private $requestFactory; + + /** + * @param ClientInterface|null $client + * @param RequestFactoryInterface|null $requestFactory + */ + public function __construct( + ClientInterface $client = null, + RequestFactoryInterface $requestFactory = null + ) { + $this->client = $client ?? HttpClientDiscovery::find(); + $this->requestFactory = $requestFactory ?? Psr17FactoryDiscovery::findRequestFactory(); + } + public function handle(SnsConfirmationRequestReceived $event) { $message = $event->message(); - //TODO Make this work with Laravel 6, as the Http facade was introduced in Laravel 7 - $response = Http::get($message->subscribeUrl()); - if ($response->successful()) { - $info = sprintf('Subscription confirmation for %s succeeded with status %s', $message->topicArn(), $response->status()); - Log::info($info); - return; + + $response = $this->getResponse($message); + + Log::info(sprintf('Subscription confirmation for %s succeeded with status %s', $message->topicArn(), $response->getStatusCode())); + } + + /** + * @param SnsMessage $message + * @return ResponseInterface + * @throws SnsConfirmSubscriptionException + * @throws SnsException + */ + private function getResponse(SnsMessage $message): ResponseInterface + { + try { + return $this->client->sendRequest( + $this->requestFactory->createRequest('GET', $message->subscribeUrl()) + ); + } catch (ClientExceptionInterface $e) { + throw new SnsConfirmSubscriptionException( + sprintf('Subscription confirmation for %s failed with status %s', $message->topicArn(), $e->getCode()) + ); } - $error = sprintf('Subscription confirmation for %s failed with status %s', $message->topicArn(), $response->status()); - Log::error($error); - throw new SnsConfirmSubscriptionException($error); } } diff --git a/src/SnsBroker.php b/src/SnsBroker.php index 279ee65..55f8f74 100644 --- a/src/SnsBroker.php +++ b/src/SnsBroker.php @@ -5,9 +5,8 @@ use Aws\Sns\Message; use Aws\Sns\MessageValidator; +use Illuminate\Config\Repository as Config; use Illuminate\Support\Facades\Log; -use Nipwaayoni\SnsHandler\Events\SnsConfirmationRequestReceived; -use Nipwaayoni\SnsHandler\Events\SnsMessageReceived; class SnsBroker { @@ -19,10 +18,15 @@ class SnsBroker * @var Log */ private $log; + /** + * @var Config + */ + private $config; - public function __construct(MessageValidator $validator) + public function __construct(MessageValidator $validator, Config $config) { $this->validator = $validator; + $this->config = $config; } /** @@ -66,19 +70,35 @@ public function handleRequest(SnsHttpRequest $request): void throw new SnsException(sprintf('Unknown message type: %s', $message->type())); } - private function getSubscriptionEvent(string $arn) + /** + * @param string $arn + * @return string + * @throws SnsUnknownTopicArnException + */ + private function getSubscriptionEvent(string $arn): string { - $map = [SnsConfirmationRequestReceived::class => ['*']]; + $map = $this->config->get('sns-handler.confirmation-events', []); return $this->arnMap($arn, $map); } - private function getNotificationEvent(string $arn) + /** + * @param string $arn + * @return string + * @throws SnsUnknownTopicArnException + */ + private function getNotificationEvent(string $arn): string { - $map = [SnsMessageReceived::class => ['*']]; + $map = $this->config->get('sns-handler.message-events', []); return $this->arnMap($arn, $map); } - private function arnMap(string $arn, array $map) + /** + * @param string $arn + * @param array $map + * @return string + * @throws SnsUnknownTopicArnException + */ + private function arnMap(string $arn, array $map): string { $default = null; foreach ($map as $className => $arnList) { @@ -90,6 +110,12 @@ private function arnMap(string $arn, array $map) } } + if (null === $default) { + throw new SnsUnknownTopicArnException(sprintf('Unmappable TopicArn: %s', $arn)); + } + + // TODO ensure class is dispatchable + return $default; } } diff --git a/src/SnsUnknownTopicArnException.php b/src/SnsUnknownTopicArnException.php index 4539c39..84f1780 100644 --- a/src/SnsUnknownTopicArnException.php +++ b/src/SnsUnknownTopicArnException.php @@ -3,12 +3,6 @@ namespace Nipwaayoni\SnsHandler; -use Throwable; - class SnsUnknownTopicArnException extends SnsException { - public function __construct(string $topicArn = '', $code = 0, Throwable $previous = null) - { - parent::__construct(sprintf('No handler registered for TopicArn %s', $topicArn), $code, $previous); - } } diff --git a/tests/Events/SnsConfirmationRequestAlphaReceived.php b/tests/Events/SnsConfirmationRequestAlphaReceived.php new file mode 100644 index 0000000..9c2fbff --- /dev/null +++ b/tests/Events/SnsConfirmationRequestAlphaReceived.php @@ -0,0 +1,25 @@ +message = $message; + } + + public function message(): SnsMessage + { + return $this->message; + } +} diff --git a/tests/Events/SnsConfirmationRequestBetaReceived.php b/tests/Events/SnsConfirmationRequestBetaReceived.php new file mode 100644 index 0000000..a8bb0cc --- /dev/null +++ b/tests/Events/SnsConfirmationRequestBetaReceived.php @@ -0,0 +1,25 @@ +message = $message; + } + + public function message(): SnsMessage + { + return $this->message; + } +} diff --git a/tests/Events/SnsMessageAlphaReceived.php b/tests/Events/SnsMessageAlphaReceived.php new file mode 100644 index 0000000..143c739 --- /dev/null +++ b/tests/Events/SnsMessageAlphaReceived.php @@ -0,0 +1,25 @@ +message = $message; + } + + public function message(): SnsMessage + { + return $this->message; + } +} diff --git a/tests/Events/SnsMessageBetaReceived.php b/tests/Events/SnsMessageBetaReceived.php new file mode 100644 index 0000000..d7927c6 --- /dev/null +++ b/tests/Events/SnsMessageBetaReceived.php @@ -0,0 +1,25 @@ +message = $message; + } + + public function message(): SnsMessage + { + return $this->message; + } +} diff --git a/tests/Feature/SnsHandlerTest.php b/tests/Feature/SnsHandlerDefaultEventsTest.php similarity index 66% rename from tests/Feature/SnsHandlerTest.php rename to tests/Feature/SnsHandlerDefaultEventsTest.php index a24e43b..a563f19 100644 --- a/tests/Feature/SnsHandlerTest.php +++ b/tests/Feature/SnsHandlerDefaultEventsTest.php @@ -4,32 +4,29 @@ namespace Nipwaayoni\Tests\SnsHandler\Feature; use Aws\Sns\MessageValidator; -use Illuminate\Http\Client\Request; use Illuminate\Support\Facades\Event; -use Illuminate\Support\Facades\Http; use Nipwaayoni\SnsHandler\Events\SnsConfirmationRequestReceived; use Nipwaayoni\SnsHandler\Events\SnsMessageReceived; -use Nipwaayoni\SnsHandler\Listeners\SnsConfirmationRequestListener; use Nipwaayoni\SnsHandler\NullMessageValidator; use Nipwaayoni\SnsHandler\ServiceProvider; use Nipwaayoni\SnsHandler\SnsMessage; +use Nipwaayoni\Tests\SnsHandler\Events\SnsConfirmationRequestAlphaReceived; +use Nipwaayoni\Tests\SnsHandler\Events\SnsConfirmationRequestBetaReceived; +use Nipwaayoni\Tests\SnsHandler\Events\SnsMessageAlphaReceived; +use Nipwaayoni\Tests\SnsHandler\Events\SnsMessageBetaReceived; use Nipwaayoni\Tests\SnsHandler\MakesSnsTests; -class SnsHandlerTest extends \Nipwaayoni\Tests\SnsHandler\TestCase +class SnsHandlerDefaultEventsTest extends \Nipwaayoni\Tests\SnsHandler\TestCase { use MakesSnsTests; public function setUp(): void { parent::setUp(); - $this->app->bind(MessageValidator::class, NullMessageValidator::class); - } - protected function getEnvironmentSetUp($app) - { -// $app['config']->set('sns-handler.sns-class-map', [ -// 'arn:aws:sns:us-west-2:123456789012:MyTopic' => SnsMessageHandlerStub::class, -// ]); + Event::fake(); + + $this->app->bind(MessageValidator::class, NullMessageValidator::class); } protected function getPackageProviders($app) @@ -37,22 +34,19 @@ protected function getPackageProviders($app) return [ServiceProvider::class]; } - public function testReturnsNotFoundForUnknownTopicArn(): void + protected function getEnvironmentSetUp($app) { - $this->markTestSkipped("Skipping until we implement enhanced mapping functionality for events."); - $data = $this->makeSnsMessageData([ - 'Type' => SnsMessage::NOTIFICATION_TYPE, - 'TopicArn' => 'arn:aws:sns:us-west-2:123456789012:Unknown', + $app['config']->set('sns-handler.confirmation-events', [ + SnsConfirmationRequestReceived::class => ['*'], ]); - $response = $this->postJson('/api/sns/message', $data); - $this->assertEquals(404, $response->status()); + $app['config']->set('sns-handler.message-events', [ + SnsMessageReceived::class => ['*'], + ]); } public function testDispatchesDefaultConfirmationEvent(): void { - Event::fake(); - $data = $this->makeSnsMessageData([ 'Type' => SnsMessage::SUBSCRIBE_TYPE, 'Message' => json_encode(['id' => 123, 'color' => 'red'], true), @@ -60,13 +54,15 @@ public function testDispatchesDefaultConfirmationEvent(): void ]); $response = $this->postJson('/api/sns/message', $data); - Event::assertDispatched(SnsConfirmationRequestReceived::class); + $this->assertEquals(200, $response->status()); + Event::assertDispatched(SnsConfirmationRequestReceived::class); + Event::assertNotDispatched(SnsConfirmationRequestAlphaReceived::class); + Event::assertNotDispatched(SnsConfirmationRequestBetaReceived::class); } public function testDispatchesDefaultMessageEvent(): void { - Event::fake(); $data = $this->makeSnsMessageData([ 'Type' => SnsMessage::NOTIFICATION_TYPE, 'TopicArn' => 'arn:aws:sns:us-west-2:123456789012:MyTopic', @@ -76,7 +72,8 @@ public function testDispatchesDefaultMessageEvent(): void $response = $this->postJson('/api/sns/message', $data); $this->assertEquals(200, $response->status()); - Event::assertDispatched(SnsMessageReceived::class); + Event::assertNotDispatched(SnsMessageAlphaReceived::class); + Event::assertNotDispatched(SnsMessageBetaReceived::class); } } diff --git a/tests/Feature/SnsHandlerMappedEventsTest.php b/tests/Feature/SnsHandlerMappedEventsTest.php new file mode 100644 index 0000000..4b1c982 --- /dev/null +++ b/tests/Feature/SnsHandlerMappedEventsTest.php @@ -0,0 +1,95 @@ +app->bind(MessageValidator::class, NullMessageValidator::class); + } + + protected function getPackageProviders($app) + { + return [ServiceProvider::class]; + } + + protected function getEnvironmentSetUp($app) + { + $app['config']->set('sns-handler.confirmation-events', [ + SnsConfirmationRequestAlphaReceived::class => ['arn:aws:sns:us-west-2:123456789012:AlphaTopic'], + ]); + + $app['config']->set('sns-handler.message-events', [ + SnsMessageAlphaReceived::class => ['arn:aws:sns:us-west-2:123456789012:AlphaTopic'], + ]); + } + + public function testDispatchesMappedConfirmationEvent(): void + { + $data = $this->makeSnsMessageData([ + 'Type' => SnsMessage::SUBSCRIBE_TYPE, + 'TopicArn' => 'arn:aws:sns:us-west-2:123456789012:AlphaTopic', + 'SubscribeURL' => 'https://aws.amazon.com/sns/register/abc123' + ]); + + $response = $this->postJson('/api/sns/message', $data); + + $this->assertEquals(200, $response->status()); + Event::assertDispatched(SnsConfirmationRequestAlphaReceived::class); + Event::assertNotDispatched(SnsConfirmationRequestBetaReceived::class); + Event::assertNotDispatched(SnsConfirmationRequestReceived::class); + } + + public function testDispatchesMappedMessageEvent(): void + { + $data = $this->makeSnsMessageData([ + 'Type' => SnsMessage::NOTIFICATION_TYPE, + 'TopicArn' => 'arn:aws:sns:us-west-2:123456789012:AlphaTopic', + 'Message' => 'Test message', + ]); + + $response = $this->postJson('/api/sns/message', $data); + + $this->assertEquals(200, $response->status()); + Event::assertDispatched(SnsMessageAlphaReceived::class); + Event::assertNotDispatched(SnsMessageReceived::class); + Event::assertNotDispatched(SnsMessageBetaReceived::class); + } + + public function testReturnsNotFoundForUnmappedMessageEvent(): void + { + $data = $this->makeSnsMessageData([ + 'Type' => SnsMessage::NOTIFICATION_TYPE, + 'TopicArn' => 'arn:aws:sns:us-west-2:123456789012:BetaTopic', + 'Message' => 'Test message', + ]); + + $response = $this->postJson('/api/sns/message', $data); + + $this->assertEquals(404, $response->status()); + Event::assertNotDispatched(SnsMessageAlphaReceived::class); + Event::assertNotDispatched(SnsMessageReceived::class); + Event::assertNotDispatched(SnsMessageBetaReceived::class); + } +} diff --git a/tests/HttpTransaction.php b/tests/HttpTransaction.php new file mode 100644 index 0000000..daf998c --- /dev/null +++ b/tests/HttpTransaction.php @@ -0,0 +1,30 @@ +transaction = $transaction; + } + + public function request(): RequestInterface + { + return $this->transaction['request']; + } + + public function response(): ResponseInterface + { + return $this->transaction['response']; + } +} diff --git a/tests/HttpTransactionContainer.php b/tests/HttpTransactionContainer.php new file mode 100644 index 0000000..b743b39 --- /dev/null +++ b/tests/HttpTransactionContainer.php @@ -0,0 +1,45 @@ +container[$offset]); + } + + public function offsetGet($offset): HttpTransaction + { + if (!isset($this->container[$offset])) { + throw new \Exception('Undefined transaction offset'); + } + + return new HttpTransaction($this->container[$offset]); + } + + public function offsetSet($offset, $value): void + { + if (is_null($offset)) { + $this->container[] = $value; + } else { + $this->container[$offset] = $value; + } + } + + public function offsetUnset($offset): void + { + unset($this->container[$offset]); + } + + public function count(): int + { + return count($this->container); + } +} diff --git a/tests/SnsHttpTestHelperTrait.php b/tests/SnsHttpTestHelperTrait.php new file mode 100644 index 0000000..24ba7b8 --- /dev/null +++ b/tests/SnsHttpTestHelperTrait.php @@ -0,0 +1,43 @@ +container = new HttpTransactionContainer(); + + $history = Middleware::history($this->container); + + $mock = new MockHandler($responses); + + $handlerStack = HandlerStack::create($mock); + $handlerStack->push($history); + + $client = new \GuzzleHttp\Client(['handler' => $handlerStack]); + $this->client = new \Http\Adapter\Guzzle6\Client($client); + } + + public function httpAssertSent(callable $function): void + { + $this->assertCount(1, $this->container); + + $request = $this->container[0]->request(); + + $function($request); + } +} diff --git a/tests/TestCase.php b/tests/TestCase.php index 937e99a..33f839f 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -3,6 +3,35 @@ namespace Nipwaayoni\Tests\SnsHandler; +use Illuminate\Config\Repository; +use Nipwaayoni\SnsHandler\Events\SnsConfirmationRequestReceived; +use Nipwaayoni\SnsHandler\Events\SnsMessageReceived; +use PHPUnit\Framework\MockObject\MockObject; + class TestCase extends \Orchestra\Testbench\TestCase { + /** @var Repository|mixed|MockObject */ + protected $config; + + protected $configValues = [ + 'validate-sns-messages' => true, + 'confirmation-events' => [ + SnsConfirmationRequestReceived::class => ['*'] + ], + 'message-events' => [ + SnsMessageReceived::class => ['*'] + ], + ]; + + public function setUp(): void + { + parent::setUp(); + + $this->config = $this->createMock(Repository::class); + $this->config->method('get') + ->willReturnCallback(function (string $key) { + $parts = explode('.', $key); + return $this->configValues[$parts[1]] ?? null; + }); + } } diff --git a/tests/Unit/SnsBrokerTest.php b/tests/Unit/SnsBrokerTest.php index a84a94f..b50644d 100644 --- a/tests/Unit/SnsBrokerTest.php +++ b/tests/Unit/SnsBrokerTest.php @@ -11,6 +11,10 @@ use Nipwaayoni\SnsHandler\SnsHttpRequest; use Nipwaayoni\SnsHandler\SnsMessage; use Nipwaayoni\SnsHandler\SnsUnknownTopicArnException; +use Nipwaayoni\Tests\SnsHandler\Events\SnsConfirmationRequestAlphaReceived; +use Nipwaayoni\Tests\SnsHandler\Events\SnsConfirmationRequestBetaReceived; +use Nipwaayoni\Tests\SnsHandler\Events\SnsMessageAlphaReceived; +use Nipwaayoni\Tests\SnsHandler\Events\SnsMessageBetaReceived; use PHPUnit\Framework\MockObject\MockObject; use Nipwaayoni\Tests\SnsHandler\MakesSnsTests; use Nipwaayoni\Tests\SnsHandler\TestCase; @@ -25,17 +29,15 @@ class SnsBrokerTest extends TestCase /** @var MessageValidator|MockObject */ private $validator; - public function setUp(): void { parent::setUp(); Event::fake(); - $this->validator = $this->createMock(MessageValidator::class); - $this->broker = new SnsBroker($this->validator); + $this->broker = new SnsBroker($this->validator, $this->config); } public function testMakesSnsMessageFromHttpRequest(): void @@ -49,33 +51,34 @@ public function testMakesSnsMessageFromHttpRequest(): void $this->assertEquals(SnsMessage::NOTIFICATION_TYPE, $message->type()); } - public function testRejectsMessageWithUnknownTopicArn(): void + public function testThrowsExceptionForUnknownMessageType(): void { - $this->markTestSkipped("Doesn't work with event faking and we think this test will go away after refactoring."); $request = $this->createMock(SnsHttpRequest::class); $request->expects($this->once())->method('jsonContent') - ->willReturn($this->makeSnsMessageJson(['TopicArn' => 'arn:aws:sns:us-west-2:123456789012:Unknown'])); + ->willReturn($this->makeSnsMessageJson(['Type' => 'Unknown'])); - $this->expectException(SnsUnknownTopicArnException::class); - $this->expectExceptionMessage('No handler registered for TopicArn arn:aws:sns:us-west-2:123456789012:Unknown'); + $this->expectException(SnsException::class); + $this->expectExceptionMessage('Unknown message type: Unknown'); $this->broker->handleRequest($request); + + Event::assertNotDispatched(SnsMessageReceived::class); } - public function testThrowsExceptionForUnknownMessageType(): void + public function testValidatesSnsMessage(): void { $request = $this->createMock(SnsHttpRequest::class); $request->expects($this->once())->method('jsonContent') - ->willReturn($this->makeSnsMessageJson(['Type' => 'Unknown'])); + ->willReturn($this->makeSnsMessageJson([ + 'MessageId' => 'abc123', + ])); - $this->expectException(SnsException::class); - $this->expectExceptionMessage('Unknown message type: Unknown'); + $this->validator->expects($this->once())->method('validate'); $this->broker->handleRequest($request); - Event::assertNotDispatched(SnsMessageReceived::class); } - public function testDispatchesSnsConfirmationRequestEvent(): void + public function testDispatchesDefaultConfirmationRequestEvent(): void { $request = $this->createMock(SnsHttpRequest::class); $request->expects($this->once())->method('jsonContent') @@ -85,10 +88,78 @@ public function testDispatchesSnsConfirmationRequestEvent(): void ])); $this->broker->handleRequest($request); + Event::assertDispatched(SnsConfirmationRequestReceived::class); } - public function testDispatchesDefaultNotificationMessage(): void + public function testDispatchesMappedConfirmationRequestEvent(): void + { + $this->configValues['confirmation-events'] = [ + SnsConfirmationRequestAlphaReceived::class => ['arn:aws:sns:us-west-2:123456789012:AlphaTopic'], + SnsConfirmationRequestBetaReceived::class => ['arn:aws:sns:us-west-2:123456789012:AlphaTopic'], + SnsConfirmationRequestReceived::class => ['*'], + ]; + + $request = $this->createMock(SnsHttpRequest::class); + $request->expects($this->once())->method('jsonContent') + ->willReturn($this->makeSnsMessageJson([ + 'Type' => SnsMessage::SUBSCRIBE_TYPE, + 'SubscribeURL' => 'https://aws.amazon.com/subscribe/123', + 'TopicArn' => 'arn:aws:sns:us-west-2:123456789012:AlphaTopic' + ])); + + $this->broker->handleRequest($request); + + Event::assertDispatched(SnsConfirmationRequestAlphaReceived::class); + Event::assertNotDispatched(SnsConfirmationRequestReceived::class); + } + + public function testDispatchFirstMappedConfirmationEvent(): void + { + $this->configValues['confirmation-events'] = [ + SnsConfirmationRequestAlphaReceived::class => ['arn:aws:sns:us-west-2:123456789012:AlphaTopic'], + SnsConfirmationRequestBetaReceived::class => ['arn:aws:sns:us-west-2:123456789012:AlphaTopic'], + SnsConfirmationRequestReceived::class => ['*'], + ]; + + $request = $this->createMock(SnsHttpRequest::class); + $request->expects($this->once())->method('jsonContent') + ->willReturn($this->makeSnsMessageJson([ + 'Type' => SnsMessage::SUBSCRIBE_TYPE, + 'SubscribeURL' => 'https://aws.amazon.com/subscribe/123', + 'TopicArn' => 'arn:aws:sns:us-west-2:123456789012:AlphaTopic' + ])); + + $this->broker->handleRequest($request); + + Event::assertDispatched(SnsConfirmationRequestAlphaReceived::class); + Event::assertNotDispatched(SnsConfirmationRequestBetaReceived::class); + Event::assertNotDispatched(SnsConfirmationRequestReceived::class); + } + + public function testRejectsWithUnhandledTopicArnOnConfirmation(): void + { + $this->configValues['confirmation-events'] = [ + SnsConfirmationRequestBetaReceived::class => ['arn:aws:sns:us-west-2:123456789012:BetaTopic'], + ]; + + $request = $this->createMock(SnsHttpRequest::class); + $request->expects($this->once())->method('jsonContent') + ->willReturn($this->makeSnsMessageJson([ + 'Type' => SnsMessage::SUBSCRIBE_TYPE, + 'SubscribeURL' => 'https://aws.amazon.com/subscribe/123', + 'TopicArn' => 'arn:aws:sns:us-west-2:123456789012:AlphaTopic' + ])); + + $this->expectException(SnsUnknownTopicArnException::class); + $this->expectExceptionMessage('Unmappable TopicArn: arn:aws:sns:us-west-2:123456789012:AlphaTopic'); + + $this->broker->handleRequest($request); + + Event::assertNotDispatched(SnsConfirmationRequestReceived::class); + } + + public function testDispatchesDefaultNotificationEvent(): void { $request = $this->createMock(SnsHttpRequest::class); $request->expects($this->once())->method('jsonContent') @@ -97,19 +168,71 @@ public function testDispatchesDefaultNotificationMessage(): void ])); $this->broker->handleRequest($request); + Event::assertDispatched(SnsMessageReceived::class); } - public function testValidatesSnsMessage(): void + public function testDispatchMappedNotificationEvent(): void { + $this->configValues['message-events'] = [ + SnsMessageAlphaReceived::class => ['arn:aws:sns:us-west-2:123456789012:AlphaTopic'], + SnsMessageBetaReceived::class => ['arn:aws:sns:us-west-2:123456789012:AlphaTopic'], + SnsMessageReceived::class => ['*'], + ]; + $request = $this->createMock(SnsHttpRequest::class); $request->expects($this->once())->method('jsonContent') ->willReturn($this->makeSnsMessageJson([ 'MessageId' => 'abc123', + 'TopicArn' => 'arn:aws:sns:us-west-2:123456789012:AlphaTopic' ])); - $this->validator->expects($this->once())->method('validate'); + $this->broker->handleRequest($request); + + Event::assertDispatched(SnsMessageAlphaReceived::class); + Event::assertNotDispatched(SnsMessageReceived::class); + } + + public function testDispatchFirstMappedNotificationEvent(): void + { + $this->configValues['message-events'] = [ + SnsMessageAlphaReceived::class => ['arn:aws:sns:us-west-2:123456789012:AlphaTopic'], + SnsMessageBetaReceived::class => ['arn:aws:sns:us-west-2:123456789012:AlphaTopic'], + SnsMessageReceived::class => ['*'], + ]; + + $request = $this->createMock(SnsHttpRequest::class); + $request->expects($this->once())->method('jsonContent') + ->willReturn($this->makeSnsMessageJson([ + 'MessageId' => 'abc123', + 'TopicArn' => 'arn:aws:sns:us-west-2:123456789012:AlphaTopic' + ])); $this->broker->handleRequest($request); + + Event::assertDispatched(SnsMessageAlphaReceived::class); + Event::assertNotDispatched(SnsMessageBetaReceived::class); + Event::assertNotDispatched(SnsMessageReceived::class); + } + + public function testRejectsWithUnhandledTopicArnOnMessage(): void + { + $this->configValues['message-events'] = [ + SnsMessageBetaReceived::class => ['arn:aws:sns:us-west-2:123456789012:BetaTopic'], + ]; + + $request = $this->createMock(SnsHttpRequest::class); + $request->expects($this->once())->method('jsonContent') + ->willReturn($this->makeSnsMessageJson([ + 'MessageId' => 'abc123', + 'TopicArn' => 'arn:aws:sns:us-west-2:123456789012:AlphaTopic' + ])); + + $this->expectException(SnsUnknownTopicArnException::class); + $this->expectExceptionMessage('Unmappable TopicArn: arn:aws:sns:us-west-2:123456789012:AlphaTopic'); + + $this->broker->handleRequest($request); + + Event::assertNotDispatched(SnsMessageReceived::class); } } diff --git a/tests/Unit/SnsConfirmationRequestListenerTest.php b/tests/Unit/SnsConfirmationRequestListenerTest.php index 741d150..2e0bde2 100644 --- a/tests/Unit/SnsConfirmationRequestListenerTest.php +++ b/tests/Unit/SnsConfirmationRequestListenerTest.php @@ -4,19 +4,20 @@ namespace Nipwaayoni\Tests\SnsHandler\Unit; use Aws\Sns\Message; -use Illuminate\Http\Client\Request; +use GuzzleHttp\Psr7\Response; use Illuminate\Support\Facades\Event; -use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; use Nipwaayoni\SnsHandler\Events\SnsConfirmationRequestReceived; use Nipwaayoni\SnsHandler\Listeners\SnsConfirmationRequestListener; use Nipwaayoni\SnsHandler\SnsConfirmSubscriptionException; use Nipwaayoni\SnsHandler\SnsMessage; use Nipwaayoni\Tests\SnsHandler\MakesSnsTests; +use Nipwaayoni\Tests\SnsHandler\SnsHttpTestHelperTrait; class SnsConfirmationRequestListenerTest extends \Nipwaayoni\Tests\SnsHandler\TestCase { use MakesSnsTests; + use SnsHttpTestHelperTrait; /** @var SnsConfirmationRequestListener */ private $listener; @@ -27,8 +28,6 @@ public function setUp(): void parent::setUp(); Event::fake(); - - $this->listener = new SnsConfirmationRequestListener(); } public function testThrowsExceptionIfConfirmSubscriptionFails(): void @@ -38,36 +37,41 @@ public function testThrowsExceptionIfConfirmSubscriptionFails(): void 'SubscribeURL' => 'https://aws.amazon.com/subscribe/123', ])); - Http::fake([ - 'https://aws.amazon.com/subscribe/123' => Http::response([], 404, []) - ]); + $this->httpExpects( + new Response(404) + ); + + $this->listener = new SnsConfirmationRequestListener($this->client); $event = new SnsConfirmationRequestReceived(new SnsMessage($message)); $this->expectException(SnsConfirmSubscriptionException::class); $this->expectExceptionMessage('Subscription confirmation for arn:aws:sns:us-west-2:123456789012:MyTopic failed with status 404'); - Log::shouldReceive('error')->once(); $this->listener->handle($event); } public function testConfirmsSubscriptionUsingSubscribeUrl(): void { - Http::fake(['https://aws.amazon.com/subscribe/123' => Http::response([], 200, [])]); + $this->httpExpects( + new Response(200) + ); $message = Message::fromJsonString($this->makeSnsMessageJson([ 'Type' => SnsMessage::SUBSCRIBE_TYPE, 'SubscribeURL' => 'https://aws.amazon.com/subscribe/123', ])); + $this->listener = new SnsConfirmationRequestListener($this->client); + $event = new SnsConfirmationRequestReceived(new SnsMessage($message)); Log::shouldReceive('info')->once(); $this->listener->handle($event); - Http::assertSent(function (Request $request) { - return $request->url() === 'https://aws.amazon.com/subscribe/123'; + $this->httpAssertSent(function (\Psr\Http\Message\RequestInterface $request) { + $this->assertEquals('https://aws.amazon.com/subscribe/123', $request->getUri()); }); } }