diff --git a/config/services/normalizers.yml b/config/services/normalizers.yml index e27e1e4..1d16cc3 100644 --- a/config/services/normalizers.yml +++ b/config/services/normalizers.yml @@ -27,6 +27,10 @@ services: tags: [ 'serializer.normalizer' ] autowire: true + PhpList\RestBundle\Subscription\Serializer\SubscriberHistoryNormalizer: + tags: [ 'serializer.normalizer' ] + autowire: true + PhpList\RestBundle\Subscription\Serializer\SubscriptionNormalizer: tags: [ 'serializer.normalizer' ] autowire: true @@ -43,6 +47,10 @@ services: tags: [ 'serializer.normalizer' ] autowire: true + PhpList\RestBundle\Messaging\Serializer\ListMessageNormalizer: + tags: [ 'serializer.normalizer' ] + autowire: true + PhpList\RestBundle\Identity\Serializer\AdministratorNormalizer: tags: [ 'serializer.normalizer' ] autowire: true diff --git a/src/Messaging/Controller/ListMessageController.php b/src/Messaging/Controller/ListMessageController.php new file mode 100644 index 0000000..0ee3eb9 --- /dev/null +++ b/src/Messaging/Controller/ListMessageController.php @@ -0,0 +1,478 @@ +listMessageManager = $listMessageManager; + $this->listMessageNormalizer = $listMessageNormalizer; + $this->subscriberListNormalizer = $subscriberListNormalizer; + $this->messageNormalizer = $messageNormalizer; + } + + #[Route( + '/message/{messageId}/lists', + name: 'get_lists_by_message', + requirements: ['messageId' => '\d+'], + methods: ['GET'] + )] + #[OA\Get( + path: '/api/v2/list-messages/message/{messageId}/lists', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Returns a list of subscriber lists associated with a message.', + tags: ['list-messages'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'messageId', + description: 'Message ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: 'items', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/SubscriberList') + ), + ], + type: 'object' + ) + ), + new OA\Response( + response: 403, + description: 'Unauthorized', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Message not found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function getListsByMessage( + Request $request, + #[MapEntity(mapping: ['messageId' => 'id'])] ?Message $message = null + ): JsonResponse { + $this->requireAuthentication($request); + + if ($message === null) { + throw $this->createNotFoundException('Message not found.'); + } + + $subscriberLists = array_map(function (SubscriberList $list) { + return $this->subscriberListNormalizer->normalize($list); + }, $this->listMessageManager->getListByMessage($message)); + + return $this->json( + data: ['items' => $subscriberLists], + status: Response::HTTP_OK + ); + } + + #[Route( + '/list/{listId}/messages', + name: 'get_messages_by_list', + requirements: ['listId' => '\d+'], + methods: ['GET'] + )] + #[OA\Get( + path: '/api/v2/list-messages/list/{listId}/messages', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Returns a list of message IDs associated with a subscriber list.', + tags: ['list-messages'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'listId', + description: 'Subscriber List ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: 'items', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/Message') + ), + ], + type: 'object' + ) + ), + new OA\Response( + response: 403, + description: 'Unauthorized', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Subscriber list not found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function getMessagesByList( + Request $request, + #[MapEntity(mapping: ['listId' => 'id'])] ?SubscriberList $subscriberList = null + ): JsonResponse { + $this->requireAuthentication($request); + + if ($subscriberList === null) { + throw $this->createNotFoundException('Subscriber list not found.'); + } + + $messages = array_map(function (Message $message) { + return $this->messageNormalizer->normalize($message); + }, $this->listMessageManager->getMessagesByList($subscriberList)); + + return $this->json( + data:['items' => $messages], + status: Response::HTTP_OK + ); + } + + #[Route( + '/message/{messageId}/list/{listId}', + name: 'associate', + requirements: ['messageId' => '\d+', 'listId' => '\d+'], + methods: ['POST'] + )] + #[OA\Post( + path: '/api/v2/list-messages/message/{messageId}/list/{listId}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Associates a message with a subscriber list.', + tags: ['list-messages'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'messageId', + description: 'Message ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + new OA\Parameter( + name: 'listId', + description: 'Subscriber List ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ) + ], + responses: [ + new OA\Response( + response: 201, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/ListMessage') + ), + new OA\Response( + response: 403, + description: 'Unauthorized', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Message or subscriber list not found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function associateMessageWithList( + Request $request, + #[MapEntity(mapping: ['messageId' => 'id'])] ?Message $message = null, + #[MapEntity(mapping: ['listId' => 'id'])] ?SubscriberList $subscriberList = null + ): JsonResponse { + $this->requireAuthentication($request); + + if ($message === null) { + throw $this->createNotFoundException('Message not found.'); + } + + if ($subscriberList === null) { + throw $this->createNotFoundException('Subscriber list not found.'); + } + + $listMessage = $this->listMessageManager->associateMessageWithList($message, $subscriberList); + + return $this->json( + data: $this->listMessageNormalizer->normalize($listMessage), + status: Response::HTTP_CREATED + ); + } + + #[Route( + '/message/{messageId}/list/{listId}', + name: 'disassociate', + requirements: ['messageId' => '\d+', 'listId' => '\d+'], + methods: ['DELETE'] + )] + #[OA\Delete( + path: '/api/v2/list-messages/message/{messageId}/list/{listId}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Disassociates a message from a subscriber list.', + tags: ['list-messages'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'messageId', + description: 'Message ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + new OA\Parameter( + name: 'listId', + description: 'Subscriber List ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ) + ], + responses: [ + new OA\Response( + response: 204, + description: 'Success, no content' + ), + new OA\Response( + response: 403, + description: 'Unauthorized', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Message or subscriber list not found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function disassociateMessageFromList( + Request $request, + #[MapEntity(mapping: ['messageId' => 'id'])] ?Message $message = null, + #[MapEntity(mapping: ['listId' => 'id'])] ?SubscriberList $subscriberList = null + ): JsonResponse { + $this->requireAuthentication($request); + + if ($message === null) { + throw $this->createNotFoundException('Message not found.'); + } + + if ($subscriberList === null) { + throw $this->createNotFoundException('Subscriber list not found.'); + } + + $this->listMessageManager->removeAssociation($message, $subscriberList); + + return $this->json(null, Response::HTTP_NO_CONTENT); + } + + #[Route( + '/message/{messageId}/lists', + name: 'remove_all_lists', + requirements: ['messageId' => '\d+'], + methods: ['DELETE'] + )] + #[OA\Delete( + path: '/api/v2/list-messages/message/{messageId}/lists', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Removes all list associations for a message.', + tags: ['list-messages'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'messageId', + description: 'Message ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ) + ], + responses: [ + new OA\Response( + response: 204, + description: 'Success, no content' + ), + new OA\Response( + response: 403, + description: 'Unauthorized', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Message not found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function removeAllListAssociationsForMessage( + Request $request, + #[MapEntity(mapping: ['messageId' => 'id'])] ?Message $message = null + ): JsonResponse { + $this->requireAuthentication($request); + + if ($message === null) { + throw $this->createNotFoundException('Message not found.'); + } + + $this->listMessageManager->removeAllListAssociationsForMessage($message); + + return $this->json(null, Response::HTTP_NO_CONTENT); + } + + #[Route( + '/message/{messageId}/list/{listId}/check', + name: 'check_association', + requirements: ['messageId' => '\d+', 'listId' => '\d+'], + methods: ['GET'] + )] + #[OA\Get( + path: '/api/v2/list-messages/message/{messageId}/list/{listId}/check', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Checks if a message is associated with a subscriber list.', + tags: ['list-messages'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'messageId', + description: 'Message ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + new OA\Parameter( + name: 'listId', + description: 'Subscriber List ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'is_associated', type: 'boolean') + ], + type: 'object' + ) + ), + new OA\Response( + response: 403, + description: 'Unauthorized', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Message or subscriber list not found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function checkAssociation( + Request $request, + #[MapEntity(mapping: ['messageId' => 'id'])] ?Message $message = null, + #[MapEntity(mapping: ['listId' => 'id'])] ?SubscriberList $subscriberList = null + ): JsonResponse { + $this->requireAuthentication($request); + + if ($message === null) { + throw $this->createNotFoundException('Message not found.'); + } + + if ($subscriberList === null) { + throw $this->createNotFoundException('Subscriber list not found.'); + } + + $isAssociated = $this->listMessageManager->isMessageAssociatedWithList($message, $subscriberList); + + return $this->json(['is_associated' => $isAssociated], Response::HTTP_OK); + } +} diff --git a/src/Messaging/Serializer/ListMessageNormalizer.php b/src/Messaging/Serializer/ListMessageNormalizer.php new file mode 100644 index 0000000..8534827 --- /dev/null +++ b/src/Messaging/Serializer/ListMessageNormalizer.php @@ -0,0 +1,44 @@ + $object->getId(), + 'message' => $this->messageNormalizer->normalize($object->getMessage()), + 'subscriber_list' => $this->subscriberListNormalizer->normalize($object->getList()), + 'created_at' => $object->getEntered()->format('Y-m-d\TH:i:sP'), + 'updated_at' => $object->getUpdatedAt()->format('Y-m-d\TH:i:sP'), + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function supportsNormalization($data, string $format = null): bool + { + return $data instanceof ListMessage; + } +} diff --git a/src/Subscription/Controller/SubscriberController.php b/src/Subscription/Controller/SubscriberController.php index d633d51..fc7be66 100644 --- a/src/Subscription/Controller/SubscriberController.php +++ b/src/Subscription/Controller/SubscriberController.php @@ -380,15 +380,16 @@ public function deleteSubscriber( path: '/api/v2/subscribers/confirm', description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', summary: 'Confirm a subscriber by uniqueId.', - requestBody: new OA\RequestBody( - required: true, - content: new OA\JsonContent( - properties: [ - new OA\Property(property: 'uniqueId', type: 'string', example: 'e9d8c9b2e6') - ] - ) - ), tags: ['subscribers'], + parameters: [ + new OA\Parameter( + name: 'uniqueId', + description: 'Unique identifier for the subscriber confirmation', + in: 'query', + required: true, + schema: new OA\Schema(type: 'string', example: 'e9d8c9b2e6') + ) + ], responses: [ new OA\Response( response: 200, diff --git a/src/Subscription/OpenApi/SwaggerSchemasResponse.php b/src/Subscription/OpenApi/SwaggerSchemasResponse.php index e2f4c38..8376495 100644 --- a/src/Subscription/OpenApi/SwaggerSchemasResponse.php +++ b/src/Subscription/OpenApi/SwaggerSchemasResponse.php @@ -122,6 +122,26 @@ ], type: 'object' )] +#[OA\Schema( + schema: 'ListMessage', + properties: [ + new OA\Property(property: 'id', type: 'integer', example: 1), + new OA\Property(property: 'message', ref: '#/components/schemas/Message'), + new OA\Property(property: 'subscriber_list', ref: '#/components/schemas/SubscriberList'), + new OA\Property( + property: 'created_at', + type: 'string', + format: 'date-time', + example: '2022-12-01T10:00:00Z' + ), + new OA\Property( + property: 'updated_at', + type: 'string', + format: 'date-time', + example: '2022-12-01T10:00:00Z' + ), + ], +)] class SwaggerSchemasResponse { } diff --git a/src/Subscription/Service/SubscriberHistoryService.php b/src/Subscription/Service/SubscriberHistoryService.php index c37d179..dbe3b60 100644 --- a/src/Subscription/Service/SubscriberHistoryService.php +++ b/src/Subscription/Service/SubscriberHistoryService.php @@ -10,16 +10,16 @@ use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Model\SubscriberHistory; use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider; +use PhpList\RestBundle\Subscription\Serializer\SubscriberHistoryNormalizer; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; -use Symfony\Component\Serializer\Normalizer\NormalizerInterface; use Symfony\Component\Validator\Exception\ValidatorException; class SubscriberHistoryService { public function __construct( private readonly PaginatedDataProvider $paginatedDataProvider, - private readonly NormalizerInterface $serializer, + private readonly SubscriberHistoryNormalizer $serializer, ) { } diff --git a/tests/Integration/Messaging/Controller/ListMessageControllerTest.php b/tests/Integration/Messaging/Controller/ListMessageControllerTest.php new file mode 100644 index 0000000..ad1cc13 --- /dev/null +++ b/tests/Integration/Messaging/Controller/ListMessageControllerTest.php @@ -0,0 +1,263 @@ +messageRepository = self::getContainer()->get(MessageRepository::class); + $this->subscriberListRepository = self::getContainer()->get(SubscriberListRepository::class); + $this->listMessageManager = self::getContainer()->get(ListMessageManager::class); + + $this->loadFixtures([ + MessageFixture::class, + SubscriberListFixture::class, + ]); + } + + public function testControllerIsAvailableViaContainer(): void + { + self::assertInstanceOf( + ListMessageController::class, + self::getContainer()->get(ListMessageController::class) + ); + } + + public function testGetListsByMessageWithoutSessionKeyReturnsForbiddenStatus(): void + { + $this->jsonRequest('get', '/api/v2/list-messages/message/1/lists'); + + $this->assertHttpForbidden(); + } + + public function testGetListsByMessageWithInvalidMessageIdReturnsNotFoundStatus(): void + { + $this->authenticatedJsonRequest('get', '/api/v2/list-messages/message/999/lists'); + + $this->assertHttpNotFound(); + } + + public function testGetListsByMessageWithValidMessageIdReturnsLists(): void + { + $message = $this->messageRepository->findOneBy([]); + $subscriberList = $this->subscriberListRepository->findOneBy([]); + + $this->listMessageManager->associateMessageWithList($message, $subscriberList); + + $this->authenticatedJsonRequest('get', '/api/v2/list-messages/message/' . $message->getId() . '/lists'); + + $this->assertHttpOkay(); + + $responseContent = $this->getDecodedJsonResponseContent(); + self::assertArrayHasKey('items', $responseContent); + self::assertIsArray($responseContent['items']); + self::assertNotEmpty($responseContent['items']); + } + + public function testGetMessagesByListWithoutSessionKeyReturnsForbiddenStatus(): void + { + $this->jsonRequest('get', '/api/v2/list-messages/list/1/messages'); + + $this->assertHttpForbidden(); + } + + public function testGetMessagesByListWithInvalidListIdReturnsNotFoundStatus(): void + { + $this->authenticatedJsonRequest('get', '/api/v2/list-messages/list/999/messages'); + + $this->assertHttpNotFound(); + } + + public function testGetMessagesByListWithValidListIdReturnsMessages(): void + { + $message = $this->messageRepository->findOneBy([]); + $subscriberList = $this->subscriberListRepository->findOneBy([]); + + $this->listMessageManager->associateMessageWithList($message, $subscriberList); + + $this->authenticatedJsonRequest('get', '/api/v2/list-messages/list/' . $subscriberList->getId() . '/messages'); + + $this->assertHttpOkay(); + + $responseContent = $this->getDecodedJsonResponseContent(); + self::assertArrayHasKey('items', $responseContent); + self::assertIsArray($responseContent['items']); + } + + public function testAssociateMessageWithListWithoutSessionKeyReturnsForbiddenStatus(): void + { + $this->jsonRequest('post', '/api/v2/list-messages/message/1/list/1'); + + $this->assertHttpForbidden(); + } + + public function testAssociateMessageWithListWithInvalidMessageIdReturnsNotFoundStatus(): void + { + $subscriberList = $this->subscriberListRepository->findOneBy([]); + + $this->authenticatedJsonRequest('post', '/api/v2/list-messages/message/999/list/' . $subscriberList->getId()); + + $this->assertHttpNotFound(); + } + + public function testAssociateMessageWithListWithInvalidListIdReturnsNotFoundStatus(): void + { + $message = $this->messageRepository->findOneBy([]); + + $this->authenticatedJsonRequest('post', '/api/v2/list-messages/message/' . $message->getId() . '/list/999'); + + $this->assertHttpNotFound(); + } + + public function testAssociateMessageWithListWithValidIdsCreatesAssociation(): void + { + $message = $this->messageRepository->findOneBy([]); + $subscriberList = $this->subscriberListRepository->findOneBy([]); + + $this->authenticatedJsonRequest( + 'post', + '/api/v2/list-messages/message/' . $message->getId() . '/list/' . $subscriberList->getId() + ); + + $this->assertHttpCreated(); + + $responseContent = $this->getDecodedJsonResponseContent(); + self::assertArrayHasKey('id', $responseContent); + self::assertArrayHasKey('message', $responseContent); + self::assertArrayHasKey('subscriber_list', $responseContent); + } + + public function testDisassociateMessageFromListWithoutSessionKeyReturnsForbiddenStatus(): void + { + $this->jsonRequest('delete', '/api/v2/list-messages/message/1/list/1'); + + $this->assertHttpForbidden(); + } + + public function testDisassociateMessageFromListWithInvalidMessageIdReturnsNotFoundStatus(): void + { + $subscriberList = $this->subscriberListRepository->findOneBy([]); + + $this->authenticatedJsonRequest( + 'delete', + '/api/v2/list-messages/message/999/list/' . $subscriberList->getId() + ); + + $this->assertHttpNotFound(); + } + + public function testDisassociateMessageFromListWithInvalidListIdReturnsNotFoundStatus(): void + { + $message = $this->messageRepository->findOneBy([]); + + $this->authenticatedJsonRequest('delete', '/api/v2/list-messages/message/' . $message->getId() . '/list/999'); + + $this->assertHttpNotFound(); + } + + public function testDisassociateMessageFromListWithValidIdsRemovesAssociation(): void + { + $message = $this->messageRepository->findOneBy([]); + $subscriberList = $this->subscriberListRepository->findOneBy([]); + + $this->listMessageManager->associateMessageWithList($message, $subscriberList); + + $this->authenticatedJsonRequest( + 'delete', + '/api/v2/list-messages/message/' . $message->getId() . '/list/' . $subscriberList->getId() + ); + + $this->assertHttpNoContent(); + } + + public function testRemoveAllListAssociationsForMessageWithoutSessionKeyReturnsForbiddenStatus(): void + { + $this->jsonRequest('delete', '/api/v2/list-messages/message/1/lists'); + + $this->assertHttpForbidden(); + } + + public function testRemoveAllListAssociationsForMessageWithInvalidMessageIdReturnsNotFoundStatus(): void + { + $this->authenticatedJsonRequest('delete', '/api/v2/list-messages/message/999/lists'); + + $this->assertHttpNotFound(); + } + + public function testRemoveAllListAssociationsForMessageWithValidIdRemovesAllAssociations(): void + { + $message = $this->messageRepository->findOneBy([]); + $subscriberList = $this->subscriberListRepository->findOneBy([]); + + $this->listMessageManager->associateMessageWithList($message, $subscriberList); + + $this->authenticatedJsonRequest('delete', '/api/v2/list-messages/message/' . $message->getId() . '/lists'); + + $this->assertHttpNoContent(); + } + + public function testCheckAssociationWithoutSessionKeyReturnsForbiddenStatus(): void + { + $this->jsonRequest('get', '/api/v2/list-messages/message/1/list/1/check'); + + $this->assertHttpForbidden(); + } + + public function testCheckAssociationWithInvalidMessageIdReturnsNotFoundStatus(): void + { + $subscriberList = $this->subscriberListRepository->findOneBy([]); + + $this->authenticatedJsonRequest( + 'get', + '/api/v2/list-messages/message/999/list/' . $subscriberList->getId() . '/check' + ); + + $this->assertHttpNotFound(); + } + + public function testCheckAssociationWithInvalidListIdReturnsNotFoundStatus(): void + { + $message = $this->messageRepository->findOneBy([]); + + $this->authenticatedJsonRequest( + 'get', + '/api/v2/list-messages/message/' . $message->getId() . '/list/999/check' + ); + + $this->assertHttpNotFound(); + } + + public function testCheckAssociationWithValidIdsReturnsAssociationStatus(): void + { + $message = $this->messageRepository->findOneBy([]); + $subscriberList = $this->subscriberListRepository->findOneBy([]); + + $this->authenticatedJsonRequest( + 'get', + '/api/v2/list-messages/message/' . $message->getId() . '/list/' . $subscriberList->getId() . '/check' + ); + + $this->assertHttpOkay(); + + $responseContent = $this->getDecodedJsonResponseContent(); + self::assertArrayHasKey('is_associated', $responseContent); + self::assertIsBool($responseContent['is_associated']); + } +} diff --git a/tests/Unit/Messaging/Serializer/ListMessageNormalizerTest.php b/tests/Unit/Messaging/Serializer/ListMessageNormalizerTest.php new file mode 100644 index 0000000..823b115 --- /dev/null +++ b/tests/Unit/Messaging/Serializer/ListMessageNormalizerTest.php @@ -0,0 +1,83 @@ +messageNormalizer = $this->createMock(MessageNormalizer::class); + $this->subscriberListNormalizer = $this->createMock(SubscriberListNormalizer::class); + $this->normalizer = new ListMessageNormalizer( + $this->messageNormalizer, + $this->subscriberListNormalizer + ); + } + + public function testSupportsNormalization(): void + { + $listMessage = $this->createMock(ListMessage::class); + $this->assertTrue($this->normalizer->supportsNormalization($listMessage)); + $this->assertFalse($this->normalizer->supportsNormalization(new \stdClass())); + } + + public function testNormalizeReturnsExpectedArray(): void + { + $id = 123; + $entered = new DateTime('2023-01-01T10:00:00+00:00'); + $updatedAt = new DateTime('2023-01-02T10:00:00+00:00'); + + $message = $this->createMock(Message::class); + $subscriberList = $this->createMock(SubscriberList::class); + + $listMessage = $this->createMock(ListMessage::class); + $listMessage->method('getId')->willReturn($id); + $listMessage->method('getMessage')->willReturn($message); + $listMessage->method('getList')->willReturn($subscriberList); + $listMessage->method('getEntered')->willReturn($entered); + $listMessage->method('getUpdatedAt')->willReturn($updatedAt); + + $normalizedMessage = ['id' => 456, 'subject' => 'Test Message']; + $normalizedList = ['id' => 789, 'name' => 'Test List']; + + $this->messageNormalizer->expects($this->once()) + ->method('normalize') + ->with($this->identicalTo($message)) + ->willReturn($normalizedMessage); + + $this->subscriberListNormalizer->expects($this->once()) + ->method('normalize') + ->with($this->identicalTo($subscriberList)) + ->willReturn($normalizedList); + + $result = $this->normalizer->normalize($listMessage); + + $this->assertSame($id, $result['id']); + $this->assertSame($normalizedMessage, $result['message']); + $this->assertSame($normalizedList, $result['subscriber_list']); + $this->assertSame('2023-01-01T10:00:00+00:00', $result['created_at']); + $this->assertSame('2023-01-02T10:00:00+00:00', $result['updated_at']); + } + + public function testNormalizeWithInvalidObjectReturnsEmptyArray(): void + { + $this->assertSame([], $this->normalizer->normalize(new \stdClass())); + } +} diff --git a/tests/Unit/Subscription/Service/SubscriberHistoryServiceTest.php b/tests/Unit/Subscription/Service/SubscriberHistoryServiceTest.php new file mode 100644 index 0000000..ba729ef --- /dev/null +++ b/tests/Unit/Subscription/Service/SubscriberHistoryServiceTest.php @@ -0,0 +1,109 @@ +paginatedDataProvider = $this->createMock(PaginatedDataProvider::class); + $this->serializer = $this->createMock(SubscriberHistoryNormalizer::class); + + $this->subscriberHistoryService = new SubscriberHistoryService( + $this->paginatedDataProvider, + $this->serializer + ); + } + + public function testGetSubscriberHistoryThrowsExceptionWhenSubscriberIsNull(): void + { + $request = new Request(); + + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('Subscriber not found.'); + + $this->subscriberHistoryService->getSubscriberHistory($request, null); + } + + public function testGetSubscriberHistoryThrowsExceptionWhenDateFormatIsInvalid(): void + { + $request = new Request(['date_from' => 'invalid-date']); + $subscriber = $this->createMock(Subscriber::class); + + $this->expectException(ValidatorException::class); + $this->expectExceptionMessage('Invalid date format. Use format: Y-m-d'); + + $this->subscriberHistoryService->getSubscriberHistory($request, $subscriber); + } + + public function testGetSubscriberHistoryReturnsExpectedResult(): void + { + $request = new Request(['date_from' => '2023-01-01', 'ip' => '127.0.0.1', 'summery' => 'test']); + $subscriber = $this->createMock(Subscriber::class); + $expectedResult = ['items' => [], 'pagination' => []]; + + $this->paginatedDataProvider->expects($this->once()) + ->method('getPaginatedList') + ->with( + $this->identicalTo($request), + $this->identicalTo($this->serializer), + SubscriberHistory::class, + $this->callback(function (SubscriberHistoryFilter $filter) use ($subscriber) { + return $filter->getSubscriber() === $subscriber + && $filter->getIp() === '127.0.0.1' + && $filter->getDateFrom() instanceof DateTimeImmutable + && $filter->getDateFrom()->format('Y-m-d') === '2023-01-01' + && $filter->getSummery() === 'test'; + }) + ) + ->willReturn($expectedResult); + + $result = $this->subscriberHistoryService->getSubscriberHistory($request, $subscriber); + + $this->assertSame($expectedResult, $result); + } + + public function testGetSubscriberHistoryWithoutDateFromReturnsExpectedResult(): void + { + $request = new Request(['ip' => '127.0.0.1']); + $subscriber = $this->createMock(Subscriber::class); + $expectedResult = ['items' => [], 'pagination' => []]; + + $this->paginatedDataProvider->expects($this->once()) + ->method('getPaginatedList') + ->with( + $this->identicalTo($request), + $this->identicalTo($this->serializer), + SubscriberHistory::class, + $this->callback(function (SubscriberHistoryFilter $filter) use ($subscriber) { + return $filter->getSubscriber() === $subscriber + && $filter->getIp() === '127.0.0.1' + && $filter->getDateFrom() === null; + }) + ) + ->willReturn($expectedResult); + + $result = $this->subscriberHistoryService->getSubscriberHistory($request, $subscriber); + + $this->assertSame($expectedResult, $result); + } +} diff --git a/tests/Unit/Subscription/Service/SubscriberServiceTest.php b/tests/Unit/Subscription/Service/SubscriberServiceTest.php new file mode 100644 index 0000000..eef8d1a --- /dev/null +++ b/tests/Unit/Subscription/Service/SubscriberServiceTest.php @@ -0,0 +1,174 @@ +subscriberManager = $this->createMock(SubscriberManager::class); + $this->subscriberNormalizer = $this->createMock(SubscriberNormalizer::class); + $this->subscriberHistoryService = $this->createMock(SubscriberHistoryService::class); + + $this->subscriberService = new SubscriberService( + $this->subscriberManager, + $this->subscriberNormalizer, + $this->subscriberHistoryService + ); + } + + public function testCreateSubscriberReturnsNormalizedSubscriber(): void + { + $subscriberDto = $this->createMock(CreateSubscriberDto::class); + $createSubscriberRequest = $this->createMock(CreateSubscriberRequest::class); + $subscriber = $this->createMock(Subscriber::class); + $expectedResult = ['id' => 1, 'email' => 'test@example.com']; + + $createSubscriberRequest->expects($this->once()) + ->method('getDto') + ->willReturn($subscriberDto); + + $this->subscriberManager->expects($this->once()) + ->method('createSubscriber') + ->with($this->identicalTo($subscriberDto)) + ->willReturn($subscriber); + + $this->subscriberNormalizer->expects($this->once()) + ->method('normalize') + ->with($this->identicalTo($subscriber), 'json') + ->willReturn($expectedResult); + + $result = $this->subscriberService->createSubscriber($createSubscriberRequest); + + $this->assertSame($expectedResult, $result); + } + + public function testUpdateSubscriberReturnsNormalizedSubscriber(): void + { + $subscriberDto = $this->createMock(UpdateSubscriberDto::class); + $updateSubscriberRequest = $this->createMock(UpdateSubscriberRequest::class); + $subscriber = $this->createMock(Subscriber::class); + $expectedResult = ['id' => 1, 'email' => 'updated@example.com']; + + $updateSubscriberRequest->expects($this->once()) + ->method('getDto') + ->willReturn($subscriberDto); + + $this->subscriberManager->expects($this->once()) + ->method('updateSubscriber') + ->with($this->identicalTo($subscriberDto)) + ->willReturn($subscriber); + + $this->subscriberNormalizer->expects($this->once()) + ->method('normalize') + ->with($this->identicalTo($subscriber), 'json') + ->willReturn($expectedResult); + + $result = $this->subscriberService->updateSubscriber($updateSubscriberRequest); + + $this->assertSame($expectedResult, $result); + } + + public function testGetSubscriberReturnsNormalizedSubscriber(): void + { + $subscriberId = 1; + $subscriber = $this->createMock(Subscriber::class); + $expectedResult = ['id' => 1, 'email' => 'test@example.com']; + + $this->subscriberManager->expects($this->once()) + ->method('getSubscriber') + ->with($subscriberId) + ->willReturn($subscriber); + + $this->subscriberNormalizer->expects($this->once()) + ->method('normalize') + ->with($this->identicalTo($subscriber)) + ->willReturn($expectedResult); + + $result = $this->subscriberService->getSubscriber($subscriberId); + + $this->assertSame($expectedResult, $result); + } + + public function testGetSubscriberHistoryDelegatesToHistoryService(): void + { + $request = new Request(); + $subscriber = $this->createMock(Subscriber::class); + $expectedResult = ['items' => [], 'pagination' => []]; + + $this->subscriberHistoryService->expects($this->once()) + ->method('getSubscriberHistory') + ->with($this->identicalTo($request), $this->identicalTo($subscriber)) + ->willReturn($expectedResult); + + $result = $this->subscriberService->getSubscriberHistory($request, $subscriber); + + $this->assertSame($expectedResult, $result); + } + + public function testDeleteSubscriberCallsManagerDelete(): void + { + $subscriber = $this->createMock(Subscriber::class); + + $this->subscriberManager->expects($this->once()) + ->method('deleteSubscriber') + ->with($this->identicalTo($subscriber)); + + $this->subscriberService->deleteSubscriber($subscriber); + } + + public function testConfirmSubscriberWithEmptyUniqueIdReturnsNull(): void + { + $this->assertNull($this->subscriberService->confirmSubscriber('')); + } + + public function testConfirmSubscriberWithValidUniqueIdReturnsSubscriber(): void + { + $uniqueId = 'valid-unique-id'; + $subscriber = $this->createMock(Subscriber::class); + + $this->subscriberManager->expects($this->once()) + ->method('markAsConfirmedByUniqueId') + ->with($uniqueId) + ->willReturn($subscriber); + + $result = $this->subscriberService->confirmSubscriber($uniqueId); + + $this->assertSame($subscriber, $result); + } + + public function testConfirmSubscriberWithInvalidUniqueIdReturnsNull(): void + { + $uniqueId = 'invalid-unique-id'; + + $this->subscriberManager->expects($this->once()) + ->method('markAsConfirmedByUniqueId') + ->with($uniqueId) + ->willThrowException(new NotFoundHttpException()); + + $result = $this->subscriberService->confirmSubscriber($uniqueId); + + $this->assertNull($result); + } +}