diff --git a/composer.json b/composer.json index 2f898e3b..35b16fe8 100644 --- a/composer.json +++ b/composer.json @@ -66,7 +66,8 @@ "symfony/mailchimp-mailer": "^6.4", "symfony/sendgrid-mailer": "^6.4", "symfony/twig-bundle": "^6.4", - "symfony/messenger": "^6.4" + "symfony/messenger": "^6.4", + "symfony/lock": "^6.4" }, "require-dev": { "phpunit/phpunit": "^9.5", diff --git a/config/services/managers.yml b/config/services/managers.yml index edd3a30f..e0f18d4e 100644 --- a/config/services/managers.yml +++ b/config/services/managers.yml @@ -55,8 +55,3 @@ services: PhpList\Core\Domain\Configuration\Service\Manager\ConfigManager: autowire: true autoconfigure: true - - PhpList\Core\Domain\Subscription\Service\SubscriberDeletionService: - autowire: true - autoconfigure: true - public: true diff --git a/config/services/providers.yml b/config/services/providers.yml new file mode 100644 index 00000000..226c4e81 --- /dev/null +++ b/config/services/providers.yml @@ -0,0 +1,4 @@ +services: + PhpList\Core\Domain\Subscription\Service\Provider\SubscriberProvider: + autowire: true + autoconfigure: true diff --git a/config/services/repositories.yml b/config/services/repositories.yml index af452145..05662fed 100644 --- a/config/services/repositories.yml +++ b/config/services/repositories.yml @@ -100,3 +100,8 @@ services: parent: PhpList\Core\Domain\Common\Repository\AbstractRepository arguments: - PhpList\Core\Domain\Subscription\Model\SubscriberHistory + + PhpList\Core\Domain\Messaging\Repository\ListMessageRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Subscription\Model\ListMessage diff --git a/config/services/services.yml b/config/services/services.yml index c9a1e43f..7120b035 100644 --- a/config/services/services.yml +++ b/config/services/services.yml @@ -14,3 +14,13 @@ services: autoconfigure: true arguments: $defaultFromEmail: '%app.mailer_from%' + + PhpList\Core\Domain\Subscription\Service\SubscriberDeletionService: + autowire: true + autoconfigure: true + public: true + + PhpList\Core\Domain\Messaging\Service\MessageProcessingPreparator: + autowire: true + autoconfigure: true + public: true diff --git a/src/Domain/Messaging/Command/ProcessQueueCommand.php b/src/Domain/Messaging/Command/ProcessQueueCommand.php new file mode 100644 index 00000000..9f5a1aff --- /dev/null +++ b/src/Domain/Messaging/Command/ProcessQueueCommand.php @@ -0,0 +1,106 @@ +messageRepository = $messageRepository; + $this->mailer = $mailer; + $this->lockFactory = $lockFactory; + $this->entityManager = $entityManager; + $this->subscriberProvider = $subscriberProvider; + $this->messageProcessingPreparator = $messageProcessingPreparator; + } + + /** + * @SuppressWarnings("PHPMD.UnusedFormalParameter") + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $lock = $this->lockFactory->createLock('queue_processor'); + if (!$lock->acquire()) { + $output->writeln('Queue is already being processed by another instance.'); + + return Command::FAILURE; + } + + try { + $this->messageProcessingPreparator->ensureSubscribersHaveUuid($output); + $this->messageProcessingPreparator->ensureCampaignsHaveUuid($output); + + $campaigns = $this->messageRepository->findBy(['status' => 'submitted']); + + foreach ($campaigns as $campaign) { + $this->processCampaign($campaign, $output); + } + } finally { + $lock->release(); + } + + return Command::SUCCESS; + } + + private function processCampaign(Message $campaign, OutputInterface $output): void + { + $subscribers = $this->subscriberProvider->getSubscribersForMessage($campaign); + // todo: check $ISPrestrictions logic + foreach ($subscribers as $subscriber) { + if (!filter_var($subscriber->getEmail(), FILTER_VALIDATE_EMAIL)) { + continue; + } + + $email = (new Email()) + ->from('news@example.com') + ->to($subscriber->getEmail()) + ->subject($campaign->getContent()->getSubject()) + ->text($campaign->getContent()->getTextMessage()) + ->html($campaign->getContent()->getText()); + + try { + $this->mailer->send($email); + + // todo: log somewhere that this subscriber got email + } catch (Throwable $e) { + $output->writeln('Failed to send to: ' . $subscriber->getEmail()); + } + + usleep(100000); + } + + $campaign->getMetadata()->setStatus('sent'); + $this->entityManager->flush(); + } +} diff --git a/src/Domain/Messaging/Model/Message.php b/src/Domain/Messaging/Model/Message.php index 4031a1ad..46acffeb 100644 --- a/src/Domain/Messaging/Model/Message.php +++ b/src/Domain/Messaging/Model/Message.php @@ -125,6 +125,12 @@ public function getUuid(): ?string return $this->uuid; } + public function setUuid(string $uuid): self + { + $this->uuid = $uuid; + return $this; + } + public function getOwner(): ?Administrator { return $this->owner; diff --git a/src/Domain/Messaging/Repository/ListMessageRepository.php b/src/Domain/Messaging/Repository/ListMessageRepository.php index 555f5fe6..bdfd4f83 100644 --- a/src/Domain/Messaging/Repository/ListMessageRepository.php +++ b/src/Domain/Messaging/Repository/ListMessageRepository.php @@ -11,4 +11,15 @@ class ListMessageRepository extends AbstractRepository implements PaginatableRepositoryInterface { use CursorPaginationTrait; + + /** @return int[] */ + public function getListIdsByMessageId(int $messageId): array + { + return $this->createQueryBuilder('lm') + ->select('IDENTITY(lm.list)') + ->where('lm.messageId = :messageId') + ->setParameter('messageId', $messageId) + ->getQuery() + ->getSingleColumnResult(); + } } diff --git a/src/Domain/Messaging/Repository/MessageRepository.php b/src/Domain/Messaging/Repository/MessageRepository.php index 674db8a0..1cc65df6 100644 --- a/src/Domain/Messaging/Repository/MessageRepository.php +++ b/src/Domain/Messaging/Repository/MessageRepository.php @@ -12,6 +12,18 @@ class MessageRepository extends AbstractRepository implements PaginatableRepositoryInterface { + /** + * @return Message[] + */ + public function findCampaignsWithoutUuid(): array + { + return $this->createQueryBuilder('m') + ->where('m.uuid IS NULL OR m.uuid = :emptyString') + ->setParameter('emptyString', '') + ->getQuery() + ->getResult(); + } + public function getByOwnerId(int $ownerId): array { return $this->createQueryBuilder('m') diff --git a/src/Domain/Messaging/Service/MessageProcessingPreparator.php b/src/Domain/Messaging/Service/MessageProcessingPreparator.php new file mode 100644 index 00000000..f687fae0 --- /dev/null +++ b/src/Domain/Messaging/Service/MessageProcessingPreparator.php @@ -0,0 +1,55 @@ +entityManager = $entityManager; + $this->subscriberRepository = $subscriberRepository; + $this->messageRepository = $messageRepository; + } + + public function ensureSubscribersHaveUuid(OutputInterface $output): void + { + $subscribersWithoutUuid = $this->subscriberRepository->findSubscribersWithoutUuid(); + + $numSubscribers = count($subscribersWithoutUuid); + if ($numSubscribers > 0) { + $output->writeln(sprintf('Giving a UUID to %d subscribers, this may take a while', $numSubscribers)); + foreach ($subscribersWithoutUuid as $subscriber) { + $subscriber->setUniqueId(bin2hex(random_bytes(16))); + } + $this->entityManager->flush(); + } + } + + public function ensureCampaignsHaveUuid(OutputInterface $output): void + { + $campaignsWithoutUuid = $this->messageRepository->findCampaignsWithoutUuid(); + + $numCampaigns = count($campaignsWithoutUuid); + if ($numCampaigns > 0) { + $output->writeln(sprintf('Giving a UUID to %d campaigns', $numCampaigns)); + foreach ($campaignsWithoutUuid as $campaign) { + $campaign->setUuid(bin2hex(random_bytes(18))); + } + $this->entityManager->flush(); + } + } +} diff --git a/src/Domain/Subscription/Repository/SubscriberRepository.php b/src/Domain/Subscription/Repository/SubscriberRepository.php index 8da29cf8..ade837f6 100644 --- a/src/Domain/Subscription/Repository/SubscriberRepository.php +++ b/src/Domain/Subscription/Repository/SubscriberRepository.php @@ -19,6 +19,18 @@ */ class SubscriberRepository extends AbstractRepository implements PaginatableRepositoryInterface { + /** + * @return Subscriber[] + */ + public function findSubscribersWithoutUuid(): array + { + return $this->createQueryBuilder('s') + ->where('s.uniqueId IS NULL OR s.uniqueId = :emptyString') + ->setParameter('emptyString', '') + ->getQuery() + ->getResult(); + } + public function findOneByEmail(string $email): ?Subscriber { return $this->findOneBy(['email' => $email]); diff --git a/src/Domain/Subscription/Service/Provider/SubscriberProvider.php b/src/Domain/Subscription/Service/Provider/SubscriberProvider.php new file mode 100644 index 00000000..0fdd2f1c --- /dev/null +++ b/src/Domain/Subscription/Service/Provider/SubscriberProvider.php @@ -0,0 +1,45 @@ +listMessageRepository = $listMessageRepository; + $this->subscriberRepository = $subscriberRepository; + } + + /** + * Get subscribers for a message + * + * @param Message $message The message to get subscribers for + * @return Subscriber[] Array of subscribers + */ + public function getSubscribersForMessage(Message $message): array + { + $listIds = $this->listMessageRepository->getListIdsByMessageId($message->getId()); + + $subscribers = []; + foreach ($listIds as $listId) { + $listSubscribers = $this->subscriberRepository->getSubscribersBySubscribedListId($listId); + foreach ($listSubscribers as $subscriber) { + $subscribers[$subscriber->getId()] = $subscriber; + } + } + + return array_values($subscribers); + } +} diff --git a/tests/Unit/Domain/Messaging/Command/ProcessQueueCommandTest.php b/tests/Unit/Domain/Messaging/Command/ProcessQueueCommandTest.php new file mode 100644 index 00000000..9cca10db --- /dev/null +++ b/tests/Unit/Domain/Messaging/Command/ProcessQueueCommandTest.php @@ -0,0 +1,305 @@ +messageRepository = $this->createMock(MessageRepository::class); + $this->mailer = $this->createMock(MailerInterface::class); + $lockFactory = $this->createMock(LockFactory::class); + $this->entityManager = $this->createMock(EntityManagerInterface::class); + $this->subscriberProvider = $this->createMock(SubscriberProvider::class); + $this->messageProcessingPreparator = $this->createMock(MessageProcessingPreparator::class); + $this->lock = $this->createMock(LockInterface::class); + + $lockFactory->method('createLock') + ->with('queue_processor') + ->willReturn($this->lock); + + $command = new ProcessQueueCommand( + $this->messageRepository, + $this->mailer, + $lockFactory, + $this->entityManager, + $this->subscriberProvider, + $this->messageProcessingPreparator + ); + + $application = new Application(); + $application->add($command); + + $this->commandTester = new CommandTester($command); + } + + public function testExecuteWithLockAlreadyAcquired(): void + { + $this->lock->expects($this->once()) + ->method('acquire') + ->willReturn(false); + + $this->messageProcessingPreparator->expects($this->never()) + ->method('ensureSubscribersHaveUuid'); + + $this->commandTester->execute([]); + + $output = $this->commandTester->getDisplay(); + $this->assertStringContainsString('Queue is already being processed by another instance', $output); + $this->assertEquals(1, $this->commandTester->getStatusCode()); + } + + public function testExecuteWithNoCampaigns(): void + { + $this->lock->expects($this->once()) + ->method('acquire') + ->willReturn(true); + + $this->lock->expects($this->once()) + ->method('release'); + + $this->messageProcessingPreparator->expects($this->once()) + ->method('ensureSubscribersHaveUuid'); + + $this->messageProcessingPreparator->expects($this->once()) + ->method('ensureCampaignsHaveUuid'); + + $this->messageRepository->expects($this->once()) + ->method('findBy') + ->with(['status' => 'submitted']) + ->willReturn([]); + + $this->commandTester->execute([]); + + $this->assertEquals(0, $this->commandTester->getStatusCode()); + } + + public function testExecuteWithCampaigns(): void + { + $this->lock->expects($this->once()) + ->method('acquire') + ->willReturn(true); + + $this->lock->expects($this->once()) + ->method('release'); + + $this->messageProcessingPreparator->expects($this->once()) + ->method('ensureSubscribersHaveUuid'); + + $this->messageProcessingPreparator->expects($this->once()) + ->method('ensureCampaignsHaveUuid'); + + $campaign = $this->createMock(Message::class); + $metadata = $this->createMock(MessageMetadata::class); + $content = $this->createMock(MessageContent::class); + + $campaign->expects($this->any()) + ->method('getMetadata') + ->willReturn($metadata); + + $campaign->expects($this->any()) + ->method('getContent') + ->willReturn($content); + + $content->expects($this->any()) + ->method('getSubject') + ->willReturn('Test Subject'); + + $content->expects($this->any()) + ->method('getTextMessage') + ->willReturn('Test Text Message'); + + $content->expects($this->any()) + ->method('getText') + ->willReturn('

Test HTML Message

'); + + $metadata->expects($this->once()) + ->method('setStatus') + ->with('sent'); + + $subscriber = $this->createMock(Subscriber::class); + $subscriber->expects($this->any()) + ->method('getEmail') + ->willReturn('test@example.com'); + + $this->messageRepository->expects($this->once()) + ->method('findBy') + ->with(['status' => 'submitted']) + ->willReturn([$campaign]); + + $this->subscriberProvider->expects($this->once()) + ->method('getSubscribersForMessage') + ->with($campaign) + ->willReturn([$subscriber]); + + $this->mailer->expects($this->once()) + ->method('send') + ->with($this->callback(function (Email $email) { + $this->assertEquals('Test Subject', $email->getSubject()); + $this->assertEquals('Test Text Message', $email->getTextBody()); + $this->assertEquals('

Test HTML Message

', $email->getHtmlBody()); + + $toAddresses = $email->getTo(); + $this->assertCount(1, $toAddresses); + $this->assertEquals('test@example.com', $toAddresses[0]->getAddress()); + + $fromAddresses = $email->getFrom(); + $this->assertCount(1, $fromAddresses); + $this->assertEquals('news@example.com', $fromAddresses[0]->getAddress()); + + return true; + })); + + $this->entityManager->expects($this->once()) + ->method('flush'); + + $this->commandTester->execute([]); + + $this->assertEquals(0, $this->commandTester->getStatusCode()); + } + + public function testExecuteWithInvalidSubscriberEmail(): void + { + $this->lock->expects($this->once()) + ->method('acquire') + ->willReturn(true); + + $this->lock->expects($this->once()) + ->method('release'); + + $campaign = $this->createMock(Message::class); + $metadata = $this->createMock(MessageMetadata::class); + $content = $this->createMock(MessageContent::class); + + $campaign->expects($this->any()) + ->method('getMetadata') + ->willReturn($metadata); + + $campaign->expects($this->any()) + ->method('getContent') + ->willReturn($content); + + $metadata->expects($this->once()) + ->method('setStatus') + ->with('sent'); + + $invalidSubscriber = $this->createMock(Subscriber::class); + $invalidSubscriber->expects($this->any()) + ->method('getEmail') + ->willReturn('invalid-email'); + + $this->messageRepository->expects($this->once()) + ->method('findBy') + ->with(['status' => 'submitted']) + ->willReturn([$campaign]); + + $this->subscriberProvider->expects($this->once()) + ->method('getSubscribersForMessage') + ->with($campaign) + ->willReturn([$invalidSubscriber]); + + $this->mailer->expects($this->never()) + ->method('send'); + + $this->entityManager->expects($this->once()) + ->method('flush'); + + $this->commandTester->execute([]); + + $this->assertEquals(0, $this->commandTester->getStatusCode()); + } + + public function testExecuteWithMailerException(): void + { + $this->lock->expects($this->once()) + ->method('acquire') + ->willReturn(true); + + $this->lock->expects($this->once()) + ->method('release'); + + $campaign = $this->createMock(Message::class); + $metadata = $this->createMock(MessageMetadata::class); + $content = $this->createMock(MessageContent::class); + + $campaign->expects($this->any()) + ->method('getMetadata') + ->willReturn($metadata); + + $campaign->expects($this->any()) + ->method('getContent') + ->willReturn($content); + + $content->expects($this->any()) + ->method('getSubject') + ->willReturn('Test Subject'); + + $content->expects($this->any()) + ->method('getTextMessage') + ->willReturn('Test Text Message'); + + $content->expects($this->any()) + ->method('getText') + ->willReturn('

Test HTML Message

'); + + $metadata->expects($this->once()) + ->method('setStatus') + ->with('sent'); + + $subscriber = $this->createMock(Subscriber::class); + $subscriber->expects($this->any()) + ->method('getEmail') + ->willReturn('test@example.com'); + + $this->messageRepository->expects($this->once()) + ->method('findBy') + ->with(['status' => 'submitted']) + ->willReturn([$campaign]); + + $this->subscriberProvider->expects($this->once()) + ->method('getSubscribersForMessage') + ->with($campaign) + ->willReturn([$subscriber]); + + $this->mailer->expects($this->once()) + ->method('send') + ->willThrowException(new \Exception('Failed to send email')); + + $this->entityManager->expects($this->once()) + ->method('flush'); + + $this->commandTester->execute([]); + + $output = $this->commandTester->getDisplay(); + $this->assertStringContainsString('Failed to send to: test@example.com', $output); + $this->assertEquals(0, $this->commandTester->getStatusCode()); + } +} diff --git a/tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php b/tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php new file mode 100644 index 00000000..d76c653e --- /dev/null +++ b/tests/Unit/Domain/Messaging/Service/MessageProcessingPreparatorTest.php @@ -0,0 +1,126 @@ +entityManager = $this->createMock(EntityManagerInterface::class); + $this->subscriberRepository = $this->createMock(SubscriberRepository::class); + $this->messageRepository = $this->createMock(MessageRepository::class); + $this->output = $this->createMock(OutputInterface::class); + + $this->preparator = new MessageProcessingPreparator( + $this->entityManager, + $this->subscriberRepository, + $this->messageRepository + ); + } + + public function testEnsureSubscribersHaveUuidWithNoSubscribers(): void + { + $this->subscriberRepository->expects($this->once()) + ->method('findSubscribersWithoutUuid') + ->willReturn([]); + + $this->output->expects($this->never()) + ->method('writeln'); + + $this->entityManager->expects($this->never()) + ->method('flush'); + + $this->preparator->ensureSubscribersHaveUuid($this->output); + } + + public function testEnsureSubscribersHaveUuidWithSubscribers(): void + { + $subscriber1 = $this->createMock(Subscriber::class); + $subscriber2 = $this->createMock(Subscriber::class); + + $subscribers = [$subscriber1, $subscriber2]; + + $this->subscriberRepository->expects($this->once()) + ->method('findSubscribersWithoutUuid') + ->willReturn($subscribers); + + $this->output->expects($this->once()) + ->method('writeln') + ->with($this->stringContains('Giving a UUID to 2 subscribers')); + + $subscriber1->expects($this->once()) + ->method('setUniqueId') + ->with($this->isType('string')); + + $subscriber2->expects($this->once()) + ->method('setUniqueId') + ->with($this->isType('string')); + + $this->entityManager->expects($this->once()) + ->method('flush'); + + $this->preparator->ensureSubscribersHaveUuid($this->output); + } + + public function testEnsureCampaignsHaveUuidWithNoCampaigns(): void + { + $this->messageRepository->expects($this->once()) + ->method('findCampaignsWithoutUuid') + ->willReturn([]); + + $this->output->expects($this->never()) + ->method('writeln'); + + $this->entityManager->expects($this->never()) + ->method('flush'); + + $this->preparator->ensureCampaignsHaveUuid($this->output); + } + + public function testEnsureCampaignsHaveUuidWithCampaigns(): void + { + $campaign1 = $this->createMock(Message::class); + $campaign2 = $this->createMock(Message::class); + + $campaigns = [$campaign1, $campaign2]; + + $this->messageRepository->expects($this->once()) + ->method('findCampaignsWithoutUuid') + ->willReturn($campaigns); + + $this->output->expects($this->once()) + ->method('writeln') + ->with($this->stringContains('Giving a UUID to 2 campaigns')); + + $campaign1->expects($this->once()) + ->method('setUuid') + ->with($this->isType('string')); + + $campaign2->expects($this->once()) + ->method('setUuid') + ->with($this->isType('string')); + + $this->entityManager->expects($this->once()) + ->method('flush'); + + $this->preparator->ensureCampaignsHaveUuid($this->output); + } +} diff --git a/tests/Unit/Domain/Subscription/Service/Provider/SubscriberProviderTest.php b/tests/Unit/Domain/Subscription/Service/Provider/SubscriberProviderTest.php new file mode 100644 index 00000000..fafc80be --- /dev/null +++ b/tests/Unit/Domain/Subscription/Service/Provider/SubscriberProviderTest.php @@ -0,0 +1,141 @@ +listMessageRepository = $this->createMock(ListMessageRepository::class); + $this->subscriberRepository = $this->createMock(SubscriberRepository::class); + + $this->subscriberProvider = new SubscriberProvider( + $this->listMessageRepository, + $this->subscriberRepository + ); + } + + public function testGetSubscribersForMessageWithNoListsReturnsEmptyArray(): void + { + $message = $this->createMock(Message::class); + $message->method('getId')->willReturn(123); + + $this->listMessageRepository + ->expects($this->once()) + ->method('getListIdsByMessageId') + ->with(123) + ->willReturn([]); + + $this->subscriberRepository + ->expects($this->never()) + ->method('getSubscribersBySubscribedListId'); + + $result = $this->subscriberProvider->getSubscribersForMessage($message); + + $this->assertIsArray($result); + $this->assertEmpty($result); + } + + public function testGetSubscribersForMessageWithOneListButNoSubscribersReturnsEmptyArray(): void + { + $message = $this->createMock(Message::class); + $message->method('getId')->willReturn(123); + + $this->listMessageRepository + ->expects($this->once()) + ->method('getListIdsByMessageId') + ->with(123) + ->willReturn([456]); + + $this->subscriberRepository + ->expects($this->once()) + ->method('getSubscribersBySubscribedListId') + ->with(456) + ->willReturn([]); + + $result = $this->subscriberProvider->getSubscribersForMessage($message); + + $this->assertIsArray($result); + $this->assertEmpty($result); + } + + public function testGetSubscribersForMessageWithOneListAndSubscribersReturnsSubscribers(): void + { + $message = $this->createMock(Message::class); + $message->method('getId')->willReturn(123); + + $subscriber1 = $this->createMock(Subscriber::class); + $subscriber1->method('getId')->willReturn(1); + $subscriber2 = $this->createMock(Subscriber::class); + $subscriber2->method('getId')->willReturn(2); + + $this->listMessageRepository + ->expects($this->once()) + ->method('getListIdsByMessageId') + ->with(123) + ->willReturn([456]); + + $this->subscriberRepository + ->expects($this->once()) + ->method('getSubscribersBySubscribedListId') + ->with(456) + ->willReturn([$subscriber1, $subscriber2]); + + $result = $this->subscriberProvider->getSubscribersForMessage($message); + + $this->assertIsArray($result); + $this->assertCount(2, $result); + $this->assertSame($subscriber1, $result[0]); + $this->assertSame($subscriber2, $result[1]); + } + + public function testGetSubscribersForMessageWithMultipleListsAndOverlappingSubscribersReturnsUniqueSubscribers(): void + { + $message = $this->createMock(Message::class); + $message->method('getId')->willReturn(123); + + $subscriber1 = $this->createMock(Subscriber::class); + $subscriber1->method('getId')->willReturn(1); + $subscriber2 = $this->createMock(Subscriber::class); + $subscriber2->method('getId')->willReturn(2); + $subscriber3 = $this->createMock(Subscriber::class); + $subscriber3->method('getId')->willReturn(3); + + $this->listMessageRepository + ->expects($this->once()) + ->method('getListIdsByMessageId') + ->with(123) + ->willReturn([456, 789]); + + $this->subscriberRepository + ->expects($this->exactly(2)) + ->method('getSubscribersBySubscribedListId') + ->willReturnMap([ + [456, [$subscriber1, $subscriber2]], + [789, [$subscriber2, $subscriber3]], + ]); + + $result = $this->subscriberProvider->getSubscribersForMessage($message); + + $this->assertIsArray($result); + $this->assertCount(3, $result); + + $this->assertContains($subscriber1, $result); + $this->assertContains($subscriber2, $result); + $this->assertContains($subscriber3, $result); + } +}