diff --git a/composer.json b/composer.json index 92598e8..477d302 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,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/src/Messaging/Controller/CampaignActionController.php b/src/Messaging/Controller/CampaignActionController.php new file mode 100644 index 0000000..a95702e --- /dev/null +++ b/src/Messaging/Controller/CampaignActionController.php @@ -0,0 +1,284 @@ + + */ +#[Route('/campaigns', name: 'campaign_')] +class CampaignActionController extends BaseController +{ + public function __construct( + Authentication $authentication, + RequestValidator $validator, + private readonly CampaignService $campaignService, + private readonly MessageBusInterface $messageBus, + private readonly EntityManagerInterface $entityManager, + private readonly MessageManager $messageManager, + ) { + parent::__construct($authentication, $validator); + } + + #[Route('/{messageId}/copy', name: 'copy_campaign', requirements: ['messageId' => '\d+'], methods: ['POST'])] + #[OA\Post( + path: '/api/v2/campaigns/{messageId}/copy', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Copies campaign/message by id into a draft message.', + summary: 'Copies campaign/message by id.', + tags: ['campaigns'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'messageId', + description: 'message ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 201, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/Message') + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ) + ] + )] + public function copyMessage( + Request $request, + #[MapEntity(mapping: ['messageId' => 'id'])] ?Message $message = null + ): JsonResponse { + $authUser = $this->requireAuthentication($request); + if ($message === null) { + throw $this->createNotFoundException('Campaign not found.'); + } + + $message = $this->messageManager->copyAsDraftMessage($message, $authUser); + $this->entityManager->flush(); + + return $this->json($this->campaignService->getMessage($message), Response::HTTP_CREATED); + } + + #[Route('/{messageId}/status', name: 'update_status', requirements: ['messageId' => '\d+'], methods: ['PATCH'])] + #[OA\Patch( + path: '/api/v2/campaigns/{messageId}/status', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Updates campaign/message status by id.', + summary: 'Update campaign status by id.', + requestBody: new OA\RequestBody( + description: 'Update message status.', + required: true, + content: new OA\JsonContent(ref: '#/components/schemas/MessageMetadataRequest') + ), + tags: ['campaigns'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema( + type: 'string' + ) + ), + new OA\Parameter( + name: 'messageId', + description: 'message ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/Message') + ), + 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') + ), + new OA\Response( + response: 422, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/ValidationErrorResponse') + ), + ] + )] + public function updateMessageStatus( + Request $request, + #[MapEntity(mapping: ['messageId' => 'id'])] ?Message $message = null, + ): JsonResponse { + $this->requireAuthentication($request); + if ($message === null) { + throw $this->createNotFoundException('Message not found.'); + } + + /** @var MessageMetadataRequest $messageMetadataRequest */ + $messageMetadataRequest = $this->validator->validate($request, MessageMetadataRequest::class); + + $message = $this->messageManager->updateStatus( + $message, + MessageStatus::from($messageMetadataRequest->status), + ); + $this->entityManager->flush(); + + return $this->json($message, Response::HTTP_OK); + } + + #[Route('/{messageId}/send', name: 'send_campaign', requirements: ['messageId' => '\d+'], methods: ['POST'])] + #[OA\Post( + path: '/api/v2/campaigns/{messageId}/send', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Processes/sends campaign/message by id.', + summary: 'Processes/sends campaign/message by id.', + tags: ['campaigns'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'messageId', + description: 'message ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/Message') + ), + 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 sendMessage( + Request $request, + #[MapEntity(mapping: ['messageId' => 'id'])] ?Message $message = null + ): JsonResponse { + $this->requireAuthentication($request); + if ($message === null) { + throw $this->createNotFoundException('Campaign not found.'); + } + + $this->messageBus->dispatch(new SyncCampaignProcessorMessage($message->getId())); + + return $this->json($this->campaignService->getMessage($message), Response::HTTP_OK); + } + + #[Route('/{messageId}/resend', name: 'resend_campaign', requirements: ['messageId' => '\d+'], methods: ['POST'])] + #[OA\Post( + path: '/api/v2/campaigns/{messageId}/resend', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Processes/sends campaign/message by id to specified mailing lists.', + summary: 'Processes/sends campaign/message by id to lists.', + requestBody: new OA\RequestBody( + description: 'List ids to send this campaign to.', + required: true, + content: new OA\JsonContent(ref: '#/components/schemas/ResendMessageToListsRequest') + ), + tags: ['campaigns'], + parameters: [ + new OA\Parameter( + name: 'php-auth-pw', + description: 'Session key obtained from login', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'messageId', + description: 'message ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/Message') + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ) + ] + )] + public function resendMessageToLists( + Request $request, + #[MapEntity(mapping: ['messageId' => 'id'])] ?Message $message = null + ): JsonResponse { + $this->requireAuthentication($request); + if ($message === null) { + throw $this->createNotFoundException('Campaign not found.'); + } + + /** @var ResendMessageToListsRequest $resendToListsRequest */ + $resendToListsRequest = $this->validator->validate($request, ResendMessageToListsRequest::class); + + $this->messageBus->dispatch( + new SyncCampaignProcessorMessage($message->getId(), $resendToListsRequest->listIds) + ); + + return $this->json($this->campaignService->getMessage($message), Response::HTTP_OK); + } +} diff --git a/src/Messaging/Controller/CampaignController.php b/src/Messaging/Controller/CampaignController.php index 1501524..5b7a1dc 100644 --- a/src/Messaging/Controller/CampaignController.php +++ b/src/Messaging/Controller/CampaignController.php @@ -6,20 +6,17 @@ 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\Security\Authentication; use PhpList\RestBundle\Common\Controller\BaseController; use PhpList\RestBundle\Common\Validator\RequestValidator; use PhpList\RestBundle\Messaging\Request\CreateMessageRequest; -use PhpList\RestBundle\Messaging\Request\ResendMessageToListsRequest; use PhpList\RestBundle\Messaging\Request\UpdateMessageRequest; use PhpList\RestBundle\Messaging\Service\CampaignService; use Symfony\Bridge\Doctrine\Attribute\MapEntity; 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; /** @@ -34,7 +31,6 @@ public function __construct( Authentication $authentication, RequestValidator $validator, private readonly CampaignService $campaignService, - private readonly MessageBusInterface $messageBus, private readonly EntityManagerInterface $entityManager, ) { parent::__construct($authentication, $validator); @@ -356,114 +352,4 @@ public function deleteMessage( return $this->json(null, Response::HTTP_NO_CONTENT); } - - #[Route('/{messageId}/send', name: 'send_campaign', requirements: ['messageId' => '\d+'], methods: ['POST'])] - #[OA\Post( - path: '/api/v2/campaigns/{messageId}/send', - description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . - 'Processes/sends campaign/message by id.', - summary: 'Processes/sends campaign/message by id.', - tags: ['campaigns'], - parameters: [ - new OA\Parameter( - name: 'php-auth-pw', - description: 'Session key obtained from login', - in: 'header', - required: true, - schema: new OA\Schema(type: 'string') - ), - new OA\Parameter( - name: 'messageId', - description: 'message ID', - in: 'path', - required: true, - schema: new OA\Schema(type: 'string') - ) - ], - responses: [ - new OA\Response( - response: 200, - description: 'Success', - content: new OA\JsonContent(ref: '#/components/schemas/Message') - ), - new OA\Response( - response: 403, - description: 'Failure', - content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') - ) - ] - )] - public function sendMessage( - Request $request, - #[MapEntity(mapping: ['messageId' => 'id'])] ?Message $message = null - ): JsonResponse { - $this->requireAuthentication($request); - if ($message === null) { - throw $this->createNotFoundException('Campaign not found.'); - } - - $this->messageBus->dispatch(new SyncCampaignProcessorMessage($message->getId())); - - return $this->json($this->campaignService->getMessage($message), Response::HTTP_OK); - } - - #[Route('/{messageId}/resend', name: 'resend_campaign', requirements: ['messageId' => '\d+'], methods: ['POST'])] - #[OA\Post( - path: '/api/v2/campaigns/{messageId}/resend', - description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . - 'Processes/sends campaign/message by id to specified mailing lists.', - summary: 'Processes/sends campaign/message by id to lists.', - requestBody: new OA\RequestBody( - description: 'List ids to send this campaign to.', - required: true, - content: new OA\JsonContent(ref: '#/components/schemas/AdminAttributeDefinitionRequest') - ), - tags: ['campaigns'], - parameters: [ - new OA\Parameter( - name: 'php-auth-pw', - description: 'Session key obtained from login', - in: 'header', - required: true, - schema: new OA\Schema(type: 'string') - ), - new OA\Parameter( - name: 'messageId', - description: 'message ID', - in: 'path', - required: true, - schema: new OA\Schema(type: 'string') - ) - ], - responses: [ - new OA\Response( - response: 200, - description: 'Success', - content: new OA\JsonContent(ref: '#/components/schemas/Message') - ), - new OA\Response( - response: 403, - description: 'Failure', - content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') - ) - ] - )] - public function resendMessageToLists( - Request $request, - #[MapEntity(mapping: ['messageId' => 'id'])] ?Message $message = null - ): JsonResponse { - $this->requireAuthentication($request); - if ($message === null) { - throw $this->createNotFoundException('Campaign not found.'); - } - - /** @var ResendMessageToListsRequest $resendToListsRequest */ - $resendToListsRequest = $this->validator->validate($request, ResendMessageToListsRequest::class); - - $this->messageBus->dispatch( - new SyncCampaignProcessorMessage($message->getId(), $resendToListsRequest->listIds) - ); - - return $this->json($this->campaignService->getMessage($message), Response::HTTP_OK); - } } diff --git a/src/Messaging/Request/Message/MessageMetadataRequest.php b/src/Messaging/Request/Message/MessageMetadataRequest.php index 7679d84..4eb5045 100644 --- a/src/Messaging/Request/Message/MessageMetadataRequest.php +++ b/src/Messaging/Request/Message/MessageMetadataRequest.php @@ -7,16 +7,18 @@ use OpenApi\Attributes as OA; use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageMetadataDto; use PhpList\Core\Domain\Messaging\Model\Message\MessageStatus; +use PhpList\RestBundle\Common\Request\RequestInterface; use Symfony\Component\Validator\Constraints as Assert; #[OA\Schema( schema: 'MessageMetadataRequest', + required: ['status'], properties: [ new OA\Property(property: 'status', type: 'string', example: 'draft'), ], type: 'object' )] -class MessageMetadataRequest implements RequestDtoInterface +class MessageMetadataRequest implements RequestDtoInterface, RequestInterface { #[Assert\NotBlank] public string $status; diff --git a/tests/Integration/Messaging/Controller/CampaignActionControllerTest.php b/tests/Integration/Messaging/Controller/CampaignActionControllerTest.php new file mode 100644 index 0000000..f62d9d8 --- /dev/null +++ b/tests/Integration/Messaging/Controller/CampaignActionControllerTest.php @@ -0,0 +1,77 @@ +get(CampaignActionController::class) + ); + } + + public function testSendMessageWithoutSessionReturnsForbidden(): void + { + $this->loadFixtures([MessageFixture::class]); + self::getClient()->request('POST', '/api/v2/campaigns/1/send'); + $this->assertHttpForbidden(); + } + + public function testSendMessageWithValidSessionReturnsOkay(): void + { + $this->loadFixtures([AdministratorFixture::class, MessageFixture::class]); + + $this->authenticatedJsonRequest('POST', '/api/v2/campaigns/2/send'); + $this->assertHttpOkay(); + + $response = $this->getDecodedJsonResponseContent(); + self::assertSame(2, $response['id']); + } + + public function testSendMessageWithInvalidIdReturnsNotFound(): void + { + $this->authenticatedJsonRequest('POST', '/api/v2/campaigns/999/send'); + $this->assertHttpNotFound(); + } + + public function testResendMessageToListsWithoutSessionReturnsForbidden(): void + { + $this->loadFixtures([MessageFixture::class, SubscriberListFixture::class]); + + $this->jsonRequest('POST', '/api/v2/campaigns/2/resend', [], [], [], json_encode(['list_ids' => [1]])); + $this->assertHttpForbidden(); + } + + public function testResendMessageToListsWithValidSessionReturnsOkay(): void + { + $this->loadFixtures([MessageFixture::class, SubscriberListFixture::class]); + + $this->authenticatedJsonRequest('POST', '/api/v2/campaigns/2/resend', [], [], [], json_encode([ + 'list_ids' => [1], + ])); + $this->assertHttpOkay(); + + $response = $this->getDecodedJsonResponseContent(); + self::assertSame(2, $response['id']); + } + + public function testResendMessageToListsWithInvalidIdReturnsNotFound(): void + { + $this->loadFixtures([SubscriberListFixture::class]); + + $this->authenticatedJsonRequest('POST', '/api/v2/campaigns/999/resend', [], [], [], json_encode([ + 'list_ids' => [1], + ])); + $this->assertHttpNotFound(); + } +} diff --git a/tests/Integration/Messaging/Controller/CampaignControllerTest.php b/tests/Integration/Messaging/Controller/CampaignControllerTest.php index 4b71508..301dee3 100644 --- a/tests/Integration/Messaging/Controller/CampaignControllerTest.php +++ b/tests/Integration/Messaging/Controller/CampaignControllerTest.php @@ -9,7 +9,6 @@ use PhpList\RestBundle\Tests\Integration\Identity\Fixtures\AdministratorFixture; use PhpList\RestBundle\Tests\Integration\Identity\Fixtures\AdministratorTokenFixture; use PhpList\RestBundle\Tests\Integration\Messaging\Fixtures\MessageFixture; -use PhpList\RestBundle\Tests\Integration\Subscription\Fixtures\SubscriberListFixture; class CampaignControllerTest extends AbstractTestController { @@ -88,58 +87,4 @@ public function testDeleteCampaignReturnsNoContent(): void $this->authenticatedJsonRequest('DELETE', '/api/v2/campaigns/1'); $this->assertHttpNoContent(); } - public function testSendMessageWithoutSessionReturnsForbidden(): void - { - $this->loadFixtures([MessageFixture::class]); - self::getClient()->request('POST', '/api/v2/campaigns/1/send'); - $this->assertHttpForbidden(); - } - - public function testSendMessageWithValidSessionReturnsOkay(): void - { - $this->loadFixtures([AdministratorFixture::class, MessageFixture::class]); - - $this->authenticatedJsonRequest('POST', '/api/v2/campaigns/2/send'); - $this->assertHttpOkay(); - - $response = $this->getDecodedJsonResponseContent(); - self::assertSame(2, $response['id']); - } - - public function testSendMessageWithInvalidIdReturnsNotFound(): void - { - $this->authenticatedJsonRequest('POST', '/api/v2/campaigns/999/send'); - $this->assertHttpNotFound(); - } - - public function testResendMessageToListsWithoutSessionReturnsForbidden(): void - { - $this->loadFixtures([MessageFixture::class, SubscriberListFixture::class]); - - $this->jsonRequest('POST', '/api/v2/campaigns/2/resend', [], [], [], json_encode(['list_ids' => [1]])); - $this->assertHttpForbidden(); - } - - public function testResendMessageToListsWithValidSessionReturnsOkay(): void - { - $this->loadFixtures([MessageFixture::class, SubscriberListFixture::class]); - - $this->authenticatedJsonRequest('POST', '/api/v2/campaigns/2/resend', [], [], [], json_encode([ - 'list_ids' => [1], - ])); - $this->assertHttpOkay(); - - $response = $this->getDecodedJsonResponseContent(); - self::assertSame(2, $response['id']); - } - - public function testResendMessageToListsWithInvalidIdReturnsNotFound(): void - { - $this->loadFixtures([SubscriberListFixture::class]); - - $this->authenticatedJsonRequest('POST', '/api/v2/campaigns/999/resend', [], [], [], json_encode([ - 'list_ids' => [1], - ])); - $this->assertHttpNotFound(); - } }