From 3306cb7a5b58423b970a73c56c84c4f515954204 Mon Sep 17 00:00:00 2001 From: butschster Date: Mon, 23 May 2022 01:44:00 +0400 Subject: [PATCH] Adds unit tests --- composer.json | 5 +- src/ChannelManager.php | 14 +- src/Config/NotificationsConfig.php | 19 +- src/Notifier.php | 1 + src/QueueableInterface.php | 10 - src/SendNotificationJob.php | 20 +- tests/app/Notifications/UserNotification.php | 24 +++ tests/src/ChannelManagerTest.php | 159 ++++++++++++++++ tests/src/Config/NotificationsConfigTest.php | 186 +++++++++++++++++++ tests/src/NotifierTest.php | 31 +++- tests/src/SendNotificationJobTest.php | 83 +++++++++ 11 files changed, 517 insertions(+), 35 deletions(-) delete mode 100644 src/QueueableInterface.php create mode 100644 tests/app/Notifications/UserNotification.php create mode 100644 tests/src/ChannelManagerTest.php create mode 100644 tests/src/Config/NotificationsConfigTest.php create mode 100644 tests/src/SendNotificationJobTest.php diff --git a/composer.json b/composer.json index 7ba2c95..75a1f4e 100644 --- a/composer.json +++ b/composer.json @@ -28,9 +28,8 @@ "symfony/notifier": "^6.0" }, "require-dev": { - "mockery/mockery": "^1.5", - "phpunit/phpunit": "^9.5", - "spiral/testing": "^1.0", + "spiral/testing": "^1.2", + "symfony/firebase-notifier": "^6.0", "vimeo/psalm": "^4.9" }, "autoload": { diff --git a/src/ChannelManager.php b/src/ChannelManager.php index 5bf0301..0d7f551 100644 --- a/src/ChannelManager.php +++ b/src/ChannelManager.php @@ -4,7 +4,7 @@ namespace Spiral\Notifications; -use Spiral\Core\Container; +use Spiral\Core\FactoryInterface; use Spiral\Notifications\Config\NotificationsConfig; use Spiral\SendIt\Config\MailerConfig; use Symfony\Component\Mailer\Transport\RoundRobinTransport as MailerRoundRobinTransport; @@ -19,7 +19,7 @@ final class ChannelManager private array $channels = []; public function __construct( - private Container $container, + private FactoryInterface $factory, private NotificationsConfig $config, private MailerConfig $mailerConfig, ) { @@ -36,16 +36,16 @@ public function getChannel(string $name): ?ChannelInterface if ($channel['type'] === EmailChannel::class) { if (\count($dsns) === 1) { - $transport = $this->resolveMailerTransport(new Transport\Dsn($dsns[0])); + $transport = $this->resolveMailerTransport($dsns[0]); } else { $transport = new MailerRoundRobinTransport( - \array_map(function (string $dsn): MailerTransportInterface { - return $this->resolveMailerTransport(new Transport\Dsn($dsn)); + \array_map(function (Transport\Dsn $dsn): MailerTransportInterface { + return $this->resolveMailerTransport($dsn); }, $dsns) ); } - return $this->container->make($channel['type'], [ + return $this->factory->make($channel['type'], [ 'transport' => $transport, 'from' => $this->mailerConfig->getFromAddress(), ]); @@ -61,7 +61,7 @@ public function getChannel(string $name): ?ChannelInterface ); } - return $this->container->make($channel['type'], [ + return $this->factory->make($channel['type'], [ 'transport' => $transport, ]); } diff --git a/src/Config/NotificationsConfig.php b/src/Config/NotificationsConfig.php index 3502ad6..f313dd0 100644 --- a/src/Config/NotificationsConfig.php +++ b/src/Config/NotificationsConfig.php @@ -7,11 +7,13 @@ use Spiral\Core\InjectableConfig; use Spiral\Notifications\Exceptions\InvalidArgumentException; use Spiral\Notifications\Exceptions\TransportException; +use Symfony\Component\Notifier\Channel\ChannelInterface; use Symfony\Component\Notifier\Transport\Dsn; final class NotificationsConfig extends InjectableConfig { public const CONFIG = 'notifications'; + protected $config = [ 'queueConnection' => null, 'channels' => [], @@ -31,33 +33,36 @@ public function getChannelPolicies(): array /** * @param string $name - * @return array{type: class-string, transport: array} + * @return array{ + * type: class-string, + * transport: array + * } * @throws InvalidArgumentException * @throws TransportException */ public function getChannel(string $name): array { if (! isset($this->config['channels'][$name])) { - throw new TransportException(sprintf('Transport with given name `%s` is not found.', $name)); + throw new TransportException(sprintf('Channel with given name `%s` is not found.', $name)); } $channel = $this->config['channels'][$name]; if (! \is_array($channel)) { throw new InvalidArgumentException( - sprintf('Config for channel `%s` must be an array', $name) + sprintf('Config for channel `%s` must be an array.', $name) ); } if (! isset($channel['type'])) { throw new InvalidArgumentException( - sprintf('Config for channel `%s` should contain `type` key', $name) + sprintf('Config for channel `%s` should contain `type` key.', $name) ); } if (! isset($channel['transport'])) { throw new InvalidArgumentException( - sprintf('Config for channel `%s` should contain `transport` key', $name) + sprintf('Config for channel `%s` should contain `transport` key.', $name) ); } @@ -84,13 +89,15 @@ public function getTransport(array $names): array throw new TransportException(sprintf('Transport with given name `%s` is not found.', $name)); } - $transport = $dsns[] = $this->config['transports'][$name]; + $transport = $this->config['transports'][$name]; if (! \is_string($transport)) { throw new InvalidArgumentException( sprintf('Config for transport `%s` must be a DSN string', $name) ); } + + $dsns[] = new Dsn($transport); } diff --git a/src/Notifier.php b/src/Notifier.php index f6fd94c..dc60389 100644 --- a/src/Notifier.php +++ b/src/Notifier.php @@ -3,6 +3,7 @@ namespace Spiral\Notifications; use Spiral\Notifications\Config\NotificationsConfig; +use Spiral\Queue\QueueableInterface; use Spiral\Queue\QueueConnectionProviderInterface; use Symfony\Component\Notifier\Channel\ChannelInterface; use Symfony\Component\Notifier\Channel\ChannelPolicy; diff --git a/src/QueueableInterface.php b/src/QueueableInterface.php deleted file mode 100644 index e88d86c..0000000 --- a/src/QueueableInterface.php +++ /dev/null @@ -1,10 +0,0 @@ -notifier->sendNow($notification, $recipient); diff --git a/tests/app/Notifications/UserNotification.php b/tests/app/Notifications/UserNotification.php new file mode 100644 index 0000000..8d97814 --- /dev/null +++ b/tests/app/Notifications/UserNotification.php @@ -0,0 +1,24 @@ + [ + 'email_single' => [ + 'type' => EmailChannel::class, + 'transport' => 'gmail', + ], + 'email_multiple' => [ + 'type' => EmailChannel::class, + 'transport' => ['gmail', 'yahoo'], + ], + 'firebase' => [ + 'type' => 'firebase', + 'transport' => 'firebase', + ], + 'firebase_multiple' => [ + 'type' => 'firebase_multiple', + 'transport' => ['firebase1', 'firebase'], + ], + 'unknown' => [ + 'type' => 'unknown', + 'transport' => 'unknown', + ], + 'unsupported' => [ + 'type' => 'unsupported', + 'transport' => 'unsupported', + ], + ], + 'transports' => [ + 'gmail' => 'smtp://gmail:pass@smtp.gmail.com:25', + 'yahoo' => 'smtp://yahoo:pass@smtp.yahoo.com:25', + 'firebase' => 'firebase://USERNAME:PASSWORD@default', + 'firebase1' => 'firebase://USERNAME1:PASSWORD@default', + 'unknown' => 'foo://USERNAME:PASSWORD@default', + 'unsupported' => 'discord://TOKEN@default?webhook_id=ID', + ], + ]); + + $this->manager = new ChannelManager( + $this->factory = m::mock(FactoryInterface::class), + $config, + new MailerConfig([ + 'dsn' => 'smtp://user:pass@smtp.example.com:25', + 'from' => 'info@site.com', + 'queue' => null, + 'pipeline' => null, + 'queueConnection' => null, + ]) + ); + } + + public function testGetsEmailChannelWithSingleTransport(): void + { + $this->factory->shouldReceive('make')->once() + ->withArgs(static function (string $type, array $args): bool { + return $type === EmailChannel::class + && $args['transport'] instanceof EsmtpTransport + && (string)$args['transport'] === 'smtp://smtp.gmail.com' + && $args['from'] === 'info@site.com'; + }) + ->andReturn($channel = m::mock(ChannelInterface::class)); + + $this->assertSame( + $channel, + $this->manager->getChannel('email_single') + ); + } + + public function testGetsEmailChannelWithMultipleTransport(): void + { + $this->factory->shouldReceive('make')->once() + ->withArgs(static function (string $type, array $args): bool { + return $type === EmailChannel::class + && $args['transport'] instanceof MailerRoundRobinTransport + && (string)$args['transport'] === 'roundrobin(smtp://smtp.gmail.com smtp://smtp.yahoo.com)' + && $args['from'] === 'info@site.com'; + }) + ->andReturn($channel = m::mock(ChannelInterface::class)); + + $this->assertSame( + $channel, + $this->manager->getChannel('email_multiple') + ); + } + + public function testGetsNotificationChannelWithSingleTransport(): void + { + $this->factory->shouldReceive('make')->once() + ->withArgs(static function (string $type, array $args): bool { + return $type === 'firebase' + && $args['transport'] instanceof FirebaseTransport + && (string)$args['transport'] === 'firebase://fcm.googleapis.com/fcm/send'; + }) + ->andReturn($channel = m::mock(ChannelInterface::class)); + + $this->assertSame( + $channel, + $this->manager->getChannel('firebase') + ); + } + + public function testGetsNotificationChannelWithMultipleTransport(): void + { + $this->factory->shouldReceive('make')->once() + ->withArgs(static function (string $type, array $args): bool { + return $type === 'firebase_multiple' + && $args['transport'] instanceof RoundRobinTransport; + }) + ->andReturn($channel = m::mock(ChannelInterface::class)); + + $this->assertSame( + $channel, + $this->manager->getChannel('firebase_multiple') + ); + } + + public function testUnknownTransportTypeShouldThrowAnException(): void + { + $this->expectException(UnsupportedSchemeException::class); + $this->expectErrorMessage('The "foo" scheme is not supported.'); + + $this->manager->getChannel('unknown'); + } + + public function testUnsupportedTransportTypeShouldThrowAnException(): void + { + $this->expectException(UnsupportedSchemeException::class); + $this->expectErrorMessage('Unable to send notification via "discord" as the bridge is not installed; try running "composer require symfony/discord-notifier".'); + + $this->manager->getChannel('unsupported'); + } +} diff --git a/tests/src/Config/NotificationsConfigTest.php b/tests/src/Config/NotificationsConfigTest.php new file mode 100644 index 0000000..af029a6 --- /dev/null +++ b/tests/src/Config/NotificationsConfigTest.php @@ -0,0 +1,186 @@ +expectException(TransportException::class); + $this->expectErrorMessage('Channel with given name `foo` is not found.'); + + $config->getChannel('foo'); + } + + public function testGetsChannelWithInvalidConfigShouldThrowAnException(): void + { + $config = new NotificationsConfig([ + 'channels' => [ + 'foo' => 'bar' + ], + ]); + + $this->expectException(InvalidArgumentException::class); + $this->expectErrorMessage('Config for channel `foo` must be an array.'); + + $config->getChannel('foo'); + } + + public function testGetsChannelWithoutTypeShouldThrowAnException(): void + { + $config = new NotificationsConfig([ + 'channels' => [ + 'foo' => [] + ], + ]); + + $this->expectException(InvalidArgumentException::class); + $this->expectErrorMessage('Config for channel `foo` should contain `type` key.'); + + $config->getChannel('foo'); + } + + public function testGetsChannelWithoutTransportShouldThrowAnException(): void + { + $config = new NotificationsConfig([ + 'channels' => [ + 'foo' => [ + 'type' => 'bar' + ] + ], + ]); + + $this->expectException(InvalidArgumentException::class); + $this->expectErrorMessage('Config for channel `foo` should contain `transport` key.'); + + $config->getChannel('foo'); + } + + public function testGetsChannel(): void + { + $config = new NotificationsConfig([ + 'channels' => [ + 'foo' => [ + 'type' => 'bar', + 'transport' => 'bar' + ] + ], + 'transports' => [ + 'bar' => 'foo://bar', + ], + ]); + + $channel = $config->getChannel('foo'); + + $this->assertSame('bar', $channel['type']); + $this->assertSame('foo://bar', $channel['transport'][0]->getOriginalDsn()); + } + + public function testGetsChannelWithTypeAlias(): void + { + $config = new NotificationsConfig([ + 'channels' => [ + 'foo' => [ + 'type' => 'bar', + 'transport' => ['bar', 'foo'] + ] + ], + 'transports' => [ + 'foo' => 'foo://baz', + 'bar' => 'foo://bar', + ], + 'typeAliases' => [ + 'bar' => EmailChannel::class + ] + ]); + + $channel = $config->getChannel('foo'); + + $this->assertSame(EmailChannel::class, $channel['type']); + $this->assertSame('foo://bar', $channel['transport'][0]->getOriginalDsn()); + $this->assertSame('foo://baz', $channel['transport'][1]->getOriginalDsn()); + } + + public function testGetsQueueConnection(): void + { + $config = new NotificationsConfig([ + 'queueConnection' => 'foo', + ]); + $this->assertSame('foo', $config->getQueueConnection()); + + + $config = new NotificationsConfig([]); + $this->assertNull($config->getQueueConnection()); + } + + public function testGetsChannelPolicies(): void + { + $config = new NotificationsConfig([ + 'policies' => ['foo'], + ]); + $this->assertSame(['foo'], $config->getChannelPolicies()); + + + $config = new NotificationsConfig([]); + $this->assertSame([], $config->getChannelPolicies()); + } + + public function testGetsTransport(): void + { + $config = new NotificationsConfig([ + 'transports' => [ + 'foo' => 'foo://bar', + 'baz' => 'baz://bar', + ], + ]); + + $this->assertSame(['foo://bar',], + \array_map(static function (Dsn $dsn): string { + return $dsn->getOriginalDsn(); + }, $config->getTransport(['foo'])) + ); + + $this->assertSame(['foo://bar', 'baz://bar',], + \array_map(static function (Dsn $dsn): string { + return $dsn->getOriginalDsn(); + }, $config->getTransport(['foo', 'baz'])) + ); + } + + public function testGetsUnknownTransportShouldThrowAnException(): void + { + $this->expectException(TransportException::class); + $this->expectErrorMessage('Transport with given name `foo` is not found.'); + + $config = new NotificationsConfig([ + 'transports' => [], + ]); + + $config->getTransport(['foo']); + } + + public function testGetsInvalidTransportShouldThrowAnException(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectErrorMessage('Config for transport `foo` must be a DSN string'); + + $config = new NotificationsConfig([ + 'transports' => [ + 'foo' => [], + ], + ]); + + $config->getTransport(['foo']); + } +} diff --git a/tests/src/NotifierTest.php b/tests/src/NotifierTest.php index 4cc341b..d1611a1 100644 --- a/tests/src/NotifierTest.php +++ b/tests/src/NotifierTest.php @@ -4,13 +4,20 @@ namespace Spiral\Notifications\Tests; +use Mockery as m; +use Spiral\Notifications\SendNotificationJob; +use Spiral\Notifications\Tests\App\Notifications\UserNotification; use Spiral\Notifications\Tests\App\Notifications\UserRegisteredNotification; use Spiral\Notifications\Tests\App\Users\UserWithEmail; -use Spiral\Notifications\Tests\App\Users\UserWithPhone; +use Spiral\Queue\QueueConnectionProviderInterface; +use Spiral\Queue\QueueInterface; +use Spiral\Testing\Mailer\FakeMailer; use Symfony\Component\Notifier\Channel\EmailChannel; final class NotifierTest extends TestCase { + private FakeMailer $mailer; + protected function setUp(): void { parent::setUp(); @@ -18,7 +25,7 @@ protected function setUp(): void $this->mailer = $this->fakeMailer(); } - public function testSendEmailNotificationViaQueue() + public function testSendEmailNotification() { $notification = new UserRegisteredNotification(); $emailRecipient = new UserWithEmail(); @@ -29,4 +36,24 @@ public function testSendEmailNotificationViaQueue() $this->getNotifier()->send($notification, $emailRecipient); } + + public function testSendEmailNotificationViaQueue() + { + $this->mockContainer(QueueConnectionProviderInterface::class) + ->shouldReceive('getConnection') + ->once() + ->andReturn($queue = m::mock(QueueInterface::class)); + + + $notification = new UserNotification(); + $emailRecipient = new UserWithEmail(); + + $queue->shouldReceive('push')->once()->with(SendNotificationJob::class, [ + 'notification' => $notification, + 'recipient' => $emailRecipient, + 'transportName' => null, + ]); + + $this->getNotifier()->send($notification, $emailRecipient); + } } diff --git a/tests/src/SendNotificationJobTest.php b/tests/src/SendNotificationJobTest.php new file mode 100644 index 0000000..152a355 --- /dev/null +++ b/tests/src/SendNotificationJobTest.php @@ -0,0 +1,83 @@ +job = new SendNotificationJob( + $this->notifier = m::mock(NotifierInterface::class) + ); + } + + public function testPayloadWithoutNotificationKeyShouldThrowAnException(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectErrorMessage('Payload `notification` key is required.'); + + $this->job->handle('foo', 'bar', []); + } + + public function testInvalidPayloadNotificationKeyShouldThrowAnException(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectErrorMessage( + 'Payload `notification` key value type should be instance of `Symfony\Component\Notifier\Notification\Notification`' + ); + + $this->job->handle('foo', 'bar', [ + 'notification' => 'foo', + ]); + } + + public function testPayloadWithoutRecipientKeyShouldThrowAnException(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectErrorMessage('Payload `recipient` key is required.'); + + $this->job->handle('foo', 'bar', [ + 'notification' => m::mock(Notification::class), + ]); + } + + public function testInvalidPayloadRecipientKeyShouldThrowAnException(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectErrorMessage( + 'Payload `recipient` key value type should be instance of `Symfony\Component\Notifier\Recipient\RecipientInterface`' + ); + + $this->job->handle('foo', 'bar', [ + 'notification' => m::mock(Notification::class), + 'recipient' => 'foo', + ]); + } + + public function testNotificationShouldBeSent(): void + { + $notification = m::mock(Notification::class); + $recipient = m::mock(RecipientInterface::class); + + $this->notifier->shouldReceive('sendNow')->once()->with($notification, $recipient); + + $this->job->handle('foo', 'bar', [ + 'notification' => $notification, + 'recipient' => $recipient, + ]); + } +}