From ad52a60a3a934cb754a07ae8773c46301a73d0f0 Mon Sep 17 00:00:00 2001 From: TatevikGr Date: Fri, 8 Aug 2025 22:30:01 +0400 Subject: [PATCH 01/12] Blacklist (#156) * blacklist controller * blacklist request * UserBlacklistNormalizer * add in development flags * phpcs fix * use blacklist branch * fix swagger * use core dev branch --------- Co-authored-by: Tatevik --- composer.json | 2 +- config/services/normalizers.yml | 4 + .../Controller/BlacklistController.php | 284 ++++++++++++++++++ .../Request/AddToBlacklistRequest.php | 23 ++ .../Serializer/UserBlacklistNormalizer.php | 42 +++ .../Controller/BlacklistControllerTest.php | 60 ++++ .../UserBlacklistNormalizerTest.php | 57 ++++ 7 files changed, 471 insertions(+), 1 deletion(-) create mode 100644 src/Subscription/Controller/BlacklistController.php create mode 100644 src/Subscription/Request/AddToBlacklistRequest.php create mode 100644 src/Subscription/Serializer/UserBlacklistNormalizer.php create mode 100644 tests/Integration/Subscription/Controller/BlacklistControllerTest.php create mode 100644 tests/Unit/Subscription/Serializer/UserBlacklistNormalizerTest.php diff --git a/composer.json b/composer.json index 5b8e2a7..ba5526c 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ }, "require": { "php": "^8.1", - "phplist/core": "dev-main", + "phplist/core": "dev-dev", "friendsofsymfony/rest-bundle": "*", "symfony/test-pack": "^1.0", "symfony/process": "^6.4", diff --git a/config/services/normalizers.yml b/config/services/normalizers.yml index 1d16cc3..a270a3f 100644 --- a/config/services/normalizers.yml +++ b/config/services/normalizers.yml @@ -93,3 +93,7 @@ services: PhpList\RestBundle\Statistics\Serializer\TopLocalPartsNormalizer: tags: [ 'serializer.normalizer' ] autowire: true + + PhpList\RestBundle\Subscription\Serializer\UserBlacklistNormalizer: + tags: [ 'serializer.normalizer' ] + autowire: true diff --git a/src/Subscription/Controller/BlacklistController.php b/src/Subscription/Controller/BlacklistController.php new file mode 100644 index 0000000..f76b094 --- /dev/null +++ b/src/Subscription/Controller/BlacklistController.php @@ -0,0 +1,284 @@ +authentication = $authentication; + $this->blacklistManager = $blacklistManager; + $this->normalizer = $normalizer; + } + + #[Route('/check/{email}', name: 'check', methods: ['GET'])] + #[OA\Get( + path: '/api/v2/blacklist/check/{email}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Check if email is blacklisted', + tags: ['blacklist'], + 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: 'email', + description: 'Email address to check', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'blacklisted', type: 'boolean'), + new OA\Property(property: 'reason', type: 'string', nullable: true) + ] + ), + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + ] + )] + public function checkEmailBlacklisted(Request $request, string $email): JsonResponse + { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to check blacklisted emails.'); + } + + $isBlacklisted = $this->blacklistManager->isEmailBlacklisted($email); + $reason = $isBlacklisted ? $this->blacklistManager->getBlacklistReason($email) : null; + + return $this->json([ + 'blacklisted' => $isBlacklisted, + 'reason' => $reason, + ]); + } + + #[Route('/add', name: 'add', methods: ['POST'])] + #[OA\Post( + path: '/api/v2/blacklist/add', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Adds an email address to the blacklist.', + requestBody: new OA\RequestBody( + description: 'Email to blacklist', + required: true, + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'email', type: 'string'), + new OA\Property(property: 'reason', type: 'string', nullable: true) + ] + ) + ), + tags: ['blacklist'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 201, + description: 'Success', + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'success', type: 'boolean'), + new OA\Property(property: 'message', type: 'string') + ] + ), + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 422, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/ValidationErrorResponse') + ), + ] + )] + public function addEmailToBlacklist(Request $request): JsonResponse + { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to add emails to blacklist.'); + } + + /** @var AddToBlacklistRequest $definitionRequest */ + $definitionRequest = $this->validator->validate($request, AddToBlacklistRequest::class); + + $userBlacklisted = $this->blacklistManager->addEmailToBlacklist( + email: $definitionRequest->email, + reasonData: $definitionRequest->reason + ); + $json = $this->normalizer->normalize($userBlacklisted, 'json'); + + return $this->json($json, Response::HTTP_CREATED); + } + + #[Route('/remove/{email}', name: 'remove', methods: ['DELETE'])] + #[OA\Delete( + path: '/api/v2/blacklist/remove/{email}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Removes an email address from the blacklist.', + tags: ['blacklist'], + 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: 'email', + description: 'Email address to remove from blacklist', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'success', type: 'boolean'), + new OA\Property(property: 'message', type: 'string') + ] + ), + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + ] + )] + public function removeEmailFromBlacklist(Request $request, string $email): JsonResponse + { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to remove emails from blacklist.'); + } + + $this->blacklistManager->removeEmailFromBlacklist($email); + + return $this->json(null, Response::HTTP_NO_CONTENT); + } + + #[Route('/info/{email}', name: 'info', methods: ['GET'])] + #[OA\Get( + path: '/api/v2/blacklist/info/{email}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Gets detailed information about a blacklisted email.', + tags: ['blacklist'], + 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: 'email', + description: 'Email address to get information for', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'email', type: 'string'), + new OA\Property(property: 'added', type: 'string', format: 'date-time', nullable: true), + new OA\Property(property: 'reason', type: 'string', nullable: true) + ] + ), + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/BadRequestResponse') + ), + ] + )] + public function getBlacklistInfo(Request $request, string $email): JsonResponse + { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to view blacklist information.'); + } + + $blacklistInfo = $this->blacklistManager->getBlacklistInfo($email); + if (!$blacklistInfo) { + return $this->json([ + 'error' => sprintf('Email %s is not blacklisted', $email) + ], Response::HTTP_NOT_FOUND); + } + + $reason = $this->blacklistManager->getBlacklistReason($email); + + return $this->json([ + 'email' => $blacklistInfo->getEmail(), + 'added' => $blacklistInfo->getAdded()?->format('c'), + 'reason' => $reason, + ]); + } +} diff --git a/src/Subscription/Request/AddToBlacklistRequest.php b/src/Subscription/Request/AddToBlacklistRequest.php new file mode 100644 index 0000000..68dc81e --- /dev/null +++ b/src/Subscription/Request/AddToBlacklistRequest.php @@ -0,0 +1,23 @@ +blacklistManager->getBlacklistReason($object->getEmail()); + + return [ + 'email' => $object->getEmail(), + 'added' => $object->getAdded()?->format('Y-m-d\TH:i:sP'), + 'reason' => $reason, + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function supportsNormalization($data, string $format = null): bool + { + return $data instanceof UserBlacklist; + } +} diff --git a/tests/Integration/Subscription/Controller/BlacklistControllerTest.php b/tests/Integration/Subscription/Controller/BlacklistControllerTest.php new file mode 100644 index 0000000..fdba52a --- /dev/null +++ b/tests/Integration/Subscription/Controller/BlacklistControllerTest.php @@ -0,0 +1,60 @@ +get(BlacklistController::class) + ); + } + + public function testCheckEmailBlacklistedWithoutSessionKeyReturnsForbiddenStatus(): void + { + $this->jsonRequest('get', '/api/v2/blacklist/check/test@example.com'); + + $this->assertHttpForbidden(); + } + + public function testAddEmailToBlacklistWithoutSessionKeyReturnsForbiddenStatus(): void + { + $this->jsonRequest('post', '/api/v2/blacklist/add'); + + $this->assertHttpForbidden(); + } + + public function testAddEmailToBlacklistWithMissingEmailReturnsUnprocessableEntityStatus(): void + { + $jsonData = []; + + $this->authenticatedJsonRequest('post', '/api/v2/blacklist/add', [], [], [], json_encode($jsonData)); + + $this->assertHttpUnprocessableEntity(); + } + + public function testRemoveEmailFromBlacklistWithoutSessionKeyReturnsForbiddenStatus(): void + { + $this->jsonRequest('delete', '/api/v2/blacklist/remove/test@example.com'); + + $this->assertHttpForbidden(); + } + + public function testGetBlacklistInfoWithoutSessionKeyReturnsForbiddenStatus(): void + { + $this->jsonRequest('get', '/api/v2/blacklist/info/test@example.com'); + + $this->assertHttpForbidden(); + } +} diff --git a/tests/Unit/Subscription/Serializer/UserBlacklistNormalizerTest.php b/tests/Unit/Subscription/Serializer/UserBlacklistNormalizerTest.php new file mode 100644 index 0000000..85619b6 --- /dev/null +++ b/tests/Unit/Subscription/Serializer/UserBlacklistNormalizerTest.php @@ -0,0 +1,57 @@ +createMock(SubscriberBlacklistManager::class); + $normalizer = new UserBlacklistNormalizer($blacklistManager); + $userBlacklist = $this->createMock(UserBlacklist::class); + + $this->assertTrue($normalizer->supportsNormalization($userBlacklist)); + $this->assertFalse($normalizer->supportsNormalization(new stdClass())); + } + + public function testNormalize(): void + { + $email = 'test@example.com'; + $added = new DateTime('2025-08-08T12:00:00+00:00'); + $reason = 'Unsubscribed by user'; + + $userBlacklist = $this->createMock(UserBlacklist::class); + $userBlacklist->method('getEmail')->willReturn($email); + $userBlacklist->method('getAdded')->willReturn($added); + + $blacklistManager = $this->createMock(SubscriberBlacklistManager::class); + $blacklistManager->method('getBlacklistReason')->with($email)->willReturn($reason); + + $normalizer = new UserBlacklistNormalizer($blacklistManager); + + $expected = [ + 'email' => $email, + 'added' => '2025-08-08T12:00:00+00:00', + 'reason' => $reason, + ]; + + $this->assertSame($expected, $normalizer->normalize($userBlacklist)); + } + + public function testNormalizeWithInvalidObject(): void + { + $blacklistManager = $this->createMock(SubscriberBlacklistManager::class); + $normalizer = new UserBlacklistNormalizer($blacklistManager); + + $this->assertSame([], $normalizer->normalize(new stdClass())); + } +} From 3b839be49d5ce59845849cb85b6594c5ecb3f5cd Mon Sep 17 00:00:00 2001 From: TatevikGr Date: Mon, 18 Aug 2025 09:10:28 +0400 Subject: [PATCH 02/12] Subscribepage (#157) * SubscribePageController * SubscribePageNormalizer * SubscribePageCreateRequest * Get/set page data, update/delete page, add tests --------- Co-authored-by: Tatevik --- composer.json | 2 +- config/services/normalizers.yml | 4 + .../Controller/SubscribePageController.php | 433 ++++++++++++++++++ .../OpenApi/SwaggerSchemasResponse.php | 9 + .../Request/SubscribePageDataRequest.php | 21 + .../Request/SubscribePageRequest.php | 23 + .../Serializer/SubscribePageNormalizer.php | 42 ++ .../SubscribePageControllerTest.php | 291 ++++++++++++ .../Subscription/Fixtures/SubscribePage.csv | 3 + .../Fixtures/SubscribePageFixture.php | 62 +++ .../SubscribePageNormalizerTest.php | 71 +++ 11 files changed, 960 insertions(+), 1 deletion(-) create mode 100644 src/Subscription/Controller/SubscribePageController.php create mode 100644 src/Subscription/Request/SubscribePageDataRequest.php create mode 100644 src/Subscription/Request/SubscribePageRequest.php create mode 100644 src/Subscription/Serializer/SubscribePageNormalizer.php create mode 100644 tests/Integration/Subscription/Controller/SubscribePageControllerTest.php create mode 100644 tests/Integration/Subscription/Fixtures/SubscribePage.csv create mode 100644 tests/Integration/Subscription/Fixtures/SubscribePageFixture.php create mode 100644 tests/Unit/Subscription/Serializer/SubscribePageNormalizerTest.php diff --git a/composer.json b/composer.json index ba5526c..89dbf44 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ }, "require": { "php": "^8.1", - "phplist/core": "dev-dev", + "phplist/core": "dev-subscribepage", "friendsofsymfony/rest-bundle": "*", "symfony/test-pack": "^1.0", "symfony/process": "^6.4", diff --git a/config/services/normalizers.yml b/config/services/normalizers.yml index a270a3f..66ee7de 100644 --- a/config/services/normalizers.yml +++ b/config/services/normalizers.yml @@ -97,3 +97,7 @@ services: PhpList\RestBundle\Subscription\Serializer\UserBlacklistNormalizer: tags: [ 'serializer.normalizer' ] autowire: true + + PhpList\RestBundle\Subscription\Serializer\SubscribePageNormalizer: + tags: [ 'serializer.normalizer' ] + autowire: true diff --git a/src/Subscription/Controller/SubscribePageController.php b/src/Subscription/Controller/SubscribePageController.php new file mode 100644 index 0000000..8f3878c --- /dev/null +++ b/src/Subscription/Controller/SubscribePageController.php @@ -0,0 +1,433 @@ + '\\d+'], methods: ['GET'])] + #[OA\Get( + path: '/api/v2/subscribe-pages/{id}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Get subscribe page', + tags: ['subscriptions'], + 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: 'id', + description: 'Subscribe page ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/SubscribePage'), + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Not Found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ), + ] + )] + public function getPage( + Request $request, + #[MapEntity(mapping: ['id' => 'id'])] ?SubscribePage $page = null + ): JsonResponse { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to view subscribe pages.'); + } + + if (!$page) { + throw $this->createNotFoundException('Subscribe page not found'); + } + + return $this->json($this->normalizer->normalize($page), Response::HTTP_OK); + } + + #[Route('', name: 'create', methods: ['POST'])] + #[OA\Post( + path: '/api/v2/subscribe-pages', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Create subscribe page', + requestBody: new OA\RequestBody( + required: true, + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'title', type: 'string'), + new OA\Property(property: 'active', type: 'boolean', nullable: true), + ] + ) + ), + tags: ['subscriptions'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 201, + description: 'Created', + content: new OA\JsonContent(ref: '#/components/schemas/SubscribePage') + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 422, + description: 'Validation failed', + content: new OA\JsonContent(ref: '#/components/schemas/ValidationErrorResponse') + ) + ] + )] + public function createPage(Request $request): JsonResponse + { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to create subscribe pages.'); + } + + /** @var SubscribePageRequest $createRequest */ + $createRequest = $this->validator->validate($request, SubscribePageRequest::class); + + $page = $this->subscribePageManager->createPage($createRequest->title, $createRequest->active, $admin); + + return $this->json($this->normalizer->normalize($page), Response::HTTP_CREATED); + } + + #[Route('/{id}', name: 'update', requirements: ['id' => '\\d+'], methods: ['PUT'])] + #[OA\Put( + path: '/api/v2/subscribe-pages/{id}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Update subscribe page', + requestBody: new OA\RequestBody( + required: true, + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'title', type: 'string', nullable: true), + new OA\Property(property: 'active', type: 'boolean', nullable: true), + ] + ) + ), + tags: ['subscriptions'], + 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: 'id', + description: 'Subscribe page ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/SubscribePage') + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Not Found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ), + ] + )] + public function updatePage( + Request $request, + #[MapEntity(mapping: ['id' => 'id'])] ?SubscribePage $page = null + ): JsonResponse { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to update subscribe pages.'); + } + + if (!$page) { + throw $this->createNotFoundException('Subscribe page not found'); + } + + /** @var SubscribePageRequest $updateRequest */ + $updateRequest = $this->validator->validate($request, SubscribePageRequest::class); + + $updated = $this->subscribePageManager->updatePage( + page: $page, + title: $updateRequest->title, + active: $updateRequest->active, + owner: $admin, + ); + + return $this->json($this->normalizer->normalize($updated), Response::HTTP_OK); + } + + #[Route('/{id}', name: 'delete', requirements: ['id' => '\\d+'], methods: ['DELETE'])] + #[OA\Delete( + path: '/api/v2/subscribe-pages/{id}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Delete subscribe page', + tags: ['subscriptions'], + 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: 'id', + description: 'Subscribe page ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ) + ], + responses: [ + new OA\Response(response: 204, description: 'No Content'), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Not Found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function deletePage( + Request $request, + #[MapEntity(mapping: ['id' => 'id'])] ?SubscribePage $page = null + ): JsonResponse { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to delete subscribe pages.'); + } + + if ($page === null) { + throw $this->createNotFoundException('Subscribe page not found'); + } + + $this->subscribePageManager->deletePage($page); + + return $this->json(null, Response::HTTP_NO_CONTENT); + } + + #[Route('/{id}/data', name: 'get_data', requirements: ['id' => '\\d+'], methods: ['GET'])] + #[OA\Get( + path: '/api/v2/subscribe-pages/{id}/data', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Get subscribe page data', + tags: ['subscriptions'], + 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: 'id', + description: 'Subscribe page ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + type: 'array', + items: new OA\Items( + properties: [ + new OA\Property(property: 'id', type: 'integer'), + new OA\Property(property: 'name', type: 'string'), + new OA\Property(property: 'data', type: 'string', nullable: true), + ], + type: 'object' + ) + ) + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Not Found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function getPageData( + Request $request, + #[MapEntity(mapping: ['id' => 'id'])] ?SubscribePage $page = null + ): JsonResponse { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to view subscribe page data.'); + } + + if (!$page) { + throw $this->createNotFoundException('Subscribe page not found'); + } + + $data = $this->subscribePageManager->getPageData($page); + + $json = array_map(static function ($item) { + return [ + 'id' => $item->getId(), + 'name' => $item->getName(), + 'data' => $item->getData(), + ]; + }, $data); + + return $this->json($json, Response::HTTP_OK); + } + + #[Route('/{id}/data', name: 'set_data', requirements: ['id' => '\\d+'], methods: ['PUT'])] + #[OA\Put( + path: '/api/v2/subscribe-pages/{id}/data', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Set subscribe page data item', + requestBody: new OA\RequestBody( + required: true, + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'name', type: 'string'), + new OA\Property(property: 'value', type: 'string', nullable: true), + ] + ) + ), + tags: ['subscriptions'], + 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: 'id', + description: 'Subscribe page 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: 'id', type: 'integer'), + new OA\Property(property: 'name', type: 'string'), + new OA\Property(property: 'data', type: 'string', nullable: true), + ], + type: 'object' + ) + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Not Found', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function setPageData( + Request $request, + #[MapEntity(mapping: ['id' => 'id'])] ?SubscribePage $page = null + ): JsonResponse { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to update subscribe page data.'); + } + + if (!$page) { + throw $this->createNotFoundException('Subscribe page not found'); + } + + /** @var SubscribePageDataRequest $createRequest */ + $createRequest = $this->validator->validate($request, SubscribePageDataRequest::class); + + $item = $this->subscribePageManager->setPageData($page, $createRequest->name, $createRequest->value); + + return $this->json([ + 'id' => $item->getId(), + 'name' => $item->getName(), + 'data' => $item->getData(), + ], Response::HTTP_OK); + } +} diff --git a/src/Subscription/OpenApi/SwaggerSchemasResponse.php b/src/Subscription/OpenApi/SwaggerSchemasResponse.php index 8376495..ac7eedf 100644 --- a/src/Subscription/OpenApi/SwaggerSchemasResponse.php +++ b/src/Subscription/OpenApi/SwaggerSchemasResponse.php @@ -142,6 +142,15 @@ ), ], )] +#[OA\Schema( + schema: 'SubscribePage', + properties: [ + new OA\Property(property: 'id', type: 'integer', example: 1), + new OA\Property(property: 'title', type: 'string', example: 'Subscribe to our newsletter'), + new OA\Property(property: 'active', type: 'boolean', example: true), + new OA\Property(property: 'owner', ref: '#/components/schemas/Administrator'), + ], +)] class SwaggerSchemasResponse { } diff --git a/src/Subscription/Request/SubscribePageDataRequest.php b/src/Subscription/Request/SubscribePageDataRequest.php new file mode 100644 index 0000000..9af2ec0 --- /dev/null +++ b/src/Subscription/Request/SubscribePageDataRequest.php @@ -0,0 +1,21 @@ + $object->getId(), + 'title' => $object->getTitle(), + 'active' => $object->isActive(), + 'owner' => $this->adminNormalizer->normalize($object->getOwner()), + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function supportsNormalization($data, string $format = null): bool + { + return $data instanceof SubscribePage; + } +} diff --git a/tests/Integration/Subscription/Controller/SubscribePageControllerTest.php b/tests/Integration/Subscription/Controller/SubscribePageControllerTest.php new file mode 100644 index 0000000..d9da7b1 --- /dev/null +++ b/tests/Integration/Subscription/Controller/SubscribePageControllerTest.php @@ -0,0 +1,291 @@ +get(SubscribePageController::class) + ); + } + + public function testGetSubscribePageWithoutSessionReturnsForbidden(): void + { + $this->loadFixtures([AdministratorFixture::class, SubscribePageFixture::class]); + + self::getClient()->request('GET', '/api/v2/subscribe-pages/1'); + $this->assertHttpForbidden(); + } + + public function testGetSubscribePageWithSessionReturnsPage(): void + { + $this->loadFixtures([ + AdministratorFixture::class, + AdministratorTokenFixture::class, + SubscribePageFixture::class, + ]); + + $this->authenticatedJsonRequest('GET', '/api/v2/subscribe-pages/1'); + + $this->assertHttpOkay(); + $data = $this->getDecodedJsonResponseContent(); + + self::assertSame(1, $data['id']); + self::assertSame('Welcome Page', $data['title']); + self::assertTrue($data['active']); + self::assertIsArray($data['owner']); + self::assertSame(1, $data['owner']['id']); + self::assertArrayHasKey('login_name', $data['owner']); + self::assertArrayHasKey('email', $data['owner']); + self::assertArrayHasKey('privileges', $data['owner']); + } + + public function testGetSubscribePageWithSessionNotFound(): void + { + $this->loadFixtures([ + AdministratorFixture::class, + AdministratorTokenFixture::class, + SubscribePageFixture::class, + ]); + + $this->authenticatedJsonRequest('GET', '/api/v2/subscribe-pages/9999'); + + $this->assertHttpNotFound(); + } + + public function testCreateSubscribePageWithoutSessionReturnsForbidden(): void + { + // no auth fixtures loaded here + $payload = json_encode([ + 'title' => 'new-page@example.org', + 'active' => true, + ], JSON_THROW_ON_ERROR); + + $this->jsonRequest('POST', '/api/v2/subscribe-pages', content: $payload); + + $this->assertHttpForbidden(); + } + + public function testCreateSubscribePageWithSessionCreatesPage(): void + { + $payload = json_encode([ + 'title' => 'new-page@example.org', + 'active' => true, + ], JSON_THROW_ON_ERROR); + + $this->authenticatedJsonRequest('POST', '/api/v2/subscribe-pages', content: $payload); + + $this->assertHttpCreated(); + $data = $this->getDecodedJsonResponseContent(); + + self::assertArrayHasKey('id', $data); + self::assertIsInt($data['id']); + self::assertGreaterThanOrEqual(1, $data['id']); + self::assertSame('new-page@example.org', $data['title']); + self::assertTrue($data['active']); + self::assertIsArray($data['owner']); + self::assertArrayHasKey('id', $data['owner']); + self::assertArrayHasKey('login_name', $data['owner']); + self::assertArrayHasKey('email', $data['owner']); + self::assertArrayHasKey('privileges', $data['owner']); + } + + public function testUpdateSubscribePageWithoutSessionReturnsForbidden(): void + { + $this->loadFixtures([AdministratorFixture::class, SubscribePageFixture::class]); + $payload = json_encode([ + 'title' => 'updated-page@example.org', + 'active' => false, + ], JSON_THROW_ON_ERROR); + + $this->jsonRequest('PUT', '/api/v2/subscribe-pages/1', content: $payload); + $this->assertHttpForbidden(); + } + + public function testUpdateSubscribePageWithSessionReturnsOk(): void + { + $this->loadFixtures([ + AdministratorFixture::class, + AdministratorTokenFixture::class, + SubscribePageFixture::class, + ]); + $payload = json_encode([ + 'title' => 'updated-page@example.org', + 'active' => false, + ], JSON_THROW_ON_ERROR); + + $this->authenticatedJsonRequest('PUT', '/api/v2/subscribe-pages/1', content: $payload); + + $this->assertHttpOkay(); + $data = $this->getDecodedJsonResponseContent(); + self::assertSame(1, $data['id']); + self::assertSame('updated-page@example.org', $data['title']); + self::assertFalse($data['active']); + self::assertIsArray($data['owner']); + } + + public function testUpdateSubscribePageWithSessionNotFound(): void + { + $this->loadFixtures([ + AdministratorFixture::class, + AdministratorTokenFixture::class, + SubscribePageFixture::class, + ]); + $payload = json_encode([ + 'title' => 'updated-page@example.org', + 'active' => false, + ], JSON_THROW_ON_ERROR); + + $this->authenticatedJsonRequest('PUT', '/api/v2/subscribe-pages/9999', content: $payload); + $this->assertHttpNotFound(); + } + + public function testDeleteSubscribePageWithoutSessionReturnsForbidden(): void + { + $this->loadFixtures([AdministratorFixture::class, SubscribePageFixture::class]); + $this->jsonRequest('DELETE', '/api/v2/subscribe-pages/1'); + $this->assertHttpForbidden(); + } + + public function testDeleteSubscribePageWithSessionReturnsNoContentAndRemovesResource(): void + { + $this->loadFixtures([ + AdministratorFixture::class, + AdministratorTokenFixture::class, + SubscribePageFixture::class, + ]); + + $this->authenticatedJsonRequest('DELETE', '/api/v2/subscribe-pages/1'); + $this->assertHttpNoContent(); + + $this->authenticatedJsonRequest('GET', '/api/v2/subscribe-pages/1'); + $this->assertHttpNotFound(); + } + + public function testDeleteSubscribePageWithSessionNotFound(): void + { + $this->loadFixtures([ + AdministratorFixture::class, + AdministratorTokenFixture::class, + SubscribePageFixture::class, + ]); + + $this->authenticatedJsonRequest('DELETE', '/api/v2/subscribe-pages/9999'); + $this->assertHttpNotFound(); + } + + public function testGetSubscribePageDataWithoutSessionReturnsForbidden(): void + { + $this->loadFixtures([AdministratorFixture::class, SubscribePageFixture::class]); + $this->jsonRequest('GET', '/api/v2/subscribe-pages/1/data'); + $this->assertHttpForbidden(); + } + + public function testGetSubscribePageDataWithSessionReturnsArray(): void + { + $this->loadFixtures([ + AdministratorFixture::class, + AdministratorTokenFixture::class, + SubscribePageFixture::class, + ]); + + $this->authenticatedJsonRequest('GET', '/api/v2/subscribe-pages/1/data'); + $this->assertHttpOkay(); + $data = $this->getDecodedJsonResponseContent(); + self::assertIsArray($data); + + if (!empty($data)) { + self::assertArrayHasKey('id', $data[0]); + self::assertArrayHasKey('name', $data[0]); + self::assertArrayHasKey('data', $data[0]); + } + } + + public function testGetSubscribePageDataWithSessionNotFound(): void + { + $this->loadFixtures([ + AdministratorFixture::class, + AdministratorTokenFixture::class, + SubscribePageFixture::class, + ]); + + $this->authenticatedJsonRequest('GET', '/api/v2/subscribe-pages/9999/data'); + $this->assertHttpNotFound(); + } + + public function testSetSubscribePageDataWithoutSessionReturnsForbidden(): void + { + $this->loadFixtures([AdministratorFixture::class, SubscribePageFixture::class]); + $payload = json_encode([ + 'name' => 'intro_text', + 'value' => 'Hello world', + ], JSON_THROW_ON_ERROR); + + $this->jsonRequest('PUT', '/api/v2/subscribe-pages/1/data', content: $payload); + $this->assertHttpForbidden(); + } + + public function testSetSubscribePageDataWithMissingNameReturnsUnprocessableEntity(): void + { + $this->loadFixtures([ + AdministratorFixture::class, + AdministratorTokenFixture::class, + SubscribePageFixture::class, + ]); + $payload = json_encode([ + 'value' => 'Hello world', + ], JSON_THROW_ON_ERROR); + + $this->authenticatedJsonRequest('PUT', '/api/v2/subscribe-pages/1/data', content: $payload); + $this->assertHttpUnprocessableEntity(); + } + + public function testSetSubscribePageDataWithSessionReturnsOk(): void + { + $this->loadFixtures([ + AdministratorFixture::class, + AdministratorTokenFixture::class, + SubscribePageFixture::class, + ]); + $payload = json_encode([ + 'name' => 'intro_text', + 'value' => 'Hello world', + ], JSON_THROW_ON_ERROR); + + $this->authenticatedJsonRequest('PUT', '/api/v2/subscribe-pages/1/data', content: $payload); + $this->assertHttpOkay(); + $data = $this->getDecodedJsonResponseContent(); + self::assertArrayHasKey('id', $data); + self::assertArrayHasKey('name', $data); + self::assertArrayHasKey('data', $data); + self::assertSame('intro_text', $data['name']); + self::assertSame('Hello world', $data['data']); + } + + public function testSetSubscribePageDataWithSessionNotFound(): void + { + $this->loadFixtures([ + AdministratorFixture::class, + AdministratorTokenFixture::class, + SubscribePageFixture::class, + ]); + $payload = json_encode([ + 'name' => 'intro_text', + 'value' => 'Hello world', + ], JSON_THROW_ON_ERROR); + + $this->authenticatedJsonRequest('PUT', '/api/v2/subscribe-pages/9999/data', content: $payload); + $this->assertHttpNotFound(); + } +} diff --git a/tests/Integration/Subscription/Fixtures/SubscribePage.csv b/tests/Integration/Subscription/Fixtures/SubscribePage.csv new file mode 100644 index 0000000..6279b9c --- /dev/null +++ b/tests/Integration/Subscription/Fixtures/SubscribePage.csv @@ -0,0 +1,3 @@ +id,title,active,owner +1,"Welcome Page",1,1 +2,"Inactive Page",0,1 diff --git a/tests/Integration/Subscription/Fixtures/SubscribePageFixture.php b/tests/Integration/Subscription/Fixtures/SubscribePageFixture.php new file mode 100644 index 0000000..a22b246 --- /dev/null +++ b/tests/Integration/Subscription/Fixtures/SubscribePageFixture.php @@ -0,0 +1,62 @@ +getRepository(Administrator::class); + + do { + $data = fgetcsv($handle); + if ($data === false) { + break; + } + $row = array_combine($headers, $data); + + $owner = $adminRepository->find($row['owner']); + if ($owner === null) { + $owner = new Administrator(); + $this->setSubjectId($owner, (int)$row['owner']); + $owner->setSuperUser(true); + $owner->setDisabled(false); + $manager->persist($owner); + } + + $page = new SubscribePage(); + $this->setSubjectId($page, (int)$row['id']); + $page->setTitle($row['title']); + $page->setActive((bool)$row['active']); + $page->setOwner($owner); + + $manager->persist($page); + } while (true); + + fclose($handle); + } +} diff --git a/tests/Unit/Subscription/Serializer/SubscribePageNormalizerTest.php b/tests/Unit/Subscription/Serializer/SubscribePageNormalizerTest.php new file mode 100644 index 0000000..523e590 --- /dev/null +++ b/tests/Unit/Subscription/Serializer/SubscribePageNormalizerTest.php @@ -0,0 +1,71 @@ +createMock(AdministratorNormalizer::class); + $normalizer = new SubscribePageNormalizer($adminNormalizer); + + $page = $this->createMock(SubscribePage::class); + + $this->assertTrue($normalizer->supportsNormalization($page)); + $this->assertFalse($normalizer->supportsNormalization(new stdClass())); + } + + public function testNormalizeReturnsExpectedArray(): void + { + $owner = $this->createMock(Administrator::class); + + $page = $this->createMock(SubscribePage::class); + $page->method('getId')->willReturn(42); + $page->method('getTitle')->willReturn('welcome@example.org'); + $page->method('isActive')->willReturn(true); + $page->method('getOwner')->willReturn($owner); + + $adminData = [ + 'id' => 7, + 'login_name' => 'admin', + 'email' => 'admin@example.org', + 'privileges' => [ + 'subscribers' => true, + 'campaigns' => false, + 'statistics' => true, + 'settings' => false, + ], + ]; + + $adminNormalizer = $this->createMock(AdministratorNormalizer::class); + $adminNormalizer->method('normalize')->with($owner)->willReturn($adminData); + + $normalizer = new SubscribePageNormalizer($adminNormalizer); + + $expected = [ + 'id' => 42, + 'title' => 'welcome@example.org', + 'active' => true, + 'owner' => $adminData, + ]; + + $this->assertSame($expected, $normalizer->normalize($page)); + } + + public function testNormalizeWithInvalidObjectReturnsEmptyArray(): void + { + $adminNormalizer = $this->createMock(AdministratorNormalizer::class); + $normalizer = new SubscribePageNormalizer($adminNormalizer); + + $this->assertSame([], $normalizer->normalize(new stdClass())); + } +} From 5b7835dc541a581fe5d1f09ae713240fecd6b2dc Mon Sep 17 00:00:00 2001 From: TatevikGr Date: Tue, 19 Aug 2025 10:12:00 +0400 Subject: [PATCH 03/12] Bounceregex (#158) * Add BounceRegexController * Use code dev branch --------- Co-authored-by: Tatevik --- composer.json | 2 +- config/services/managers.yml | 10 +- config/services/normalizers.yml | 4 + .../Controller/BounceRegexController.php | 246 ++++++++++++++++++ .../Controller/TemplateController.php | 2 +- .../OpenApi/SwaggerSchemasResponse.php | 15 ++ .../Request/CreateBounceRegexRequest.php | 42 +++ .../Serializer/BounceRegexNormalizer.php | 41 +++ src/Messaging/Service/CampaignService.php | 2 +- .../Controller/BounceRegexControllerTest.php | 81 ++++++ .../Request/CreateBounceRegexRequestTest.php | 46 ++++ .../Serializer/BounceRegexNormalizerTest.php | 60 +++++ .../Messaging/Service/CampaignServiceTest.php | 2 +- 13 files changed, 546 insertions(+), 7 deletions(-) create mode 100644 src/Messaging/Controller/BounceRegexController.php create mode 100644 src/Messaging/Request/CreateBounceRegexRequest.php create mode 100644 src/Messaging/Serializer/BounceRegexNormalizer.php create mode 100644 tests/Integration/Messaging/Controller/BounceRegexControllerTest.php create mode 100644 tests/Unit/Messaging/Request/CreateBounceRegexRequestTest.php create mode 100644 tests/Unit/Messaging/Serializer/BounceRegexNormalizerTest.php diff --git a/composer.json b/composer.json index 89dbf44..ba5526c 100644 --- a/composer.json +++ b/composer.json @@ -36,7 +36,7 @@ }, "require": { "php": "^8.1", - "phplist/core": "dev-subscribepage", + "phplist/core": "dev-dev", "friendsofsymfony/rest-bundle": "*", "symfony/test-pack": "^1.0", "symfony/process": "^6.4", diff --git a/config/services/managers.yml b/config/services/managers.yml index 37f0b02..9925399 100644 --- a/config/services/managers.yml +++ b/config/services/managers.yml @@ -20,15 +20,19 @@ services: autowire: true autoconfigure: true - PhpList\Core\Domain\Messaging\Service\MessageManager: + PhpList\Core\Domain\Messaging\Service\Manager\MessageManager: autowire: true autoconfigure: true - PhpList\Core\Domain\Messaging\Service\TemplateManager: + PhpList\Core\Domain\Messaging\Service\Manager\TemplateManager: autowire: true autoconfigure: true - PhpList\Core\Domain\Messaging\Service\TemplateImageManager: + PhpList\Core\Domain\Messaging\Service\Manager\TemplateImageManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Messaging\Service\Manager\BounceRegexManager: autowire: true autoconfigure: true diff --git a/config/services/normalizers.yml b/config/services/normalizers.yml index 66ee7de..2179f6a 100644 --- a/config/services/normalizers.yml +++ b/config/services/normalizers.yml @@ -101,3 +101,7 @@ services: PhpList\RestBundle\Subscription\Serializer\SubscribePageNormalizer: tags: [ 'serializer.normalizer' ] autowire: true + + PhpList\RestBundle\Messaging\Serializer\BounceRegexNormalizer: + tags: [ 'serializer.normalizer' ] + autowire: true diff --git a/src/Messaging/Controller/BounceRegexController.php b/src/Messaging/Controller/BounceRegexController.php new file mode 100644 index 0000000..7f3f275 --- /dev/null +++ b/src/Messaging/Controller/BounceRegexController.php @@ -0,0 +1,246 @@ +requireAuthentication($request); + $items = $this->manager->getAll(); + $normalized = array_map(fn($bounceRegex) => $this->normalizer->normalize($bounceRegex), $items); + + return $this->json($normalized, Response::HTTP_OK); + } + + #[Route('/{regexHash}', name: 'get_one', methods: ['GET'])] + #[OA\Get( + path: '/api/v2/bounces/regex/{regexHash}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a bounce regex by its hash.', + summary: 'Get a bounce regex by its hash', + tags: ['bounces'], + 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: 'regexHash', + description: 'Regex hash', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/BounceRegex') + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function getOne(Request $request, string $regexHash): JsonResponse + { + $this->requireAuthentication($request); + $entity = $this->manager->getByHash($regexHash); + if (!$entity) { + throw $this->createNotFoundException('Bounce regex not found.'); + } + + return $this->json($this->normalizer->normalize($entity), Response::HTTP_OK); + } + + #[Route('', name: 'create_or_update', methods: ['POST'])] + #[OA\Post( + path: '/api/v2/bounces/regex', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Creates a new bounce regex or updates an existing one (matched by regex hash).', + summary: 'Create or update a bounce regex', + requestBody: new OA\RequestBody( + description: 'Create or update a bounce regex rule.', + required: true, + content: new OA\JsonContent( + required: ['regex'], + properties: [ + new OA\Property(property: 'regex', type: 'string', example: '/mailbox is full/i'), + new OA\Property(property: 'action', type: 'string', example: 'delete', nullable: true), + new OA\Property(property: 'list_order', type: 'integer', example: 0, nullable: true), + new OA\Property(property: 'admin', type: 'integer', example: 1, nullable: true), + new OA\Property(property: 'comment', type: 'string', example: 'Auto-generated', nullable: true), + new OA\Property(property: 'status', type: 'string', example: 'active', nullable: true), + ], + type: 'object' + ) + ), + tags: ['bounces'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + ], + responses: [ + new OA\Response( + response: 201, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/BounceRegex') + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 422, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/ValidationErrorResponse') + ), + ] + )] + public function createOrUpdate(Request $request): JsonResponse + { + $this->requireAuthentication($request); + /** @var CreateBounceRegexRequest $dto */ + $dto = $this->validator->validate($request, CreateBounceRegexRequest::class); + + $entity = $this->manager->createOrUpdateFromPattern( + regex: $dto->regex, + action: $dto->action, + listOrder: $dto->listOrder, + adminId: $dto->admin, + comment: $dto->comment, + status: $dto->status + ); + + return $this->json($this->normalizer->normalize($entity), Response::HTTP_CREATED); + } + + #[Route('/{regexHash}', name: 'delete', methods: ['DELETE'])] + #[OA\Delete( + path: '/api/v2/bounces/regex/{regexHash}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Delete a bounce regex by its hash.', + summary: 'Delete a bounce regex by its hash', + tags: ['bounces'], + 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: 'regexHash', + description: 'Regex hash', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ), + ], + responses: [ + new OA\Response( + response: Response::HTTP_NO_CONTENT, + description: 'Success' + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 404, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function delete(Request $request, string $regexHash): JsonResponse + { + $this->requireAuthentication($request); + $entity = $this->manager->getByHash($regexHash); + if (!$entity) { + throw $this->createNotFoundException('Bounce regex not found.'); + } + $this->manager->delete($entity); + + return $this->json(null, Response::HTTP_NO_CONTENT); + } +} diff --git a/src/Messaging/Controller/TemplateController.php b/src/Messaging/Controller/TemplateController.php index 6513db2..b814c89 100644 --- a/src/Messaging/Controller/TemplateController.php +++ b/src/Messaging/Controller/TemplateController.php @@ -6,7 +6,7 @@ use OpenApi\Attributes as OA; use PhpList\Core\Domain\Messaging\Model\Template; -use PhpList\Core\Domain\Messaging\Service\TemplateManager; +use PhpList\Core\Domain\Messaging\Service\Manager\TemplateManager; use PhpList\Core\Security\Authentication; use PhpList\RestBundle\Common\Controller\BaseController; use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider; diff --git a/src/Messaging/OpenApi/SwaggerSchemasResponse.php b/src/Messaging/OpenApi/SwaggerSchemasResponse.php index a14e9b5..9e4cfb5 100644 --- a/src/Messaging/OpenApi/SwaggerSchemasResponse.php +++ b/src/Messaging/OpenApi/SwaggerSchemasResponse.php @@ -120,6 +120,21 @@ ], type: 'object' )] +#[OA\Schema( + schema: 'BounceRegex', + properties: [ + new OA\Property(property: 'id', type: 'integer', example: 10), + new OA\Property(property: 'regex', type: 'string', example: '/mailbox is full/i'), + new OA\Property(property: 'regex_hash', type: 'string', example: 'd41d8cd98f00b204e9800998ecf8427e'), + new OA\Property(property: 'action', type: 'string', example: 'delete', nullable: true), + new OA\Property(property: 'list_order', type: 'integer', example: 0, nullable: true), + new OA\Property(property: 'admin_id', type: 'integer', example: 1, nullable: true), + new OA\Property(property: 'comment', type: 'string', example: 'Auto-generated rule', nullable: true), + new OA\Property(property: 'status', type: 'string', example: 'active', nullable: true), + new OA\Property(property: 'count', type: 'integer', example: 5, nullable: true), + ], + type: 'object' +)] class SwaggerSchemasResponse { } diff --git a/src/Messaging/Request/CreateBounceRegexRequest.php b/src/Messaging/Request/CreateBounceRegexRequest.php new file mode 100644 index 0000000..6977119 --- /dev/null +++ b/src/Messaging/Request/CreateBounceRegexRequest.php @@ -0,0 +1,42 @@ + $this->regex, + 'action' => $this->action, + 'listOrder' => $this->listOrder, + 'admin' => $this->admin, + 'comment' => $this->comment, + 'status' => $this->status, + ]; + } +} diff --git a/src/Messaging/Serializer/BounceRegexNormalizer.php b/src/Messaging/Serializer/BounceRegexNormalizer.php new file mode 100644 index 0000000..5771bd8 --- /dev/null +++ b/src/Messaging/Serializer/BounceRegexNormalizer.php @@ -0,0 +1,41 @@ + $object->getId(), + 'regex' => $object->getRegex(), + 'regex_hash' => $object->getRegexHash(), + 'action' => $object->getAction(), + 'list_order' => $object->getListOrder(), + 'admin_id' => $object->getAdminId(), + 'comment' => $object->getComment(), + 'status' => $object->getStatus(), + 'count' => $object->getCount(), + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function supportsNormalization($data, string $format = null): bool + { + return $data instanceof BounceRegex; + } +} diff --git a/src/Messaging/Service/CampaignService.php b/src/Messaging/Service/CampaignService.php index b6680d1..d2cd8c0 100644 --- a/src/Messaging/Service/CampaignService.php +++ b/src/Messaging/Service/CampaignService.php @@ -8,7 +8,7 @@ use PhpList\Core\Domain\Identity\Model\PrivilegeFlag; use PhpList\Core\Domain\Messaging\Model\Filter\MessageFilter; use PhpList\Core\Domain\Messaging\Model\Message; -use PhpList\Core\Domain\Messaging\Service\MessageManager; +use PhpList\Core\Domain\Messaging\Service\Manager\MessageManager; use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider; use PhpList\RestBundle\Messaging\Request\CreateMessageRequest; use PhpList\RestBundle\Messaging\Request\UpdateMessageRequest; diff --git a/tests/Integration/Messaging/Controller/BounceRegexControllerTest.php b/tests/Integration/Messaging/Controller/BounceRegexControllerTest.php new file mode 100644 index 0000000..4c7872e --- /dev/null +++ b/tests/Integration/Messaging/Controller/BounceRegexControllerTest.php @@ -0,0 +1,81 @@ +get(BounceRegexController::class)); + } + + public function testListWithoutSessionKeyReturnsForbidden(): void + { + self::getClient()->request('GET', '/api/v2/bounces/regex'); + $this->assertHttpForbidden(); + } + + public function testListWithExpiredSessionKeyReturnsForbidden(): void + { + $this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class]); + + self::getClient()->request( + 'GET', + '/api/v2/bounces/regex', + [], + [], + ['PHP_AUTH_USER' => 'unused', 'PHP_AUTH_PW' => 'expiredtoken'] + ); + + $this->assertHttpForbidden(); + } + + public function testListWithValidSessionKeyReturnsOkay(): void + { + $this->authenticatedJsonRequest('GET', '/api/v2/bounces/regex'); + $this->assertHttpOkay(); + } + + public function testCreateGetDeleteFlow(): void + { + $payload = json_encode([ + 'regex' => '/mailbox is full/i', + 'action' => 'delete', + 'list_order' => 0, + 'admin' => 1, + 'comment' => 'Auto-generated rule', + 'status' => 'active', + ]); + + $this->authenticatedJsonRequest('POST', '/api/v2/bounces/regex', [], [], [], $payload); + $this->assertHttpCreated(); + $created = $this->getDecodedJsonResponseContent(); + $this->assertSame('/mailbox is full/i', $created['regex']); + $this->assertSame(md5('/mailbox is full/i'), $created['regex_hash']); + + $hash = $created['regex_hash']; + $this->authenticatedJsonRequest('GET', '/api/v2/bounces/regex/' . $hash); + $this->assertHttpOkay(); + $one = $this->getDecodedJsonResponseContent(); + $this->assertSame($hash, $one['regex_hash']); + + $this->authenticatedJsonRequest('GET', '/api/v2/bounces/regex'); + $this->assertHttpOkay(); + $list = $this->getDecodedJsonResponseContent(); + $this->assertIsArray($list); + $this->assertIsArray($list[0] ?? []); + + $this->authenticatedJsonRequest('DELETE', '/api/v2/bounces/regex/' . $hash); + $this->assertHttpNoContent(); + + $this->authenticatedJsonRequest('GET', '/api/v2/bounces/regex/' . $hash); + $this->assertHttpNotFound(); + } +} diff --git a/tests/Unit/Messaging/Request/CreateBounceRegexRequestTest.php b/tests/Unit/Messaging/Request/CreateBounceRegexRequestTest.php new file mode 100644 index 0000000..8767477 --- /dev/null +++ b/tests/Unit/Messaging/Request/CreateBounceRegexRequestTest.php @@ -0,0 +1,46 @@ +regex = '/mailbox is full/i'; + $req->action = 'delete'; + $req->listOrder = 3; + $req->admin = 9; + $req->comment = 'Auto'; + $req->status = 'active'; + + $dto = $req->getDto(); + + $this->assertSame('/mailbox is full/i', $dto['regex']); + $this->assertSame('delete', $dto['action']); + $this->assertSame(3, $dto['listOrder']); + $this->assertSame(9, $dto['admin']); + $this->assertSame('Auto', $dto['comment']); + $this->assertSame('active', $dto['status']); + } + + public function testGetDtoWithDefaults(): void + { + $req = new CreateBounceRegexRequest(); + $req->regex = '/some/i'; + + $dto = $req->getDto(); + + $this->assertSame('/some/i', $dto['regex']); + $this->assertNull($dto['action']); + $this->assertSame(0, $dto['listOrder']); + $this->assertNull($dto['admin']); + $this->assertNull($dto['comment']); + $this->assertNull($dto['status']); + } +} diff --git a/tests/Unit/Messaging/Serializer/BounceRegexNormalizerTest.php b/tests/Unit/Messaging/Serializer/BounceRegexNormalizerTest.php new file mode 100644 index 0000000..a86b065 --- /dev/null +++ b/tests/Unit/Messaging/Serializer/BounceRegexNormalizerTest.php @@ -0,0 +1,60 @@ +normalizer = new BounceRegexNormalizer(); + } + + public function testSupportsNormalization(): void + { + $regex = new BounceRegex(); + $this->assertTrue($this->normalizer->supportsNormalization($regex)); + $this->assertFalse($this->normalizer->supportsNormalization(new \stdClass())); + } + + public function testNormalizeReturnsExpectedArray(): void + { + $regexPattern = '/mailbox is full/i'; + $hash = md5($regexPattern); + + $entity = new BounceRegex( + regex: $regexPattern, + regexHash: $hash, + action: 'delete', + listOrder: 2, + adminId: 42, + comment: 'Auto-generated rule', + status: 'active', + count: 7 + ); + + $result = $this->normalizer->normalize($entity); + + $this->assertSame($regexPattern, $result['regex']); + $this->assertSame($hash, $result['regex_hash']); + $this->assertSame('delete', $result['action']); + $this->assertSame(2, $result['list_order']); + $this->assertSame(42, $result['admin_id']); + $this->assertSame('Auto-generated rule', $result['comment']); + $this->assertSame('active', $result['status']); + $this->assertSame(7, $result['count']); + $this->assertArrayHasKey('id', $result); + } + + public function testNormalizeWithInvalidObjectReturnsEmptyArray(): void + { + $this->assertSame([], $this->normalizer->normalize(new \stdClass())); + } +} diff --git a/tests/Unit/Messaging/Service/CampaignServiceTest.php b/tests/Unit/Messaging/Service/CampaignServiceTest.php index 5293f0f..0a3d2e4 100644 --- a/tests/Unit/Messaging/Service/CampaignServiceTest.php +++ b/tests/Unit/Messaging/Service/CampaignServiceTest.php @@ -11,7 +11,7 @@ use PhpList\Core\Domain\Messaging\Model\Message; use PhpList\Core\Domain\Messaging\Model\Dto\CreateMessageDto; use PhpList\Core\Domain\Messaging\Model\Dto\UpdateMessageDto; -use PhpList\Core\Domain\Messaging\Service\MessageManager; +use PhpList\Core\Domain\Messaging\Service\Manager\MessageManager; use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider; use PhpList\RestBundle\Messaging\Request\CreateMessageRequest; use PhpList\RestBundle\Messaging\Request\UpdateMessageRequest; From 3ed98fbdb38e157551d84054dda4591d7cb37ae7 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Tue, 23 Sep 2025 11:44:22 +0400 Subject: [PATCH 04/12] Fix message status (enum) --- .../Serializer/MessageNormalizer.php | 2 +- .../Messaging/Fixtures/MessageFixture.php | 52 +++++++++---------- .../Serializer/MessageNormalizerTest.php | 4 +- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Messaging/Serializer/MessageNormalizer.php b/src/Messaging/Serializer/MessageNormalizer.php index b659b3d..dcad635 100644 --- a/src/Messaging/Serializer/MessageNormalizer.php +++ b/src/Messaging/Serializer/MessageNormalizer.php @@ -39,7 +39,7 @@ public function normalize($object, string $format = null, array $context = []): 'format_options' => $object->getFormat()->getFormatOptions() ], 'message_metadata' => [ - 'status' => $object->getMetadata()->getStatus(), + 'status' => $object->getMetadata()->getStatus()->value, 'processed' => $object->getMetadata()->isProcessed(), 'views' => $object->getMetadata()->getViews(), 'bounce_count' => $object->getMetadata()->getBounceCount(), diff --git a/tests/Integration/Messaging/Fixtures/MessageFixture.php b/tests/Integration/Messaging/Fixtures/MessageFixture.php index 3e6b4a9..0f5891b 100644 --- a/tests/Integration/Messaging/Fixtures/MessageFixture.php +++ b/tests/Integration/Messaging/Fixtures/MessageFixture.php @@ -59,43 +59,43 @@ public function load(ObjectManager $manager): void ); $schedule = new MessageSchedule( - (int)$row['repeatinterval'], - new DateTime($row['repeatuntil']), - (int)$row['requeueinterval'], - new DateTime($row['requeueuntil']), - new DateTime($row['embargo']), + repeatInterval: (int)$row['repeatinterval'], + repeatUntil: new DateTime($row['repeatuntil']), + requeueInterval: (int)$row['requeueinterval'], + requeueUntil: new DateTime($row['requeueuntil']), + embargo: new DateTime($row['embargo']), ); $metadata = new MessageMetadata( - $row['status'], - (int)$row['bouncecount'], - new DateTime($row['entered']), - new DateTime($row['sent']), - new DateTime($row['sendstart']), + status: Message\MessageStatus::from($row['status']), + bounceCount: (int)$row['bouncecount'], + entered: new DateTime($row['entered']), + sent: new DateTime($row['sent']), + sendStart: new DateTime($row['sendstart']), ); $metadata->setProcessed((bool) $row['processed']); $metadata->setViews((int)$row['viewed']); $content = new MessageContent( - $row['subject'], - $row['message'], - $row['textmessage'], - $row['footer'] + subject: $row['subject'], + text: $row['message'], + textMessage: $row['textmessage'], + footer: $row['footer'] ); $options = new MessageOptions( - $row['fromfield'], - $row['tofield'], - $row['replyto'], - $row['userselection'], - $row['rsstemplate'], + fromField: $row['fromfield'], + toField: $row['tofield'], + replyTo: $row['replyto'], + userSelection: $row['userselection'], + rssTemplate: $row['rsstemplate'], ); $message = new Message( - $format, - $schedule, - $metadata, - $content, - $options, - $admin, - $template, + format: $format, + schedule: $schedule, + metadata: $metadata, + content: $content, + options: $options, + owner: $admin, + template: $template, ); $this->setSubjectId($message, (int)$row['id']); $this->setSubjectProperty($message, 'uuid', $row['uuid']); diff --git a/tests/Unit/Messaging/Serializer/MessageNormalizerTest.php b/tests/Unit/Messaging/Serializer/MessageNormalizerTest.php index df5a33d..36f41d2 100644 --- a/tests/Unit/Messaging/Serializer/MessageNormalizerTest.php +++ b/tests/Unit/Messaging/Serializer/MessageNormalizerTest.php @@ -46,7 +46,7 @@ public function testNormalizeReturnsExpectedArray(): void $entered = new DateTime('2025-01-01T10:00:00+00:00'); $sent = new DateTime('2025-01-02T10:00:00+00:00'); - $metadata = new Message\MessageMetadata('draft'); + $metadata = new Message\MessageMetadata( Message\MessageStatus::Draft); $metadata->setProcessed(true); $metadata->setViews(10); $metadata->setBounceCount(3); @@ -80,7 +80,7 @@ public function testNormalizeReturnsExpectedArray(): void $this->assertSame('Test Template', $result['template']['title']); $this->assertSame('Subject', $result['message_content']['subject']); $this->assertSame(['text', 'html'], $result['message_format']['format_options']); - $this->assertSame('draft', $result['message_metadata']['status']); + $this->assertSame( Message\MessageStatus::Draft->value, $result['message_metadata']['status']); $this->assertSame('from@example.com', $result['message_options']['from_field']); } From 0f452033aea975d8fb02785730994ff459164160 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Tue, 23 Sep 2025 11:44:46 +0400 Subject: [PATCH 05/12] Fix function name --- src/Subscription/Service/SubscriberService.php | 2 +- tests/Unit/Subscription/Service/SubscriberServiceTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Subscription/Service/SubscriberService.php b/src/Subscription/Service/SubscriberService.php index 4a8fa97..893b892 100644 --- a/src/Subscription/Service/SubscriberService.php +++ b/src/Subscription/Service/SubscriberService.php @@ -35,7 +35,7 @@ public function updateSubscriber(UpdateSubscriberRequest $updateSubscriberReques public function getSubscriber(int $subscriberId): array { - $subscriber = $this->subscriberManager->getSubscriber($subscriberId); + $subscriber = $this->subscriberManager->getSubscriberById($subscriberId); return $this->subscriberNormalizer->normalize($subscriber); } diff --git a/tests/Unit/Subscription/Service/SubscriberServiceTest.php b/tests/Unit/Subscription/Service/SubscriberServiceTest.php index eef8d1a..ffc27dc 100644 --- a/tests/Unit/Subscription/Service/SubscriberServiceTest.php +++ b/tests/Unit/Subscription/Service/SubscriberServiceTest.php @@ -97,7 +97,7 @@ public function testGetSubscriberReturnsNormalizedSubscriber(): void $expectedResult = ['id' => 1, 'email' => 'test@example.com']; $this->subscriberManager->expects($this->once()) - ->method('getSubscriber') + ->method('getSubscriberById') ->with($subscriberId) ->willReturn($subscriber); From abcdb9cd57396c44bc7c8adc067a6a907c54b627 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Tue, 23 Sep 2025 12:18:32 +0400 Subject: [PATCH 06/12] Fix send campaign (status transition) --- .../Controller/CampaignController.php | 2 +- .../Controller/CampaignControllerTest.php | 4 ++-- .../Messaging/Fixtures/Message.csv | 20 +++++++++++++++++++ 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Messaging/Controller/CampaignController.php b/src/Messaging/Controller/CampaignController.php index 6ebac37..d897706 100644 --- a/src/Messaging/Controller/CampaignController.php +++ b/src/Messaging/Controller/CampaignController.php @@ -6,7 +6,7 @@ use OpenApi\Attributes as OA; use PhpList\Core\Domain\Messaging\Model\Message; -use PhpList\Core\Domain\Messaging\Service\CampaignProcessor; +use PhpList\Core\Domain\Messaging\Service\Processor\CampaignProcessor; use PhpList\Core\Security\Authentication; use PhpList\RestBundle\Common\Controller\BaseController; use PhpList\RestBundle\Common\Validator\RequestValidator; diff --git a/tests/Integration/Messaging/Controller/CampaignControllerTest.php b/tests/Integration/Messaging/Controller/CampaignControllerTest.php index a47fe02..fcbf46f 100644 --- a/tests/Integration/Messaging/Controller/CampaignControllerTest.php +++ b/tests/Integration/Messaging/Controller/CampaignControllerTest.php @@ -98,11 +98,11 @@ public function testSendMessageWithValidSessionReturnsOkay(): void { $this->loadFixtures([AdministratorFixture::class, MessageFixture::class]); - $this->authenticatedJsonRequest('POST', '/api/v2/campaigns/1/send'); + $this->authenticatedJsonRequest('POST', '/api/v2/campaigns/2/send'); $this->assertHttpOkay(); $response = $this->getDecodedJsonResponseContent(); - self::assertSame(1, $response['id']); + self::assertSame(2, $response['id']); } public function testSendMessageWithInvalidIdReturnsNotFound(): void diff --git a/tests/Integration/Messaging/Fixtures/Message.csv b/tests/Integration/Messaging/Fixtures/Message.csv index c2e52a7..6e83106 100644 --- a/tests/Integration/Messaging/Fixtures/Message.csv +++ b/tests/Integration/Messaging/Fixtures/Message.csv @@ -19,3 +19,23 @@ id,uuid,subject,fromfield,tofield,replyto,message,textmessage,footer,entered,mod ",2024-11-10 16:57:46,2024-11-14 08:32:15,2024-11-14 08:32:00,0,2024-11-14 08:32:00,0,2024-11-14 08:32:00,sent,,2024-11-14 08:32:15,1,invite,0,0,0,0,0,0,0,0,0,2024-11-14 08:32:15,,1 +2,2df6b147-8470-45ed-8e4e-86aa01af400f,Do you want to continue receiving our messages?, My Name ,"","","

Hi [FIRST NAME%%there], remember us? You first signed up for our email newsletter on [ENTERED] – please click here to confirm you're happy to continue receiving our messages:

+ +

Continue receiving messages  (If you do not confirm using this link, then you won't hear from us again)

+ +

While you're at it, you can also update your preferences, including your email address or other details, by clicking here:

+ +

Update preferences

+ +

By confirming your membership and keeping your details up to date, you're helping us to manage and protect your data in accordance with best practices.

+ +

Thank you!

","","-- + +
+

This message was sent to [EMAIL] by [FROMEMAIL].

+

To forward this message, please do not use the forward button of your email application, because this message was made specifically for you only. Instead use the forward page in our newsletter system.
+ To change your details and to choose which lists to be subscribed to, visit your personal preferences page.
+ Or you can opt-out completely from all future mailings.

+
+ + ",2024-11-10 16:57:46,2024-11-14 08:32:15,2024-11-14 08:32:00,0,2024-11-14 08:32:00,0,2024-11-14 08:32:00,submitted,,2024-11-14 08:32:15,1,invite,0,0,0,0,0,0,0,0,0,2024-11-14 08:32:15,,1 From ffa7078f98d22f969d7b2b074e70e68103b075a8 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Tue, 23 Sep 2025 12:28:27 +0400 Subject: [PATCH 07/12] Fix MessageMetadataDto --- src/Messaging/Request/Message/MessageMetadataRequest.php | 6 +++++- tests/Unit/Messaging/Serializer/MessageNormalizerTest.php | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Messaging/Request/Message/MessageMetadataRequest.php b/src/Messaging/Request/Message/MessageMetadataRequest.php index ca908e6..03fd332 100644 --- a/src/Messaging/Request/Message/MessageMetadataRequest.php +++ b/src/Messaging/Request/Message/MessageMetadataRequest.php @@ -5,6 +5,7 @@ namespace PhpList\RestBundle\Messaging\Request\Message; use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageMetadataDto; +use PhpList\Core\Domain\Messaging\Model\Message\MessageStatus; use Symfony\Component\Validator\Constraints as Assert; class MessageMetadataRequest implements RequestDtoInterface @@ -12,10 +13,13 @@ class MessageMetadataRequest implements RequestDtoInterface #[Assert\NotBlank] public string $status; + /** + * @SuppressWarnings(PHPMD.StaticAccess) + */ public function getDto(): MessageMetadataDto { return new MessageMetadataDto( - status: $this->status, + status: MessageStatus::from($this->status), ); } } diff --git a/tests/Unit/Messaging/Serializer/MessageNormalizerTest.php b/tests/Unit/Messaging/Serializer/MessageNormalizerTest.php index 36f41d2..d90ef90 100644 --- a/tests/Unit/Messaging/Serializer/MessageNormalizerTest.php +++ b/tests/Unit/Messaging/Serializer/MessageNormalizerTest.php @@ -46,7 +46,7 @@ public function testNormalizeReturnsExpectedArray(): void $entered = new DateTime('2025-01-01T10:00:00+00:00'); $sent = new DateTime('2025-01-02T10:00:00+00:00'); - $metadata = new Message\MessageMetadata( Message\MessageStatus::Draft); + $metadata = new Message\MessageMetadata(Message\MessageStatus::Draft); $metadata->setProcessed(true); $metadata->setViews(10); $metadata->setBounceCount(3); @@ -80,7 +80,7 @@ public function testNormalizeReturnsExpectedArray(): void $this->assertSame('Test Template', $result['template']['title']); $this->assertSame('Subject', $result['message_content']['subject']); $this->assertSame(['text', 'html'], $result['message_format']['format_options']); - $this->assertSame( Message\MessageStatus::Draft->value, $result['message_metadata']['status']); + $this->assertSame(Message\MessageStatus::Draft->value, $result['message_metadata']['status']); $this->assertSame('from@example.com', $result['message_options']['from_field']); } From 274849110e7a17328fdcf4340429d9dd288b04f1 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Wed, 24 Sep 2025 12:17:44 +0400 Subject: [PATCH 08/12] Add: Reset subscriber bounce count --- .../Controller/SubscriberController.php | 68 +++++++++++++++++++ .../Service/SubscriberService.php | 11 +++ 2 files changed, 79 insertions(+) diff --git a/src/Subscription/Controller/SubscriberController.php b/src/Subscription/Controller/SubscriberController.php index fc7be66..f053dff 100644 --- a/src/Subscription/Controller/SubscriberController.php +++ b/src/Subscription/Controller/SubscriberController.php @@ -375,6 +375,74 @@ public function deleteSubscriber( return $this->json(null, Response::HTTP_NO_CONTENT); } + #[Route( + '/{subscriberId}/reset-bounce-count', + name: 'reset_bounce_count', + requirements: ['subscriberId' => '\d+'], + methods: ['POST'] + )] + #[OA\Post( + path: '/api/v2/subscribers/{subscriberId}/reset-bounce-count', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production.', + summary: 'Reset bounce count for a subscriber.', + tags: ['subscribers'], + 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: 'subscriberId', + description: 'Subscriber ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/Subscriber'), + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ), + new OA\Response( + response: 422, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/ValidationErrorResponse') + ), + new OA\Response( + response: 404, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/NotFoundErrorResponse') + ) + ] + )] + public function resetBounceCount( + Request $request, + #[MapEntity(mapping: ['subscriberId' => 'id'])] ?Subscriber $subscriber = null, + ): Response { + $admin = $this->requireAuthentication($request); + if (!$admin->getPrivileges()->has(PrivilegeFlag::Subscribers)) { + throw $this->createAccessDeniedException('You are not allowed to manage Subscribers.'); + } + + if (!$subscriber) { + throw $this->createNotFoundException('Subscriber not found.'); + } + + $subscriberData = $this->subscriberService->resetSubscriberBounceCount($subscriber); + + return $this->json($subscriberData, Response::HTTP_OK); + } + #[Route('/confirm', name: 'confirm', methods: ['GET'])] #[OA\Get( path: '/api/v2/subscribers/confirm', diff --git a/src/Subscription/Service/SubscriberService.php b/src/Subscription/Service/SubscriberService.php index 893b892..2ddce62 100644 --- a/src/Subscription/Service/SubscriberService.php +++ b/src/Subscription/Service/SubscriberService.php @@ -12,6 +12,11 @@ use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; +/** + * This encapsulates subscriber-related services + * + * @author Tatevik Grigoryan + */ class SubscriberService { public function __construct( @@ -33,6 +38,12 @@ public function updateSubscriber(UpdateSubscriberRequest $updateSubscriberReques return $this->subscriberNormalizer->normalize($subscriber, 'json'); } + public function resetSubscriberBounceCount(Subscriber $subscriber): array + { + $subscriber = $this->subscriberManager->resetBounceCount($subscriber); + return $this->subscriberNormalizer->normalize($subscriber, 'json'); + } + public function getSubscriber(int $subscriberId): array { $subscriber = $this->subscriberManager->getSubscriberById($subscriberId); From c68921c0752a2d00f87e3eade33e61fc18d4eeb6 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Wed, 24 Sep 2025 12:29:34 +0400 Subject: [PATCH 09/12] Catch CouldNotReadUploadedFileException --- src/Subscription/Controller/SubscriberImportController.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Subscription/Controller/SubscriberImportController.php b/src/Subscription/Controller/SubscriberImportController.php index d084116..44fc335 100644 --- a/src/Subscription/Controller/SubscriberImportController.php +++ b/src/Subscription/Controller/SubscriberImportController.php @@ -7,6 +7,7 @@ use Exception; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Identity\Model\PrivilegeFlag; +use PhpList\Core\Domain\Subscription\Exception\CouldNotReadUploadedFileException; use PhpList\Core\Domain\Subscription\Model\Dto\SubscriberImportOptions; use PhpList\Core\Domain\Subscription\Service\SubscriberCsvImporter; use PhpList\Core\Security\Authentication; @@ -144,6 +145,10 @@ public function importSubscribers(Request $request): JsonResponse 'skipped' => $stats['skipped'], 'errors' => $stats['errors'] ]); + } catch (CouldNotReadUploadedFileException $exception) { + return $this->json([ + 'message' => 'Could not read uploaded file.' . $exception->getMessage() + ], Response::HTTP_BAD_REQUEST); } catch (Exception $e) { return $this->json([ 'message' => $e->getMessage() From 95b0e62d9bdecb553957e1c6f0d750a25ef97f37 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Sun, 12 Oct 2025 19:56:14 +0400 Subject: [PATCH 10/12] Flush in controller --- src/Identity/Controller/AdminAttributeValueController.php | 7 ++++++- src/Subscription/Controller/SubscriptionController.php | 6 ++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Identity/Controller/AdminAttributeValueController.php b/src/Identity/Controller/AdminAttributeValueController.php index ca89723..964bd83 100644 --- a/src/Identity/Controller/AdminAttributeValueController.php +++ b/src/Identity/Controller/AdminAttributeValueController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Identity\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Identity\Model\Filter\AdminAttributeValueFilter; use PhpList\Core\Domain\Identity\Model\Administrator; @@ -27,18 +28,21 @@ class AdminAttributeValueController extends BaseController private AdminAttributeManager $attributeManager; private AdminAttributeValueNormalizer $normalizer; private PaginatedDataProvider $paginatedDataProvider; + private EntityManagerInterface $entityManager; public function __construct( Authentication $authentication, RequestValidator $validator, AdminAttributeManager $attributeManager, AdminAttributeValueNormalizer $normalizer, - PaginatedDataProvider $paginatedDataProvider + PaginatedDataProvider $paginatedDataProvider, + EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->attributeManager = $attributeManager; $this->normalizer = $normalizer; $this->paginatedDataProvider = $paginatedDataProvider; + $this->entityManager = $entityManager; } #[Route( @@ -122,6 +126,7 @@ public function createOrUpdate( definition: $definition, value: $request->toArray()['value'] ?? null ); + $this->entityManager->flush(); $json = $this->normalizer->normalize($attributeDefinition, 'json'); return $this->json($json, Response::HTTP_CREATED); diff --git a/src/Subscription/Controller/SubscriptionController.php b/src/Subscription/Controller/SubscriptionController.php index b8bf72d..a7fffba 100644 --- a/src/Subscription/Controller/SubscriptionController.php +++ b/src/Subscription/Controller/SubscriptionController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Subscription\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Subscription\Model\SubscriberList; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriptionManager; @@ -28,16 +29,19 @@ class SubscriptionController extends BaseController { private SubscriptionManager $subscriptionManager; private SubscriptionNormalizer $subscriptionNormalizer; + private EntityManagerInterface $entityManager; public function __construct( Authentication $authentication, RequestValidator $validator, SubscriptionManager $subscriptionManager, SubscriptionNormalizer $subscriptionNormalizer, + EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->subscriptionManager = $subscriptionManager; $this->subscriptionNormalizer = $subscriptionNormalizer; + $this->entityManager = $entityManager; } #[Route('/{listId}/subscribers', name: 'create', requirements: ['listId' => '\d+'], methods: ['POST'])] @@ -127,6 +131,7 @@ public function createSubscription( /** @var SubscriptionRequest $subscriptionRequest */ $subscriptionRequest = $this->validator->validate($request, SubscriptionRequest::class); $subscriptions = $this->subscriptionManager->createSubscriptions($list, $subscriptionRequest->emails); + $this->entityManager->flush(); $normalized = array_map(fn($item) => $this->subscriptionNormalizer->normalize($item), $subscriptions); return $this->json($normalized, Response::HTTP_CREATED); @@ -193,6 +198,7 @@ public function deleteSubscriptions( /** @var SubscriptionRequest $subscriptionRequest */ $subscriptionRequest = $this->validator->validateDto($subscriptionRequest); $this->subscriptionManager->deleteSubscriptions($list, $subscriptionRequest->emails); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } From d68ce67804ff962eb4c86445ed1fa879a1ca2956 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 13 Oct 2025 11:06:45 +0400 Subject: [PATCH 11/12] Fix composer json --- composer.json | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index ba5526c..99e5fca 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,12 @@ "role": "Maintainer" } ], + "repositories": [ + { + "type": "vcs", + "url": "https://github.com/TatevikGr/rss-bundle.git" + } + ], "support": { "issues": "https://github.com/phpList/rest-api/issues", "forum": "https://discuss.phplist.org/", @@ -41,7 +47,8 @@ "symfony/test-pack": "^1.0", "symfony/process": "^6.4", "zircote/swagger-php": "^4.11", - "ext-dom": "*" + "ext-dom": "*", + "tatevikgr/rss-feed": "dev-main as 0.1.0" }, "require-dev": { "phpunit/phpunit": "^10.0", @@ -123,5 +130,10 @@ } } } + }, + "config": { + "allow-plugins": { + "php-http/discovery": true + } } } From 053422d5ce026d00b1cec0bee03a294ba56b5f17 Mon Sep 17 00:00:00 2001 From: TatevikGr Date: Wed, 22 Oct 2025 13:54:44 +0400 Subject: [PATCH 12/12] Em flush (#160) * Fix composer json * Add flush after delete * Fix: test * SubscriberBlacklistManager * listMessageManager * AdminController flush * TemplateController flush * SubscribePageController flush * save * fix test --------- Co-authored-by: Tatevik --- config/services/messenger_handlers.yml | 5 + .../AdminAttributeDefinitionController.php | 8 +- .../AdminAttributeValueController.php | 2 + .../Controller/AdministratorController.php | 7 +- .../Controller/PasswordResetController.php | 5 + src/Identity/Controller/SessionController.php | 4 + .../Controller/BounceRegexController.php | 4 + .../Controller/CampaignController.php | 28 +-- .../Controller/ListMessageController.php | 5 +- .../Controller/TemplateController.php | 7 +- src/Messaging/Service/CampaignService.php | 3 + .../Controller/BlacklistController.php | 4 + .../Controller/SubscribePageController.php | 4 + ...ubscriberAttributeDefinitionController.php | 7 +- .../SubscriberAttributeValueController.php | 6 +- .../Controller/SubscriberController.php | 41 +++-- .../Controller/SubscriberListController.php | 6 + .../Service/SubscriberService.php | 75 -------- .../Controller/SubscriberControllerTest.php | 2 + .../Messaging/Service/CampaignServiceTest.php | 8 +- .../Service/SubscriberServiceTest.php | 174 ------------------ 21 files changed, 120 insertions(+), 285 deletions(-) create mode 100644 config/services/messenger_handlers.yml delete mode 100644 src/Subscription/Service/SubscriberService.php delete mode 100644 tests/Unit/Subscription/Service/SubscriberServiceTest.php diff --git a/config/services/messenger_handlers.yml b/config/services/messenger_handlers.yml new file mode 100644 index 0000000..9073d52 --- /dev/null +++ b/config/services/messenger_handlers.yml @@ -0,0 +1,5 @@ +services: + PhpList\Core\Domain\Messaging\MessageHandler\CampaignProcessorMessageHandler: + autowire: true + autoconfigure: true + public: false diff --git a/src/Identity/Controller/AdminAttributeDefinitionController.php b/src/Identity/Controller/AdminAttributeDefinitionController.php index 772a56d..ff4c853 100644 --- a/src/Identity/Controller/AdminAttributeDefinitionController.php +++ b/src/Identity/Controller/AdminAttributeDefinitionController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Identity\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Identity\Model\AdminAttributeDefinition; use PhpList\Core\Domain\Identity\Service\AdminAttributeDefinitionManager; @@ -32,7 +33,8 @@ public function __construct( RequestValidator $validator, AdminAttributeDefinitionManager $definitionManager, AdminAttributeDefinitionNormalizer $normalizer, - PaginatedDataProvider $paginatedDataProvider + PaginatedDataProvider $paginatedDataProvider, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->definitionManager = $definitionManager; @@ -89,6 +91,8 @@ public function create(Request $request): JsonResponse $definitionRequest = $this->validator->validate($request, CreateAttributeDefinitionRequest::class); $attributeDefinition = $this->definitionManager->create($definitionRequest->getDto()); + $this->entityManager->flush(); + $json = $this->normalizer->normalize($attributeDefinition, 'json'); return $this->json($json, Response::HTTP_CREATED); @@ -156,6 +160,7 @@ public function update( attributeDefinition: $attributeDefinition, attributeDefinitionDto: $definitionRequest->getDto(), ); + $this->entityManager->flush(); $json = $this->normalizer->normalize($attributeDefinition, 'json'); return $this->json($json, Response::HTTP_OK); @@ -211,6 +216,7 @@ public function delete( } $this->definitionManager->delete($attributeDefinition); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } diff --git a/src/Identity/Controller/AdminAttributeValueController.php b/src/Identity/Controller/AdminAttributeValueController.php index 964bd83..573608b 100644 --- a/src/Identity/Controller/AdminAttributeValueController.php +++ b/src/Identity/Controller/AdminAttributeValueController.php @@ -198,6 +198,7 @@ public function delete( throw $this->createNotFoundException('Administrator attribute not found.'); } $this->attributeManager->delete($attribute); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } @@ -355,6 +356,7 @@ public function getAttributeDefinition( attributeDefinitionId: $definition->getId() ); $this->attributeManager->delete($attribute); + $this->entityManager->flush(); return $this->json( $this->normalizer->normalize($attribute), diff --git a/src/Identity/Controller/AdministratorController.php b/src/Identity/Controller/AdministratorController.php index 77e9288..365cf12 100644 --- a/src/Identity/Controller/AdministratorController.php +++ b/src/Identity/Controller/AdministratorController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Identity\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\Domain\Identity\Service\AdministratorManager; @@ -35,7 +36,8 @@ public function __construct( RequestValidator $validator, AdministratorManager $administratorManager, AdministratorNormalizer $normalizer, - PaginatedDataProvider $paginatedProvider + PaginatedDataProvider $paginatedProvider, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->administratorManager = $administratorManager; @@ -149,6 +151,7 @@ public function createAdministrator( $createRequest = $validator->validate($request, CreateAdministratorRequest::class); $administrator = $this->administratorManager->createAdministrator($createRequest->getDto()); + $this->entityManager->flush(); $json = $normalizer->normalize($administrator, 'json'); return $this->json($json, Response::HTTP_CREATED); @@ -255,6 +258,7 @@ public function updateAdministrator( /** @var UpdateAdministratorRequest $updateRequest */ $updateRequest = $this->validator->validate($request, UpdateAdministratorRequest::class); $this->administratorManager->updateAdministrator($administrator, $updateRequest->getDto()); + $this->entityManager->flush(); return $this->json($this->normalizer->normalize($administrator), Response::HTTP_OK); } @@ -303,6 +307,7 @@ public function deleteAdministrator( throw $this->createNotFoundException('Administrator not found.'); } $this->administratorManager->deleteAdministrator($administrator); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } diff --git a/src/Identity/Controller/PasswordResetController.php b/src/Identity/Controller/PasswordResetController.php index de5d3d6..a3527b0 100644 --- a/src/Identity/Controller/PasswordResetController.php +++ b/src/Identity/Controller/PasswordResetController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Identity\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Identity\Service\PasswordManager; use PhpList\Core\Security\Authentication; @@ -29,6 +30,7 @@ public function __construct( Authentication $authentication, RequestValidator $validator, PasswordManager $passwordManager, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); @@ -74,6 +76,7 @@ public function requestPasswordReset(Request $request): JsonResponse $resetRequest = $this->validator->validate($request, RequestPasswordResetRequest::class); $this->passwordManager->generatePasswordResetToken($resetRequest->email); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } @@ -117,6 +120,7 @@ public function validateToken(Request $request): JsonResponse $validateRequest = $this->validator->validate($request, ValidateTokenRequest::class); $administrator = $this->passwordManager->validatePasswordResetToken($validateRequest->token); + $this->entityManager->flush(); return $this->json([ 'valid' => $administrator !== null]); } @@ -169,6 +173,7 @@ public function resetPassword(Request $request): JsonResponse $resetRequest->token, $resetRequest->newPassword ); + $this->entityManager->flush(); if ($success) { return $this->json([ 'message' => 'Password updated successfully']); diff --git a/src/Identity/Controller/SessionController.php b/src/Identity/Controller/SessionController.php index 5f58811..66b49ce 100644 --- a/src/Identity/Controller/SessionController.php +++ b/src/Identity/Controller/SessionController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Identity\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Identity\Model\AdministratorToken; use PhpList\Core\Domain\Identity\Service\SessionManager; @@ -34,6 +35,7 @@ public function __construct( Authentication $authentication, RequestValidator $validator, SessionManager $sessionManager, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); @@ -96,6 +98,7 @@ public function createSession( loginName:$createSessionRequest->loginName, password: $createSessionRequest->password ); + $this->entityManager->flush(); $json = $normalizer->normalize($token, 'json'); @@ -163,6 +166,7 @@ public function deleteSession( } $this->sessionManager->deleteSession($token); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } diff --git a/src/Messaging/Controller/BounceRegexController.php b/src/Messaging/Controller/BounceRegexController.php index 7f3f275..c9e9a1c 100644 --- a/src/Messaging/Controller/BounceRegexController.php +++ b/src/Messaging/Controller/BounceRegexController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Messaging\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Messaging\Service\Manager\BounceRegexManager; use PhpList\Core\Security\Authentication; @@ -27,6 +28,7 @@ public function __construct( RequestValidator $validator, private readonly BounceRegexManager $manager, private readonly BounceRegexNormalizer $normalizer, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); } @@ -188,6 +190,7 @@ public function createOrUpdate(Request $request): JsonResponse comment: $dto->comment, status: $dto->status ); + $this->entityManager->flush(); return $this->json($this->normalizer->normalize($entity), Response::HTTP_CREATED); } @@ -240,6 +243,7 @@ public function delete(Request $request, string $regexHash): JsonResponse throw $this->createNotFoundException('Bounce regex not found.'); } $this->manager->delete($entity); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } diff --git a/src/Messaging/Controller/CampaignController.php b/src/Messaging/Controller/CampaignController.php index d897706..b1b8e80 100644 --- a/src/Messaging/Controller/CampaignController.php +++ b/src/Messaging/Controller/CampaignController.php @@ -4,9 +4,10 @@ namespace PhpList\RestBundle\Messaging\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; +use PhpList\Core\Domain\Messaging\Message\SyncCampaignProcessorMessage; use PhpList\Core\Domain\Messaging\Model\Message; -use PhpList\Core\Domain\Messaging\Service\Processor\CampaignProcessor; use PhpList\Core\Security\Authentication; use PhpList\RestBundle\Common\Controller\BaseController; use PhpList\RestBundle\Common\Validator\RequestValidator; @@ -17,6 +18,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Messenger\MessageBusInterface; use Symfony\Component\Routing\Attribute\Route; /** @@ -28,17 +30,18 @@ class CampaignController extends BaseController { private CampaignService $campaignService; - private CampaignProcessor $campaignProcessor; + private MessageBusInterface $messageBus; public function __construct( Authentication $authentication, RequestValidator $validator, CampaignService $campaignService, - CampaignProcessor $campaignProcessor, + MessageBusInterface $messageBus, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->campaignService = $campaignService; - $this->campaignProcessor = $campaignProcessor; + $this->messageBus = $messageBus; } #[Route('', name: 'get_list', methods: ['GET'])] @@ -211,11 +214,10 @@ public function createMessage(Request $request): JsonResponse /** @var CreateMessageRequest $createMessageRequest */ $createMessageRequest = $this->validator->validate($request, CreateMessageRequest::class); + $message = $this->campaignService->createMessage($createMessageRequest, $authUser); + $this->entityManager->flush(); - return $this->json( - $this->campaignService->createMessage($createMessageRequest, $authUser), - Response::HTTP_CREATED - ); + return $this->json(data: $message, status: Response::HTTP_CREATED); } #[Route('/{messageId}', name: 'update', requirements: ['messageId' => '\d+'], methods: ['PUT'])] @@ -284,11 +286,10 @@ public function updateMessage( /** @var UpdateMessageRequest $updateMessageRequest */ $updateMessageRequest = $this->validator->validate($request, UpdateMessageRequest::class); + $message = $this->campaignService->updateMessage($updateMessageRequest, $authUser, $message); + $this->entityManager->flush(); - return $this->json( - $this->campaignService->updateMessage($updateMessageRequest, $authUser, $message), - Response::HTTP_OK - ); + return $this->json(data:$message, status: Response::HTTP_OK); } #[Route('/{messageId}', name: 'delete', requirements: ['messageId' => '\d+'], methods: ['DELETE'])] @@ -339,6 +340,7 @@ public function deleteMessage( $authUser = $this->requireAuthentication($request); $this->campaignService->deleteMessage($authUser, $message); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } @@ -388,7 +390,7 @@ public function sendMessage( throw $this->createNotFoundException('Campaign not found.'); } - $this->campaignProcessor->process($message); + $this->messageBus->dispatch(new SyncCampaignProcessorMessage($message->getId())); return $this->json($this->campaignService->getMessage($message), Response::HTTP_OK); } diff --git a/src/Messaging/Controller/ListMessageController.php b/src/Messaging/Controller/ListMessageController.php index 0ee3eb9..f67c8fa 100644 --- a/src/Messaging/Controller/ListMessageController.php +++ b/src/Messaging/Controller/ListMessageController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Messaging\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Messaging\Model\Message; use PhpList\Core\Domain\Messaging\Service\Manager\ListMessageManager; @@ -37,7 +38,8 @@ public function __construct( ListMessageManager $listMessageManager, ListMessageNormalizer $listMessageNormalizer, SubscriberListNormalizer $subscriberListNormalizer, - MessageNormalizer $messageNormalizer + MessageNormalizer $messageNormalizer, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->listMessageManager = $listMessageManager; @@ -262,6 +264,7 @@ public function associateMessageWithList( } $listMessage = $this->listMessageManager->associateMessageWithList($message, $subscriberList); + $this->entityManager->flush(); return $this->json( data: $this->listMessageNormalizer->normalize($listMessage), diff --git a/src/Messaging/Controller/TemplateController.php b/src/Messaging/Controller/TemplateController.php index b814c89..bc24a02 100644 --- a/src/Messaging/Controller/TemplateController.php +++ b/src/Messaging/Controller/TemplateController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Messaging\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Messaging\Model\Template; use PhpList\Core\Domain\Messaging\Service\Manager\TemplateManager; @@ -37,6 +38,7 @@ public function __construct( TemplateNormalizer $normalizer, TemplateManager $templateManager, PaginatedDataProvider $paginatedDataProvider, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->normalizer = $normalizer; @@ -260,9 +262,11 @@ public function createTemplates(Request $request): JsonResponse /** @var CreateTemplateRequest $createTemplateRequest */ $createTemplateRequest = $this->validator->validate($request, CreateTemplateRequest::class); + $template = $this->templateManager->create($createTemplateRequest->getDto()); + $this->entityManager->flush(); return $this->json( - $this->normalizer->normalize($this->templateManager->create($createTemplateRequest->getDto())), + $this->normalizer->normalize($template), Response::HTTP_CREATED ); } @@ -318,6 +322,7 @@ public function delete( } $this->templateManager->delete($template); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } diff --git a/src/Messaging/Service/CampaignService.php b/src/Messaging/Service/CampaignService.php index d2cd8c0..4bc9e3c 100644 --- a/src/Messaging/Service/CampaignService.php +++ b/src/Messaging/Service/CampaignService.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Messaging\Service; +use Doctrine\ORM\EntityManagerInterface; use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\Domain\Identity\Model\PrivilegeFlag; use PhpList\Core\Domain\Messaging\Model\Filter\MessageFilter; @@ -23,6 +24,7 @@ public function __construct( private readonly MessageManager $messageManager, private readonly PaginatedDataProvider $paginatedProvider, private readonly MessageNormalizer $normalizer, + private readonly EntityManagerInterface $entityManager, ) { } @@ -86,5 +88,6 @@ public function deleteMessage(Administrator $administrator, Message $message = n } $this->messageManager->delete($message); + $this->entityManager->flush(); } } diff --git a/src/Subscription/Controller/BlacklistController.php b/src/Subscription/Controller/BlacklistController.php index f76b094..9b4e958 100644 --- a/src/Subscription/Controller/BlacklistController.php +++ b/src/Subscription/Controller/BlacklistController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Subscription\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Identity\Model\PrivilegeFlag; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberBlacklistManager; @@ -31,6 +32,7 @@ public function __construct( RequestValidator $validator, SubscriberBlacklistManager $blacklistManager, UserBlacklistNormalizer $normalizer, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->authentication = $authentication; @@ -156,6 +158,7 @@ public function addEmailToBlacklist(Request $request): JsonResponse email: $definitionRequest->email, reasonData: $definitionRequest->reason ); + $this->entityManager->flush(); $json = $this->normalizer->normalize($userBlacklisted, 'json'); return $this->json($json, Response::HTTP_CREATED); @@ -209,6 +212,7 @@ public function removeEmailFromBlacklist(Request $request, string $email): JsonR } $this->blacklistManager->removeEmailFromBlacklist($email); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } diff --git a/src/Subscription/Controller/SubscribePageController.php b/src/Subscription/Controller/SubscribePageController.php index 8f3878c..38117c8 100644 --- a/src/Subscription/Controller/SubscribePageController.php +++ b/src/Subscription/Controller/SubscribePageController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Subscription\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Identity\Model\PrivilegeFlag; use PhpList\Core\Domain\Subscription\Model\SubscribePage; @@ -28,6 +29,7 @@ public function __construct( RequestValidator $validator, private readonly SubscribePageManager $subscribePageManager, private readonly SubscribePageNormalizer $normalizer, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); } @@ -141,6 +143,7 @@ public function createPage(Request $request): JsonResponse $createRequest = $this->validator->validate($request, SubscribePageRequest::class); $page = $this->subscribePageManager->createPage($createRequest->title, $createRequest->active, $admin); + $this->entityManager->flush(); return $this->json($this->normalizer->normalize($page), Response::HTTP_CREATED); } @@ -423,6 +426,7 @@ public function setPageData( $createRequest = $this->validator->validate($request, SubscribePageDataRequest::class); $item = $this->subscribePageManager->setPageData($page, $createRequest->name, $createRequest->value); + $this->entityManager->flush(); return $this->json([ 'id' => $item->getId(), diff --git a/src/Subscription/Controller/SubscriberAttributeDefinitionController.php b/src/Subscription/Controller/SubscriberAttributeDefinitionController.php index 6d7bad3..e096552 100644 --- a/src/Subscription/Controller/SubscriberAttributeDefinitionController.php +++ b/src/Subscription/Controller/SubscriberAttributeDefinitionController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Subscription\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition; use PhpList\Core\Domain\Subscription\Service\Manager\AttributeDefinitionManager; @@ -32,7 +33,8 @@ public function __construct( RequestValidator $validator, AttributeDefinitionManager $definitionManager, AttributeDefinitionNormalizer $normalizer, - PaginatedDataProvider $paginatedDataProvider + PaginatedDataProvider $paginatedDataProvider, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->definitionManager = $definitionManager; @@ -87,6 +89,7 @@ public function create(Request $request): JsonResponse $definitionRequest = $this->validator->validate($request, CreateAttributeDefinitionRequest::class); $attributeDefinition = $this->definitionManager->create($definitionRequest->getDto()); + $this->entityManager->flush(); $json = $this->normalizer->normalize($attributeDefinition, 'json'); return $this->json($json, Response::HTTP_CREATED); @@ -154,6 +157,7 @@ public function update( attributeDefinition: $attributeDefinition, attributeDefinitionDto: $definitionRequest->getDto(), ); + $this->entityManager->flush(); $json = $this->normalizer->normalize($attributeDefinition, 'json'); return $this->json($json, Response::HTTP_OK); @@ -209,6 +213,7 @@ public function delete( } $this->definitionManager->delete($attributeDefinition); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } diff --git a/src/Subscription/Controller/SubscriberAttributeValueController.php b/src/Subscription/Controller/SubscriberAttributeValueController.php index 834b097..24b5420 100644 --- a/src/Subscription/Controller/SubscriberAttributeValueController.php +++ b/src/Subscription/Controller/SubscriberAttributeValueController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Subscription\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Subscription\Model\Filter\SubscriberAttributeValueFilter; use PhpList\Core\Domain\Subscription\Model\Subscriber; @@ -33,7 +34,8 @@ public function __construct( RequestValidator $validator, SubscriberAttributeManager $attributeManager, SubscriberAttributeValueNormalizer $normalizer, - PaginatedDataProvider $paginatedDataProvider + PaginatedDataProvider $paginatedDataProvider, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->attributeManager = $attributeManager; @@ -193,6 +195,7 @@ public function delete( throw $this->createNotFoundException('Subscriber attribute not found.'); } $this->attributeManager->delete($attribute); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } @@ -349,6 +352,7 @@ public function getAttributeDefinition( } $attribute = $this->attributeManager->getSubscriberAttribute($subscriber->getId(), $definition->getId()); $this->attributeManager->delete($attribute); + $this->entityManager->flush(); return $this->json( $this->normalizer->normalize($attribute), diff --git a/src/Subscription/Controller/SubscriberController.php b/src/Subscription/Controller/SubscriberController.php index f053dff..e144e20 100644 --- a/src/Subscription/Controller/SubscriberController.php +++ b/src/Subscription/Controller/SubscriberController.php @@ -4,19 +4,23 @@ namespace PhpList\RestBundle\Subscription\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Identity\Model\PrivilegeFlag; use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager; use PhpList\Core\Security\Authentication; use PhpList\RestBundle\Common\Controller\BaseController; use PhpList\RestBundle\Common\Validator\RequestValidator; use PhpList\RestBundle\Subscription\Request\CreateSubscriberRequest; use PhpList\RestBundle\Subscription\Request\UpdateSubscriberRequest; -use PhpList\RestBundle\Subscription\Service\SubscriberService; +use PhpList\RestBundle\Subscription\Serializer\SubscriberNormalizer; +use PhpList\RestBundle\Subscription\Service\SubscriberHistoryService; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Attribute\Route; /** @@ -28,16 +32,16 @@ #[Route('/subscribers', name: 'subscriber_')] class SubscriberController extends BaseController { - private SubscriberService $subscriberService; - public function __construct( Authentication $authentication, RequestValidator $validator, - SubscriberService $subscriberService, + private readonly SubscriberManager $subscriberManager, + private readonly SubscriberNormalizer $subscriberNormalizer, + private readonly SubscriberHistoryService $subscriberHistoryService, + private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->authentication = $authentication; - $this->subscriberService = $subscriberService; } #[Route('', name: 'create', methods: ['POST'])] @@ -93,7 +97,9 @@ public function createSubscriber(Request $request): JsonResponse /** @var CreateSubscriberRequest $subscriberRequest */ $subscriberRequest = $this->validator->validate($request, CreateSubscriberRequest::class); - $subscriberData = $this->subscriberService->createSubscriber($subscriberRequest); + $subscriber = $this->subscriberManager->createSubscriber($subscriberRequest->getDto()); + $this->entityManager->flush(); + $subscriberData = $this->subscriberNormalizer->normalize($subscriber, 'json'); return $this->json($subscriberData, Response::HTTP_CREATED); } @@ -163,7 +169,9 @@ public function updateSubscriber( } /** @var UpdateSubscriberRequest $updateSubscriberRequest */ $updateSubscriberRequest = $this->validator->validate($request, UpdateSubscriberRequest::class); - $subscriberData = $this->subscriberService->updateSubscriber($updateSubscriberRequest); + $subscriber = $this->subscriberManager->updateSubscriber($updateSubscriberRequest->getDto(), $admin); + $this->entityManager->flush(); + $subscriberData = $this->subscriberNormalizer->normalize($subscriber, 'json'); return $this->json($subscriberData, Response::HTTP_OK); } @@ -213,7 +221,8 @@ public function getSubscriber(Request $request, int $subscriberId): JsonResponse { $this->requireAuthentication($request); - $subscriberData = $this->subscriberService->getSubscriber($subscriberId); + $subscriber = $this->subscriberManager->getSubscriberById($subscriberId); + $subscriberData = $this->subscriberNormalizer->normalize($subscriber); return $this->json($subscriberData, Response::HTTP_OK); } @@ -309,7 +318,7 @@ public function getSubscriberHistory( ): JsonResponse { $this->requireAuthentication($request); - $historyData = $this->subscriberService->getSubscriberHistory($request, $subscriber); + $historyData = $this->subscriberHistoryService->getSubscriberHistory($request, $subscriber); return $this->json( data: $historyData, @@ -370,7 +379,8 @@ public function deleteSubscriber( if (!$subscriber) { throw $this->createNotFoundException('Subscriber not found.'); } - $this->subscriberService->deleteSubscriber($subscriber); + $this->subscriberManager->deleteSubscriber($subscriber); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } @@ -438,7 +448,9 @@ public function resetBounceCount( throw $this->createNotFoundException('Subscriber not found.'); } - $subscriberData = $this->subscriberService->resetSubscriberBounceCount($subscriber); + $subscriber = $this->subscriberManager->resetBounceCount($subscriber); + $this->entityManager->flush(); + $subscriberData = $this->subscriberNormalizer->normalize($subscriber, 'json'); return $this->json($subscriberData, Response::HTTP_OK); } @@ -484,9 +496,10 @@ public function setSubscriberAsConfirmed(Request $request): Response return new Response('

Missing confirmation code.

', 400); } - $subscriber = $this->subscriberService->confirmSubscriber($uniqueId); - - if (!$subscriber) { + try { + $this->subscriberManager->markAsConfirmedByUniqueId($uniqueId); + $this->entityManager->flush(); + } catch (NotFoundHttpException) { return new Response('

Subscriber isn\'t found or already confirmed.

', 404); } diff --git a/src/Subscription/Controller/SubscriberListController.php b/src/Subscription/Controller/SubscriberListController.php index a0ea079..304c343 100644 --- a/src/Subscription/Controller/SubscriberListController.php +++ b/src/Subscription/Controller/SubscriberListController.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Subscription\Controller; +use Doctrine\ORM\EntityManagerInterface; use OpenApi\Attributes as OA; use PhpList\Core\Domain\Subscription\Model\SubscriberList; use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberListManager; @@ -32,6 +33,7 @@ class SubscriberListController extends BaseController private SubscriberListNormalizer $normalizer; private SubscriberListManager $subscriberListManager; private PaginatedDataProvider $paginatedDataProvider; + private EntityManagerInterface $entityManager; public function __construct( Authentication $authentication, @@ -39,11 +41,13 @@ public function __construct( SubscriberListNormalizer $normalizer, SubscriberListManager $subscriberListManager, PaginatedDataProvider $paginatedDataProvider, + EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); $this->normalizer = $normalizer; $this->subscriberListManager = $subscriberListManager; $this->paginatedDataProvider = $paginatedDataProvider; + $this->entityManager = $entityManager; } #[Route('', name: 'get_list', methods: ['GET'])] @@ -223,6 +227,7 @@ public function deleteList( } $this->subscriberListManager->delete($list); + $this->entityManager->flush(); return $this->json(null, Response::HTTP_NO_CONTENT); } @@ -273,6 +278,7 @@ public function createList(Request $request, SubscriberListNormalizer $normalize /** @var CreateSubscriberListRequest $subscriberListRequest */ $subscriberListRequest = $this->validator->validate($request, CreateSubscriberListRequest::class); $data = $this->subscriberListManager->createSubscriberList($subscriberListRequest->getDto(), $authUser); + $this->entityManager->flush(); return $this->json($normalizer->normalize($data), Response::HTTP_CREATED); } diff --git a/src/Subscription/Service/SubscriberService.php b/src/Subscription/Service/SubscriberService.php deleted file mode 100644 index 2ddce62..0000000 --- a/src/Subscription/Service/SubscriberService.php +++ /dev/null @@ -1,75 +0,0 @@ - - */ -class SubscriberService -{ - public function __construct( - private readonly SubscriberManager $subscriberManager, - private readonly SubscriberNormalizer $subscriberNormalizer, - private readonly SubscriberHistoryService $subscriberHistoryService, - ) { - } - - public function createSubscriber(CreateSubscriberRequest $subscriberRequest): array - { - $subscriber = $this->subscriberManager->createSubscriber($subscriberRequest->getDto()); - return $this->subscriberNormalizer->normalize($subscriber, 'json'); - } - - public function updateSubscriber(UpdateSubscriberRequest $updateSubscriberRequest): array - { - $subscriber = $this->subscriberManager->updateSubscriber($updateSubscriberRequest->getDto()); - return $this->subscriberNormalizer->normalize($subscriber, 'json'); - } - - public function resetSubscriberBounceCount(Subscriber $subscriber): array - { - $subscriber = $this->subscriberManager->resetBounceCount($subscriber); - return $this->subscriberNormalizer->normalize($subscriber, 'json'); - } - - public function getSubscriber(int $subscriberId): array - { - $subscriber = $this->subscriberManager->getSubscriberById($subscriberId); - return $this->subscriberNormalizer->normalize($subscriber); - } - - public function getSubscriberHistory(Request $request, ?Subscriber $subscriber): array - { - return $this->subscriberHistoryService->getSubscriberHistory($request, $subscriber); - } - - public function deleteSubscriber(Subscriber $subscriber): void - { - $this->subscriberManager->deleteSubscriber($subscriber); - } - - public function confirmSubscriber(string $uniqueId): ?Subscriber - { - if (!$uniqueId) { - return null; - } - - try { - return $this->subscriberManager->markAsConfirmedByUniqueId($uniqueId); - } catch (NotFoundHttpException) { - return null; - } - } -} diff --git a/tests/Integration/Subscription/Controller/SubscriberControllerTest.php b/tests/Integration/Subscription/Controller/SubscriberControllerTest.php index 3ffb4dd..93cf93d 100644 --- a/tests/Integration/Subscription/Controller/SubscriberControllerTest.php +++ b/tests/Integration/Subscription/Controller/SubscriberControllerTest.php @@ -72,6 +72,8 @@ public function testPostSubscribersWithValidSessionKeyAndMinimalValidDataReturns self::assertMatchesRegularExpression('/^[0-9a-f]{32}$/', $responseContent['unique_id']); } + + public function testPostSubscribersWithValidSessionKeyAndValidDataCreatesSubscriber() { $email = 'subscriber@example.com'; diff --git a/tests/Unit/Messaging/Service/CampaignServiceTest.php b/tests/Unit/Messaging/Service/CampaignServiceTest.php index 0a3d2e4..e328fe9 100644 --- a/tests/Unit/Messaging/Service/CampaignServiceTest.php +++ b/tests/Unit/Messaging/Service/CampaignServiceTest.php @@ -4,6 +4,7 @@ namespace PhpList\RestBundle\Tests\Unit\Messaging\Service; +use Doctrine\ORM\EntityManagerInterface; use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\Domain\Identity\Model\PrivilegeFlag; use PhpList\Core\Domain\Identity\Model\Privileges; @@ -37,9 +38,10 @@ protected function setUp(): void $this->normalizer = $this->createMock(MessageNormalizer::class); $this->campaignService = new CampaignService( - $this->messageManager, - $this->paginatedProvider, - $this->normalizer + messageManager: $this->messageManager, + paginatedProvider: $this->paginatedProvider, + normalizer: $this->normalizer, + entityManager: $this->createMock(EntityManagerInterface::class), ); } diff --git a/tests/Unit/Subscription/Service/SubscriberServiceTest.php b/tests/Unit/Subscription/Service/SubscriberServiceTest.php deleted file mode 100644 index ffc27dc..0000000 --- a/tests/Unit/Subscription/Service/SubscriberServiceTest.php +++ /dev/null @@ -1,174 +0,0 @@ -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('getSubscriberById') - ->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); - } -}