diff --git a/config/services/managers.yml b/config/services/managers.yml index e0f18d4e..edd3a30f 100644 --- a/config/services/managers.yml +++ b/config/services/managers.yml @@ -55,3 +55,8 @@ 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/repositories.yml b/config/services/repositories.yml index 5e4f4473..af452145 100644 --- a/config/services/repositories.yml +++ b/config/services/repositories.yml @@ -85,3 +85,18 @@ services: parent: PhpList\Core\Domain\Common\Repository\AbstractRepository arguments: - PhpList\Core\Domain\Analytics\Model\UserMessageView + + PhpList\Core\Domain\Analytics\Repository\LinkTrackUmlClickRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Analytics\Model\LinkTrackUmlClick + + PhpList\Core\Domain\Messaging\Repository\UserMessageRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Messaging\Model\UserMessage + + PhpList\Core\Domain\Subscription\Repository\SubscriberHistoryRepository: + parent: PhpList\Core\Domain\Common\Repository\AbstractRepository + arguments: + - PhpList\Core\Domain\Subscription\Model\SubscriberHistory diff --git a/src/Domain/Messaging/Model/UserMessageBounce.php b/src/Domain/Messaging/Model/UserMessageBounce.php index c842867f..ccb05597 100644 --- a/src/Domain/Messaging/Model/UserMessageBounce.php +++ b/src/Domain/Messaging/Model/UserMessageBounce.php @@ -25,7 +25,7 @@ class UserMessageBounce implements DomainModel, Identity private ?int $id = null; #[ORM\Column(name: 'user', type: 'integer')] - private int $user; + private int $userId; #[ORM\Column(name: 'message', type: 'integer')] private int $messageId; @@ -36,8 +36,9 @@ class UserMessageBounce implements DomainModel, Identity #[ORM\Column(name: 'time', type: 'datetime', options: ['default' => 'CURRENT_TIMESTAMP'])] private DateTime $createdAt; - public function __construct() + public function __construct(int $bounce) { + $this->bounce = $bounce; $this->createdAt = new DateTime(); } @@ -46,9 +47,9 @@ public function getId(): ?int return $this->id; } - public function getUser(): int + public function getUserId(): int { - return $this->user; + return $this->userId; } public function getMessageId(): int @@ -66,9 +67,9 @@ public function getCreatedAt(): DateTime return $this->createdAt; } - public function setUser(int $user): self + public function setUserId(int $userId): self { - $this->user = $user; + $this->userId = $userId; return $this; } diff --git a/src/Domain/Messaging/Model/UserMessageForward.php b/src/Domain/Messaging/Model/UserMessageForward.php index a9432e45..9b2a8ef4 100644 --- a/src/Domain/Messaging/Model/UserMessageForward.php +++ b/src/Domain/Messaging/Model/UserMessageForward.php @@ -23,7 +23,7 @@ class UserMessageForward implements DomainModel, Identity private ?int $id = null; #[ORM\Column(name: 'user', type: 'integer')] - private int $user; + private int $userId; #[ORM\Column(name: 'message', type: 'integer')] private int $messageId; @@ -47,9 +47,9 @@ public function getId(): ?int return $this->id; } - public function getUser(): int + public function getUserId(): int { - return $this->user; + return $this->userId; } public function getMessageId(): int @@ -72,9 +72,9 @@ public function getCreatedAt(): DateTime return $this->createdAt; } - public function setUser(int $user): self + public function setUserId(int $userId): self { - $this->user = $user; + $this->userId = $userId; return $this; } diff --git a/src/Domain/Subscription/Service/Manager/SubscriberManager.php b/src/Domain/Subscription/Service/Manager/SubscriberManager.php index 4c142b15..93420795 100644 --- a/src/Domain/Subscription/Service/Manager/SubscriberManager.php +++ b/src/Domain/Subscription/Service/Manager/SubscriberManager.php @@ -11,6 +11,7 @@ use PhpList\Core\Domain\Subscription\Model\Dto\UpdateSubscriberDto; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; +use PhpList\Core\Domain\Subscription\Service\SubscriberDeletionService; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Messenger\MessageBusInterface; @@ -19,15 +20,18 @@ class SubscriberManager private SubscriberRepository $subscriberRepository; private EntityManagerInterface $entityManager; private MessageBusInterface $messageBus; + private SubscriberDeletionService $subscriberDeletionService; public function __construct( SubscriberRepository $subscriberRepository, EntityManagerInterface $entityManager, - MessageBusInterface $messageBus + MessageBusInterface $messageBus, + SubscriberDeletionService $subscriberDeletionService ) { $this->subscriberRepository = $subscriberRepository; $this->entityManager = $entityManager; $this->messageBus = $messageBus; + $this->subscriberDeletionService = $subscriberDeletionService; } public function createSubscriber(CreateSubscriberDto $subscriberDto): Subscriber @@ -90,7 +94,7 @@ public function updateSubscriber(UpdateSubscriberDto $subscriberDto): Subscriber public function deleteSubscriber(Subscriber $subscriber): void { - $this->subscriberRepository->remove($subscriber); + $this->subscriberDeletionService->deleteLeavingBlacklist($subscriber); } public function createFromImport(ImportSubscriberDto $subscriberDto): Subscriber diff --git a/src/Domain/Subscription/Service/SubscriberDeletionService.php b/src/Domain/Subscription/Service/SubscriberDeletionService.php new file mode 100644 index 00000000..9681a49d --- /dev/null +++ b/src/Domain/Subscription/Service/SubscriberDeletionService.php @@ -0,0 +1,77 @@ +linkTrackUmlClickRepo = $linkTrackUmlClickRepo; + $this->entityManager = $entityManager; + $this->userMessageRepo = $userMessageRepo; + $this->subscriberAttrValueRepo = $subscriberAttrValueRepo; + $this->subscriberHistoryRepo = $subscriberHistoryRepo; + $this->userMessageBounceRepo = $userMessageBounceRepo; + $this->userMessageForwardRepo = $userMessageForwardRepo; + $this->userMessageViewRepo = $userMessageViewRepo; + $this->subscriptionRepo = $subscriptionRepo; + } + + public function deleteLeavingBlacklist(Subscriber $subscriber): void + { + $this->removeEntities($this->linkTrackUmlClickRepo->findBy(['userId' => $subscriber->getId()])); + $this->removeEntities($this->subscriptionRepo->findBy(['subscriber' => $subscriber])); + $this->removeEntities($this->userMessageRepo->findBy(['user' => $subscriber])); + $this->removeEntities($this->subscriberAttrValueRepo->findBy(['subscriber' => $subscriber])); + $this->removeEntities($this->subscriberHistoryRepo->findBy(['subscriber' => $subscriber])); + $this->removeEntities($this->userMessageBounceRepo->findBy(['userId' => $subscriber->getId()])); + $this->removeEntities($this->userMessageForwardRepo->findBy(['userId' => $subscriber->getId()])); + $this->removeEntities($this->userMessageViewRepo->findBy(['userId' => $subscriber->getId()])); + + $this->entityManager->remove($subscriber); + } + + /** + * Remove a collection of entities + * + * @param array $entities + */ + private function removeEntities(array $entities): void + { + foreach ($entities as $entity) { + $this->entityManager->remove($entity); + } + } +} diff --git a/tests/Integration/Domain/Subscription/Service/SubscriberDeletionServiceTest.php b/tests/Integration/Domain/Subscription/Service/SubscriberDeletionServiceTest.php new file mode 100644 index 00000000..e6d42236 --- /dev/null +++ b/tests/Integration/Domain/Subscription/Service/SubscriberDeletionServiceTest.php @@ -0,0 +1,149 @@ +loadSchema(); + + $this->subscriberDeletionService = self::getContainer()->get(SubscriberDeletionService::class); + $this->entityManager = self::getContainer()->get(EntityManagerInterface::class); + } + + protected function tearDown(): void + { + $schemaTool = new SchemaTool($this->entityManager); + $schemaTool->dropDatabase(); + parent::tearDown(); + } + + public function testDeleteSubscriberWithRelatedDataDoesNotThrowDoctrineError(): void + { + $admin = new Administrator(); + $this->entityManager->persist($admin); + + $msg = new Message( + format: new MessageFormat(true, MessageFormat::FORMAT_TEXT), + schedule: new MessageSchedule(1, null, 3, null, null), + metadata: new MessageMetadata('done'), + content: new MessageContent('Owned by Admin 1!'), + options: new MessageOptions(), + owner: $admin + ); + $this->entityManager->persist($msg); + + $subscriber = new Subscriber(); + $subscriber->setEmail('test-delete@example.com'); + $subscriber->setConfirmed(true); + $subscriber->setHtmlEmail(true); + $subscriber->setBlacklisted(false); + $subscriber->setDisabled(false); + $this->entityManager->persist($subscriber); + $this->entityManager->flush(); + + $subscriberId = $subscriber->getId(); + $this->assertNotNull($subscriberId, 'Subscriber ID should not be null'); + + $subscriberList = new SubscriberList(); + $subscriberList->setDescription('Test List Description'); + $this->entityManager->persist($subscriberList); + + $subscription = new Subscription(); + $subscription->setSubscriber($subscriber); + $subscription->setSubscriberList($subscriberList); + $this->entityManager->persist($subscription); + + $linkTrackUmlClick = new LinkTrackUmlClick(); + $linkTrackUmlClick->setMessageId(1); + $linkTrackUmlClick->setUserId($subscriberId); + $this->entityManager->persist($linkTrackUmlClick); + + $userMessage = new UserMessage($subscriber, $msg); + $userMessage->setStatus('sent'); + $this->entityManager->persist($userMessage); + + $userMessageBounce = new UserMessageBounce(1); + $userMessageBounce->setUserId($subscriberId); + $userMessageBounce->setMessageId(1); + $this->entityManager->persist($userMessageBounce); + + $userMessageForward = new UserMessageForward(); + $userMessageForward->setUserId($subscriberId); + $userMessageForward->setMessageId(1); + $this->entityManager->persist($userMessageForward); + + $userMessageView = new UserMessageView(); + $userMessageView->setMessageId(1); + $userMessageView->setUserid($subscriberId); + $this->entityManager->persist($userMessageView); + + $this->entityManager->flush(); + + try { + $this->subscriberDeletionService->deleteLeavingBlacklist($subscriber); + $this->entityManager->flush(); + $this->assertTrue(true, 'No exception was thrown'); + } catch (Exception $e) { + $this->fail('Exception was thrown: ' . $e->getMessage()); + } + + $deletedSubscriber = $this->entityManager->getRepository(Subscriber::class)->find($subscriberId); + $this->assertNull($deletedSubscriber, 'Subscriber should be deleted'); + + $subscriptionRepo = $this->entityManager->getRepository(Subscription::class); + $subscriptions = $subscriptionRepo->findBy(['subscriber' => $subscriber]); + $this->assertEmpty($subscriptions, 'Subscriptions should be deleted'); + + $linkTrackRepo = $this->entityManager->getRepository(LinkTrackUmlClick::class); + $linkTrackUmlClicks = $linkTrackRepo->findBy(['userId' => $subscriberId]); + $this->assertEmpty($linkTrackUmlClicks, 'LinkTrackUmlClicks should be deleted'); + + $userMessageRepo = $this->entityManager->getRepository(UserMessage::class); + $userMessages = $userMessageRepo->findBy(['user' => $subscriber]); + $this->assertEmpty($userMessages, 'UserMessages should be deleted'); + + $bounceRepo = $this->entityManager->getRepository(UserMessageBounce::class); + $userMessageBounces = $bounceRepo->findBy(['userId' => $subscriberId]); + $this->assertEmpty($userMessageBounces, 'UserMessageBounces should be deleted'); + + $forwardRepo = $this->entityManager->getRepository(UserMessageForward::class); + $userMessageForwards = $forwardRepo->findBy(['userId' => $subscriberId]); + $this->assertEmpty($userMessageForwards, 'UserMessageForwards should be deleted'); + + $viewRepo = $this->entityManager->getRepository(UserMessageView::class); + $userMessageViews = $viewRepo->findBy(['userId' => $subscriberId]); + $this->assertEmpty($userMessageViews, 'UserMessageViews should be deleted'); + } +} diff --git a/tests/Unit/Domain/Subscription/Service/SubscriberDeletionServiceTest.php b/tests/Unit/Domain/Subscription/Service/SubscriberDeletionServiceTest.php new file mode 100644 index 00000000..1ba1d7d0 --- /dev/null +++ b/tests/Unit/Domain/Subscription/Service/SubscriberDeletionServiceTest.php @@ -0,0 +1,200 @@ +linkTrackUmlClickRepository = $this->createMock(LinkTrackUmlClickRepository::class); + $this->entityManager = $this->createMock(EntityManagerInterface::class); + $this->userMessageRepository = $this->createMock(UserMessageRepository::class); + $this->subscriberAttributeValueRepository = $this->createMock(SubscriberAttributeValueRepository::class); + $this->subscriberHistoryRepository = $this->createMock(SubscriberHistoryRepository::class); + $this->userMessageBounceRepository = $this->createMock(UserMessageBounceRepository::class); + $this->userMessageForwardRepository = $this->createMock(UserMessageForwardRepository::class); + $this->userMessageViewRepository = $this->createMock(UserMessageViewRepository::class); + $this->subscriptionRepository = $this->createMock(SubscriptionRepository::class); + + $this->service = new SubscriberDeletionService( + $this->linkTrackUmlClickRepository, + $this->entityManager, + $this->userMessageRepository, + $this->subscriberAttributeValueRepository, + $this->subscriberHistoryRepository, + $this->userMessageBounceRepository, + $this->userMessageForwardRepository, + $this->userMessageViewRepository, + $this->subscriptionRepository + ); + } + + public function testDeleteLeavingBlacklistRemovesAllRelatedData(): void + { + $subscriber = $this->createMock(Subscriber::class); + $subscriberId = 123; + $subscriber->method('getId')->willReturn($subscriberId); + + $subscription = $this->createMock(Subscription::class); + $this->subscriptionRepository + ->method('findBy') + ->with(['subscriber' => $subscriber]) + ->willReturn([$subscription]); + + $linkTrackUmlClick = $this->createMock(LinkTrackUmlClick::class); + $this->linkTrackUmlClickRepository + ->method('findBy') + ->with(['userId' => $subscriberId]) + ->willReturn([$linkTrackUmlClick]); + + $this->entityManager + ->expects($this->atLeast(1)) + ->method('remove'); + + $userMessage = $this->createMock(UserMessage::class); + $this->userMessageRepository + ->method('findBy') + ->with(['user' => $subscriber]) + ->willReturn([$userMessage]); + + $subscriberAttribute = $this->createMock(SubscriberAttributeValue::class); + $this->subscriberAttributeValueRepository + ->method('findBy') + ->with(['subscriber' => $subscriber]) + ->willReturn([$subscriberAttribute]); + + $subscriberHistory = $this->createMock(SubscriberHistory::class); + $this->subscriberHistoryRepository + ->method('findBy') + ->with(['subscriber' => $subscriber]) + ->willReturn([$subscriberHistory]); + + $userMessageBounce = $this->createMock(UserMessageBounce::class); + $this->userMessageBounceRepository + ->method('findBy') + ->with(['userId' => $subscriberId]) + ->willReturn([$userMessageBounce]); + + $userMessageForward = $this->createMock(UserMessageForward::class); + $this->userMessageForwardRepository + ->method('findBy') + ->with(['userId' => $subscriberId]) + ->willReturn([$userMessageForward]); + + $userMessageView = $this->createMock(UserMessageView::class); + $this->userMessageViewRepository + ->method('findBy') + ->with(['userId' => $subscriberId]) + ->willReturn([$userMessageView]); + + $this->service->deleteLeavingBlacklist($subscriber); + } + + public function testDeleteLeavingBlacklistHandlesEmptyRelatedData(): void + { + $subscriber = $this->createMock(Subscriber::class); + $subscriberId = 123; + $subscriber->method('getId')->willReturn($subscriberId); + + $this->subscriptionRepository + ->method('findBy') + ->with(['subscriber' => $subscriber]) + ->willReturn([]); + + $this->linkTrackUmlClickRepository + ->method('findBy') + ->with(['userId' => $subscriberId]) + ->willReturn([]); + + $this->userMessageRepository + ->method('findBy') + ->with(['user' => $subscriber]) + ->willReturn([]); + $this->userMessageRepository + ->expects($this->never()) + ->method('remove'); + + $this->subscriberAttributeValueRepository + ->method('findBy') + ->with(['subscriber' => $subscriber]) + ->willReturn([]); + $this->subscriberAttributeValueRepository + ->expects($this->never()) + ->method('remove'); + + $this->subscriberHistoryRepository + ->method('findBy') + ->with(['subscriber' => $subscriber]) + ->willReturn([]); + $this->subscriberHistoryRepository + ->expects($this->never()) + ->method('remove'); + + $this->userMessageBounceRepository + ->method('findBy') + ->with(['userId' => $subscriberId]) + ->willReturn([]); + $this->userMessageBounceRepository + ->expects($this->never()) + ->method('remove'); + + $this->userMessageForwardRepository + ->method('findBy') + ->with(['userId' => $subscriberId]) + ->willReturn([]); + $this->userMessageForwardRepository + ->expects($this->never()) + ->method('remove'); + + $this->userMessageViewRepository + ->method('findBy') + ->with(['userId' => $subscriberId]) + ->willReturn([]); + $this->userMessageViewRepository + ->expects($this->never()) + ->method('remove'); + + $this->entityManager + ->expects($this->once()) + ->method('remove') + ->with($subscriber); + + $this->service->deleteLeavingBlacklist($subscriber); + } +} diff --git a/tests/Unit/Domain/Subscription/Service/SubscriberManagerTest.php b/tests/Unit/Domain/Subscription/Service/SubscriberManagerTest.php index d5564434..7d246b7d 100644 --- a/tests/Unit/Domain/Subscription/Service/SubscriberManagerTest.php +++ b/tests/Unit/Domain/Subscription/Service/SubscriberManagerTest.php @@ -11,30 +11,60 @@ use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager; use PHPUnit\Framework\MockObject\MockObject; +use PhpList\Core\Domain\Subscription\Service\SubscriberDeletionService; use PHPUnit\Framework\TestCase; use Symfony\Component\Messenger\Envelope; use Symfony\Component\Messenger\MessageBusInterface; class SubscriberManagerTest extends TestCase { - private SubscriberRepository&MockObject $subscriberRepository; - private MessageBusInterface&MockObject $messageBus; + private SubscriberRepository|MockObject $subscriberRepository; + private EntityManagerInterface|MockObject $entityManager; + private MessageBusInterface|MockObject $messageBus; + private SubscriberDeletionService|MockObject $subscriberDeletionService; private SubscriberManager $subscriberManager; protected function setUp(): void { $this->subscriberRepository = $this->createMock(SubscriberRepository::class); - $entityManager = $this->createMock(EntityManagerInterface::class); + $this->entityManager = $this->createMock(EntityManagerInterface::class); $this->messageBus = $this->createMock(MessageBusInterface::class); + $this->subscriberDeletionService = $this->createMock(SubscriberDeletionService::class); $this->subscriberManager = new SubscriberManager( $this->subscriberRepository, - $entityManager, - $this->messageBus + $this->entityManager, + $this->messageBus, + $this->subscriberDeletionService ); } public function testCreateSubscriberPersistsAndReturnsProperlyInitializedEntity(): void + { + $this->subscriberRepository + ->expects($this->once()) + ->method('save') + ->with($this->callback(function (Subscriber $sub): bool { + return $sub->getEmail() === 'foo@bar.com' + && $sub->isConfirmed() === true + && $sub->isBlacklisted() === false + && $sub->hasHtmlEmail() === true + && $sub->isDisabled() === false; + })); + + $dto = new CreateSubscriberDto(email: 'foo@bar.com', requestConfirmation: false, htmlEmail: true); + + $result = $this->subscriberManager->createSubscriber($dto); + + $this->assertInstanceOf(Subscriber::class, $result); + $this->assertSame('foo@bar.com', $result->getEmail()); + $this->assertTrue($result->isConfirmed()); + $this->assertFalse($result->isBlacklisted()); + $this->assertTrue($result->hasHtmlEmail()); + $this->assertFalse($result->isDisabled()); + } + + public function testCreateSubscriberPersistsAndSendsEmail(): void { $this->subscriberRepository ->expects($this->once()) @@ -51,7 +81,9 @@ public function testCreateSubscriberPersistsAndReturnsProperlyInitializedEntity( $this->messageBus ->expects($this->once()) ->method('dispatch') - ->willReturn(new Envelope(new SubscriberConfirmationMessage('foo@bar.com', 'test-unique-id-456'))); + ->willReturnCallback(function ($message) { + return new Envelope($message); + }); $dto = new CreateSubscriberDto(email: 'foo@bar.com', requestConfirmation: true, htmlEmail: true); @@ -85,7 +117,9 @@ public function testCreateSubscriberWithConfirmationSendsConfirmationEmail(): vo $this->assertTrue($message->hasHtmlEmail()); return true; })) - ->willReturn(new Envelope(new SubscriberConfirmationMessage('foo@bar.com', 'test-unique-id-456'))); + ->willReturnCallback(function ($message) { + return new Envelope($message); + }); $dto = new CreateSubscriberDto(email: 'test@example.com', requestConfirmation: true, htmlEmail: true); $this->subscriberManager->createSubscriber($dto);