diff --git a/composer.json b/composer.json index 89dbf442..ba5526cd 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 37f0b029..99253992 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 66ee7def..2179f6ad 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 00000000..7f3f275b --- /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 6513db26..b814c89e 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 a14e9b58..9e4cfb55 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 00000000..69771191 --- /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 00000000..5771bd8b --- /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 b6680d1e..d2cd8c0a 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 00000000..4c7872e4 --- /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 00000000..8767477f --- /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 00000000..a86b0657 --- /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 5293f0f9..0a3d2e4a 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;