diff --git a/composer.json b/composer.json index 1d4c9f0..890aafb 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ }, "require": { "php": "^8.1", - "phplist/core": "v5.0.0-alpha6", + "phplist/core": "v5.0.0-alpha7", "friendsofsymfony/rest-bundle": "*", "symfony/test-pack": "^1.0", "symfony/process": "^6.4", @@ -97,8 +97,18 @@ "PhpList\\RestBundle\\PhpListRestBundle" ], "routes": { - "rest-api": { - "resource": "@PhpListRestBundle/Controller/", + "rest-api-identity": { + "resource": "@PhpListRestBundle/Identity/Controller/", + "type": "attribute", + "prefix": "/api/v2" + }, + "rest-api-subscription": { + "resource": "@PhpListRestBundle/Subscription/Controller/", + "type": "attribute", + "prefix": "/api/v2" + }, + "rest-api-messaging": { + "resource": "@PhpListRestBundle/Messaging/Controller/", "type": "attribute", "prefix": "/api/v2" } diff --git a/config/services.yml b/config/services.yml index 4d668a3..086d199 100644 --- a/config/services.yml +++ b/config/services.yml @@ -4,12 +4,6 @@ services: Psr\Container\ContainerInterface: alias: 'service_container' - PhpList\RestBundle\Controller\: - resource: '../src/Controller' - public: true - autowire: true - tags: ['controller.service_arguments'] - my.secure_handler: class: PhpList\RestBundle\ViewHandler\SecuredViewHandler diff --git a/config/services/builders.yml b/config/services/builders.yml index e37a342..10a994a 100644 --- a/config/services/builders.yml +++ b/config/services/builders.yml @@ -4,22 +4,22 @@ services: autoconfigure: true public: false - PhpList\RestBundle\Service\Builder\MessageBuilder: + PhpList\Core\Domain\Messaging\Service\Builder\MessageBuilder: autowire: true autoconfigure: true - PhpList\RestBundle\Service\Builder\MessageFormatBuilder: + PhpList\Core\Domain\Messaging\Service\Builder\MessageFormatBuilder: autowire: true autoconfigure: true - PhpList\RestBundle\Service\Builder\MessageScheduleBuilder: + PhpList\Core\Domain\Messaging\Service\Builder\MessageScheduleBuilder: autowire: true autoconfigure: true - PhpList\RestBundle\Service\Builder\MessageContentBuilder: + PhpList\Core\Domain\Messaging\Service\Builder\MessageContentBuilder: autowire: true autoconfigure: true - PhpList\RestBundle\Service\Builder\MessageOptionsBuilder: + PhpList\Core\Domain\Messaging\Service\Builder\MessageOptionsBuilder: autowire: true autoconfigure: true diff --git a/config/services/controllers.yml b/config/services/controllers.yml new file mode 100644 index 0000000..857146f --- /dev/null +++ b/config/services/controllers.yml @@ -0,0 +1,26 @@ +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + PhpList\RestBundle\Identity\Controller\: + resource: '../src/Identity/Controller' + tags: [ 'controller.service_arguments' ] + autowire: true + autoconfigure: true + public: true + + PhpList\RestBundle\Messaging\Controller\: + resource: '../src/Messaging/Controller' + tags: [ 'controller.service_arguments' ] + autowire: true + autoconfigure: true + public: true + + PhpList\RestBundle\Subscription\Controller\: + resource: '../src/Subscription/Controller' + tags: [ 'controller.service_arguments' ] + autowire: true + autoconfigure: true + public: true diff --git a/config/services/factories.yml b/config/services/factories.yml index 3a8734d..459a86f 100644 --- a/config/services/factories.yml +++ b/config/services/factories.yml @@ -4,6 +4,6 @@ services: autoconfigure: true public: false - PhpList\RestBundle\Service\Factory\PaginationCursorRequestFactory: + PhpList\RestBundle\Common\Service\Factory\PaginationCursorRequestFactory: autowire: true autoconfigure: true diff --git a/config/services/listeners.yml b/config/services/listeners.yml index 6257282..be859c9 100644 --- a/config/services/listeners.yml +++ b/config/services/listeners.yml @@ -4,10 +4,10 @@ services: autoconfigure: true public: false - PhpList\RestBundle\EventListener\ExceptionListener: + PhpList\RestBundle\Common\EventListener\ExceptionListener: tags: - { name: kernel.event_listener, event: kernel.exception } - PhpList\RestBundle\EventListener\ResponseListener: + PhpList\RestBundle\Common\EventListener\ResponseListener: tags: - { name: kernel.event_listener, event: kernel.response } diff --git a/config/services/managers.yml b/config/services/managers.yml index 7f42416..eeb4958 100644 --- a/config/services/managers.yml +++ b/config/services/managers.yml @@ -4,34 +4,38 @@ services: autoconfigure: true public: false - PhpList\RestBundle\Service\Manager\SubscriberManager: + PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager: autowire: true autoconfigure: true - PhpList\RestBundle\Service\Manager\SessionManager: + PhpList\Core\Domain\Identity\Service\SessionManager: autowire: true autoconfigure: true - PhpList\RestBundle\Service\Manager\SubscriberListManager: + PhpList\Core\Domain\Subscription\Service\Manager\SubscriberListManager: autowire: true autoconfigure: true - PhpList\RestBundle\Service\Manager\SubscriptionManager: + PhpList\Core\Domain\Subscription\Service\Manager\SubscriptionManager: autowire: true autoconfigure: true - PhpList\RestBundle\Service\Manager\MessageManager: + PhpList\Core\Domain\Messaging\Service\MessageManager: autowire: true autoconfigure: true - PhpList\RestBundle\Service\Manager\TemplateManager: + PhpList\Core\Domain\Messaging\Service\TemplateManager: autowire: true autoconfigure: true - PhpList\RestBundle\Service\Manager\TemplateImageManager: + PhpList\Core\Domain\Messaging\Service\TemplateImageManager: autowire: true autoconfigure: true - PhpList\RestBundle\Service\Manager\AdministratorManager: + PhpList\Core\Domain\Identity\Service\AdministratorManager: + autowire: true + autoconfigure: true + + PhpList\Core\Domain\Subscription\Service\SubscriberCsvExporter: autowire: true autoconfigure: true diff --git a/config/services/normalizers.yml b/config/services/normalizers.yml index ab3d3d7..0420706 100644 --- a/config/services/normalizers.yml +++ b/config/services/normalizers.yml @@ -11,37 +11,61 @@ services: $classMetadataFactory: '@?serializer.mapping.class_metadata_factory' $nameConverter: '@Symfony\Component\Serializer\NameConverter\CamelCaseToSnakeCaseNameConverter' - PhpList\RestBundle\Serializer\SubscriberNormalizer: + PhpList\RestBundle\Subscription\Serializer\SubscriberNormalizer: tags: [ 'serializer.normalizer' ] autowire: true - PhpList\RestBundle\Serializer\AdministratorTokenNormalizer: + PhpList\RestBundle\Subscription\Serializer\SubscriberOnlyNormalizer: tags: [ 'serializer.normalizer' ] autowire: true - PhpList\RestBundle\Serializer\SubscriberListNormalizer: + PhpList\RestBundle\Identity\Serializer\AdministratorTokenNormalizer: tags: [ 'serializer.normalizer' ] autowire: true - PhpList\RestBundle\Serializer\SubscriptionNormalizer: + PhpList\RestBundle\Subscription\Serializer\SubscriberListNormalizer: tags: [ 'serializer.normalizer' ] autowire: true - PhpList\RestBundle\Serializer\MessageNormalizer: + PhpList\RestBundle\Subscription\Serializer\SubscriptionNormalizer: tags: [ 'serializer.normalizer' ] autowire: true - PhpList\RestBundle\Serializer\TemplateImageNormalizer: + PhpList\RestBundle\Messaging\Serializer\MessageNormalizer: tags: [ 'serializer.normalizer' ] autowire: true - PhpList\RestBundle\Serializer\TemplateNormalizer: + PhpList\RestBundle\Messaging\Serializer\TemplateImageNormalizer: tags: [ 'serializer.normalizer' ] autowire: true - PhpList\RestBundle\Serializer\AdministratorNormalizer: + PhpList\RestBundle\Messaging\Serializer\TemplateNormalizer: tags: [ 'serializer.normalizer' ] autowire: true - PhpList\RestBundle\Serializer\CursorPaginationNormalizer: + PhpList\RestBundle\Identity\Serializer\AdministratorNormalizer: + tags: [ 'serializer.normalizer' ] + autowire: true + + PhpList\RestBundle\Identity\Serializer\AdminAttributeDefinitionNormalizer: + tags: [ 'serializer.normalizer' ] + autowire: true + + PhpList\RestBundle\Identity\Serializer\AdminAttributeValueNormalizer: + tags: [ 'serializer.normalizer' ] + autowire: true + + PhpList\RestBundle\Subscription\Serializer\AttributeDefinitionNormalizer: + tags: [ 'serializer.normalizer' ] + autowire: true + + PhpList\RestBundle\Subscription\Serializer\SubscriberAttributeValueNormalizer: + tags: [ 'serializer.normalizer' ] + autowire: true + + PhpList\RestBundle\Common\Serializer\CursorPaginationNormalizer: + autowire: true + + PhpList\RestBundle\Subscription\Serializer\SubscribersExportRequestNormalizer: + tags: [ 'serializer.normalizer' ] autowire: true diff --git a/config/services/providers.yml b/config/services/providers.yml index 49c7ff7..be69707 100644 --- a/config/services/providers.yml +++ b/config/services/providers.yml @@ -4,6 +4,6 @@ services: autoconfigure: true public: false - PhpList\RestBundle\Service\Provider\PaginatedDataProvider: + PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider: autowire: true autoconfigure: true diff --git a/config/services/validators.yml b/config/services/validators.yml index d1d519c..0080f7a 100644 --- a/config/services/validators.yml +++ b/config/services/validators.yml @@ -1,36 +1,38 @@ services: - PhpList\RestBundle\Validator\RequestValidator: + PhpList\RestBundle\Common\Validator\RequestValidator: autowire: true autoconfigure: true public: false - PhpList\RestBundle\Validator\Constraint\UniqueEmailValidator: + PhpList\RestBundle\Identity\Validator\Constraint\UniqueEmailValidator: autowire: true autoconfigure: true tags: [ 'validator.constraint_validator' ] - PhpList\RestBundle\Validator\Constraint\EmailExistsValidator: + PhpList\RestBundle\Subscription\Validator\Constraint\UniqueEmailValidator: autowire: true autoconfigure: true tags: [ 'validator.constraint_validator' ] - PhpList\RestBundle\Validator\Constraint\TemplateExistsValidator: + PhpList\RestBundle\Subscription\Validator\Constraint\EmailExistsValidator: autowire: true autoconfigure: true tags: [ 'validator.constraint_validator' ] - PhpList\RestBundle\Validator\TemplateLinkValidator: + PhpList\RestBundle\Messaging\Validator\Constraint\TemplateExistsValidator: autowire: true autoconfigure: true + tags: [ 'validator.constraint_validator' ] + + PhpList\RestBundle\Messaging\Validator\Constraint\ContainsPlaceholderValidator: + tags: ['validator.constraint_validator'] - PhpList\RestBundle\Validator\TemplateImageValidator: + PhpList\RestBundle\Identity\Validator\Constraint\UniqueLoginNameValidator: autowire: true autoconfigure: true + tags: [ 'validator.constraint_validator' ] - PhpList\RestBundle\Validator\Constraint\ContainsPlaceholderValidator: - tags: ['validator.constraint_validator'] - - PhpList\RestBundle\Validator\Constraint\UniqueLoginNameValidator: + PhpList\RestBundle\Subscription\Validator\Constraint\ListExistsValidator: autowire: true autoconfigure: true tags: [ 'validator.constraint_validator' ] diff --git a/src/Controller/BaseController.php b/src/Common/Controller/BaseController.php similarity index 86% rename from src/Controller/BaseController.php rename to src/Common/Controller/BaseController.php index 6b84672..32bed0a 100644 --- a/src/Controller/BaseController.php +++ b/src/Common/Controller/BaseController.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Controller; +namespace PhpList\RestBundle\Common\Controller; -use PhpList\Core\Domain\Model\Identity\Administrator; +use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\Security\Authentication; -use PhpList\RestBundle\Validator\RequestValidator; +use PhpList\RestBundle\Common\Validator\RequestValidator; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; diff --git a/src/Entity/Dto/CursorPaginationResult.php b/src/Common/Dto/CursorPaginationResult.php similarity index 84% rename from src/Entity/Dto/CursorPaginationResult.php rename to src/Common/Dto/CursorPaginationResult.php index 0302f28..04fca6d 100644 --- a/src/Entity/Dto/CursorPaginationResult.php +++ b/src/Common/Dto/CursorPaginationResult.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Entity\Dto; +namespace PhpList\RestBundle\Common\Dto; class CursorPaginationResult { diff --git a/src/Entity/Dto/ValidationContext.php b/src/Common/Dto/ValidationContext.php similarity index 91% rename from src/Entity/Dto/ValidationContext.php rename to src/Common/Dto/ValidationContext.php index 225e0d9..162dd1c 100644 --- a/src/Entity/Dto/ValidationContext.php +++ b/src/Common/Dto/ValidationContext.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Entity\Dto; +namespace PhpList\RestBundle\Common\Dto; class ValidationContext { diff --git a/src/EventListener/ExceptionListener.php b/src/Common/EventListener/ExceptionListener.php similarity index 91% rename from src/EventListener/ExceptionListener.php rename to src/Common/EventListener/ExceptionListener.php index 1dc4662..f48bf86 100644 --- a/src/EventListener/ExceptionListener.php +++ b/src/Common/EventListener/ExceptionListener.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\RestBundle\EventListener; +namespace PhpList\RestBundle\Common\EventListener; use Exception; -use PhpList\RestBundle\Exception\SubscriptionCreationException; +use PhpList\Core\Domain\Subscription\Exception\SubscriptionCreationException; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpKernel\Event\ExceptionEvent; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; diff --git a/src/EventListener/ResponseListener.php b/src/Common/EventListener/ResponseListener.php similarity index 91% rename from src/EventListener/ResponseListener.php rename to src/Common/EventListener/ResponseListener.php index 2bfb915..b960ebd 100644 --- a/src/EventListener/ResponseListener.php +++ b/src/Common/EventListener/ResponseListener.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\RestBundle\EventListener; +namespace PhpList\RestBundle\Common\EventListener; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpKernel\Event\ResponseEvent; diff --git a/src/Entity/Request/PaginationCursorRequest.php b/src/Common/Request/PaginationCursorRequest.php similarity index 92% rename from src/Entity/Request/PaginationCursorRequest.php rename to src/Common/Request/PaginationCursorRequest.php index bf82b4e..ef514e1 100644 --- a/src/Entity/Request/PaginationCursorRequest.php +++ b/src/Common/Request/PaginationCursorRequest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Entity\Request; +namespace PhpList\RestBundle\Common\Request; use Symfony\Component\HttpFoundation\Request; diff --git a/src/Common/Request/RequestInterface.php b/src/Common/Request/RequestInterface.php new file mode 100644 index 0000000..ce20c02 --- /dev/null +++ b/src/Common/Request/RequestInterface.php @@ -0,0 +1,10 @@ +user; - } - - public function getExisting(): ?Message - { - return $this->existing; - } -} diff --git a/src/Entity/Request/CreateAdministratorRequest.php b/src/Entity/Request/CreateAdministratorRequest.php deleted file mode 100644 index 71bf9d6..0000000 --- a/src/Entity/Request/CreateAdministratorRequest.php +++ /dev/null @@ -1,30 +0,0 @@ -statusCode = $statusCode; - } - - public function getStatusCode(): int - { - return $this->statusCode; - } -} diff --git a/src/Identity/Controller/AdminAttributeDefinitionController.php b/src/Identity/Controller/AdminAttributeDefinitionController.php new file mode 100644 index 0000000..16dcdd3 --- /dev/null +++ b/src/Identity/Controller/AdminAttributeDefinitionController.php @@ -0,0 +1,351 @@ +definitionManager = $definitionManager; + $this->normalizer = $normalizer; + $this->paginatedDataProvider = $paginatedDataProvider; + } + + #[Route('', name: 'create', methods: ['POST'])] + #[OA\Post( + path: '/administrators/attributes', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns created admin attribute definition.', + summary: 'Create an admin attribute definition.', + requestBody: new OA\RequestBody( + description: 'Pass parameters to create admin attribute.', + required: true, + content: new OA\JsonContent(ref: '#/components/schemas/CreateAdminAttributeDefinitionRequest') + ), + tags: ['admin-attributes'], + parameters: [ + new OA\Parameter( + name: 'session', + description: 'Session ID obtained from authentication', + 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/AdminAttributeDefinition') + ), + 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 create(Request $request): JsonResponse + { + $this->requireAuthentication($request); + + /** @var CreateAttributeDefinitionRequest $definitionRequest */ + $definitionRequest = $this->validator->validate($request, CreateAttributeDefinitionRequest::class); + + $attributeDefinition = $this->definitionManager->create($definitionRequest->getDto()); + $json = $this->normalizer->normalize($attributeDefinition, 'json'); + + return $this->json($json, Response::HTTP_CREATED); + } + + #[Route('/{definitionId}', name: 'update', requirements: ['definitionId' => '\d+'], methods: ['PUT'])] + #[OA\Put( + path: '/administrators/attributes/{definitionId}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns updated admin attribute definition.', + summary: 'Update an admin attribute definition.', + requestBody: new OA\RequestBody( + description: 'Pass parameters to update admin attribute.', + required: true, + content: new OA\JsonContent(ref: '#/components/schemas/CreateAdminAttributeDefinitionRequest') + ), + tags: ['admin-attributes'], + parameters: [ + new OA\Parameter( + name: 'definitionId', + description: 'Definition ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/AdminAttributeDefinition') + ), + 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 update( + Request $request, + #[MapEntity(mapping: ['definitionId' => 'id'])] ?AdminAttributeDefinition $attributeDefinition, + ): JsonResponse { + $this->requireAuthentication($request); + if (!$attributeDefinition) { + throw $this->createNotFoundException('Attribute definition not found.'); + } + + /** @var UpdateAttributeDefinitionRequest $definitionRequest */ + $definitionRequest = $this->validator->validate($request, UpdateAttributeDefinitionRequest::class); + + $attributeDefinition = $this->definitionManager->update( + attributeDefinition: $attributeDefinition, + attributeDefinitionDto: $definitionRequest->getDto(), + ); + $json = $this->normalizer->normalize($attributeDefinition, 'json'); + + return $this->json($json, Response::HTTP_OK); + } + + #[Route('/{definitionId}', name: 'delete', requirements: ['definitionId' => '\d+'], methods: ['DELETE'])] + #[OA\Delete( + path: '/administrators/attributes/{definitionId}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Deletes a single admin attribute definition.', + summary: 'Deletes an attribute definition.', + tags: ['admin-attributes'], + parameters: [ + new OA\Parameter( + name: 'session', + description: 'Session ID', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'definitionId', + description: 'Definition ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + 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, + #[MapEntity(mapping: ['definitionId' => 'id'])] ?AdminAttributeDefinition $attributeDefinition, + ): JsonResponse { + $this->requireAuthentication($request); + if (!$attributeDefinition) { + throw $this->createNotFoundException('Attribute definition not found.'); + } + + $this->definitionManager->delete($attributeDefinition); + + return $this->json(null, Response::HTTP_NO_CONTENT); + } + + #[Route('', name: 'get_lists', methods: ['GET'])] + #[OA\Get( + path: '/administrators/attributes', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a JSON list of all admin attribute definitions.', + summary: 'Gets a list of all admin attribute definitions.', + tags: ['admin-attributes'], + parameters: [ + new OA\Parameter( + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', + required: true, + schema: new OA\Schema( + type: 'string' + ) + ), + new OA\Parameter( + name: 'after_id', + description: 'Last id (starting from 0)', + in: 'query', + required: false, + schema: new OA\Schema(type: 'integer', default: 1, minimum: 1) + ), + new OA\Parameter( + name: 'limit', + description: 'Number of results per page', + in: 'query', + required: false, + schema: new OA\Schema(type: 'integer', default: 25, maximum: 100, minimum: 1) + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: 'items', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/AdminAttributeDefinition') + ), + new OA\Property(property: 'pagination', ref: '#/components/schemas/CursorPagination') + ], + type: 'object' + ) + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ) + ] + )] + public function getPaginated(Request $request): JsonResponse + { + $this->requireAuthentication($request); + + return $this->json( + $this->paginatedDataProvider->getPaginatedList( + $request, + $this->normalizer, + AdminAttributeDefinition::class, + ), + Response::HTTP_OK + ); + } + + #[Route('/{definitionId}', name: 'get_one', requirements: ['definitionId' => '\d+'], methods: ['GET'])] + #[OA\Get( + path: '/administrators/attributes/{definitionId}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a single attribute with specified ID.', + summary: 'Gets attribute with specified ID.', + tags: ['admin-attributes'], + parameters: [ + new OA\Parameter( + name: 'definitionId', + description: 'Definition ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/AdminAttributeDefinition') + ), + 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( + properties: [ + new OA\Property( + property: 'message', + type: 'string', + example: 'There is no attribute with that ID.' + ) + ], + type: 'object' + ) + ) + ] + )] + public function getAttributeDefinition( + Request $request, + #[MapEntity(mapping: ['definitionId' => 'id'])] ?AdminAttributeDefinition $attributeDefinition, + ): JsonResponse { + $this->requireAuthentication($request); + if (!$attributeDefinition) { + throw $this->createNotFoundException('Attribute definition not found.'); + } + + return $this->json( + $this->normalizer->normalize($attributeDefinition), + Response::HTTP_OK + ); + } +} diff --git a/src/Identity/Controller/AdminAttributeValueController.php b/src/Identity/Controller/AdminAttributeValueController.php new file mode 100644 index 0000000..5a2f76d --- /dev/null +++ b/src/Identity/Controller/AdminAttributeValueController.php @@ -0,0 +1,359 @@ +attributeManager = $attributeManager; + $this->normalizer = $normalizer; + $this->paginatedDataProvider = $paginatedDataProvider; + } + + #[Route( + path: '/{adminId}/{definitionId}', + name: 'create', + requirements: ['adminId' => '\d+', 'definitionId' => '\d+'], + methods: ['POST', 'PUT'], + )] + #[OA\Post( + path: '/administrators/attribute-values/{adminId}/{definitionId}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns created/updated admin attribute.', + summary: 'Create/update an admin attribute.', + requestBody: new OA\RequestBody( + description: 'Pass parameters to create admin attribute.', + required: true, + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'value', type: 'string', example: 'United States'), + ] + ) + ), + tags: ['admin-attributes'], + parameters: [ + new OA\Parameter( + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'definitionId', + description: 'attribute definition id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + new OA\Parameter( + name: 'adminId', + description: 'Administrator id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + ], + responses: [ + new OA\Response( + response: 201, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/AdminAttributeValue') + ), + 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, + #[MapEntity(mapping: ['definitionId' => 'id'])] ?AdminAttributeDefinition $definition = null, + #[MapEntity(mapping: ['adminId' => 'id'])] ?Administrator $admin = null, + ): JsonResponse { + $this->requireAuthentication($request); + + if (!$definition) { + throw $this->createNotFoundException('Attribute definition not found.'); + } + if (!$admin) { + throw $this->createNotFoundException('Administrator not found.'); + } + + $attributeDefinition = $this->attributeManager->createOrUpdate( + admin:$admin, + definition: $definition, + value: $request->toArray()['value'] ?? null + ); + $json = $this->normalizer->normalize($attributeDefinition, 'json'); + + return $this->json($json, Response::HTTP_CREATED); + } + + #[Route( + path: '/{adminId}/{definitionId}', + name: 'delete', + requirements: ['adminId' => '\d+', 'definitionId' => '\d+'], + methods: ['DELETE'], + )] + #[OA\Delete( + path: '/administrators/attribute-values/{adminId}/{definitionId}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Deletes a single admin attribute.', + summary: 'Deletes an attribute.', + tags: ['admin-attributes'], + parameters: [ + new OA\Parameter( + name: 'session', + description: 'Session ID', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'definitionId', + description: 'attribute definition id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + new OA\Parameter( + name: 'adminId', + description: 'Administrator id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + ], + responses: [ + new OA\Response( + response: 200, + 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, + #[MapEntity(mapping: ['definitionId' => 'id'])] ?AdminAttributeDefinition $definition = null, + #[MapEntity(mapping: ['adminId' => 'id'])] ?Administrator $admin = null, + ): JsonResponse { + $this->requireAuthentication($request); + if (!$definition || !$admin) { + throw $this->createNotFoundException('Administrator attribute not found.'); + } + $attribute = $this->attributeManager->getAdminAttribute($admin->getId(), $definition->getId()); + if ($attribute === null) { + throw $this->createNotFoundException('Administrator attribute not found.'); + } + $this->attributeManager->delete($attribute); + + return $this->json(null, Response::HTTP_NO_CONTENT); + } + + #[Route('/{adminId}', name: 'get__list', requirements: ['adminId' => '\d+'], methods: ['GET'])] + #[OA\Get( + path: '/administrators/attribute-values/{adminId}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a JSON list of all admin attributes.', + summary: 'Gets a list of all admin attributes.', + tags: ['admin-attributes'], + parameters: [ + new OA\Parameter( + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'adminId', + description: 'Administrator id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + new OA\Parameter( + name: 'after_id', + description: 'Last id (starting from 0)', + in: 'query', + required: false, + schema: new OA\Schema(type: 'integer', default: 1, minimum: 1) + ), + new OA\Parameter( + name: 'limit', + description: 'Number of results per page', + in: 'query', + required: false, + schema: new OA\Schema(type: 'integer', default: 25, maximum: 100, minimum: 1) + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: 'items', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/AdminAttributeValue') + ), + new OA\Property(property: 'pagination', ref: '#/components/schemas/CursorPagination') + ], + type: 'object' + ) + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ) + ] + )] + public function getPaginated( + Request $request, + #[MapEntity(mapping: ['adminId' => 'id'])] ?Administrator $admin = null, + ): JsonResponse { + $this->requireAuthentication($request); + if (!$admin) { + throw $this->createNotFoundException('Administrator not found.'); + } + + $filter = (new AdminAttributeValueFilter())->setAdminId($admin->getId()); + + return $this->json( + $this->paginatedDataProvider->getPaginatedList( + $request, + $this->normalizer, + AdminAttributeValue::class, + $filter + ), + Response::HTTP_OK + ); + } + + #[Route('/{adminId}/{definitionId}', name: 'get_one', methods: ['GET'])] + #[OA\Get( + path: '/administrators/attribute-values/{adminId}/{definitionId}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a single attribute.', + summary: 'Gets admin attribute.', + tags: ['admin-attributes'], + parameters: [ + new OA\Parameter( + name: 'definitionId', + description: 'attribute definition id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + new OA\Parameter( + name: 'adminId', + description: 'Administrator id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + new OA\Parameter( + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/AdminAttributeValue') + ), + 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( + properties: [ + new OA\Property( + property: 'message', + type: 'string', + example: 'There is no attribute with that ID.' + ) + ], + type: 'object' + ) + ) + ] + )] + public function getAttributeDefinition( + Request $request, + #[MapEntity(mapping: ['adminId' => 'id'])] ?AdminAttributeDefinition $admin, + #[MapEntity(mapping: ['definitionId' => 'id'])] ?AdminAttributeDefinition $definition, + ): JsonResponse { + $this->requireAuthentication($request); + if (!$definition || !$admin) { + throw $this->createNotFoundException('Administrator attribute not found.'); + } + $attribute = $this->attributeManager->getAdminAttribute( + adminId: $admin->getId(), + attributeDefinitionId: $definition->getId() + ); + $this->attributeManager->delete($attribute); + + return $this->json( + $this->normalizer->normalize($attribute), + Response::HTTP_OK + ); + } +} diff --git a/src/Controller/AdministratorController.php b/src/Identity/Controller/AdministratorController.php similarity index 78% rename from src/Controller/AdministratorController.php rename to src/Identity/Controller/AdministratorController.php index 1adb2be..ff79878 100644 --- a/src/Controller/AdministratorController.php +++ b/src/Identity/Controller/AdministratorController.php @@ -2,17 +2,18 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Controller; +namespace PhpList\RestBundle\Identity\Controller; use OpenApi\Attributes as OA; -use PhpList\Core\Domain\Model\Identity\Administrator; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Identity\Service\AdministratorManager; use PhpList\Core\Security\Authentication; -use PhpList\RestBundle\Entity\Request\CreateAdministratorRequest; -use PhpList\RestBundle\Entity\Request\UpdateAdministratorRequest; -use PhpList\RestBundle\Serializer\AdministratorNormalizer; -use PhpList\RestBundle\Service\Manager\AdministratorManager; -use PhpList\RestBundle\Service\Provider\PaginatedDataProvider; -use PhpList\RestBundle\Validator\RequestValidator; +use PhpList\RestBundle\Common\Controller\BaseController; +use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider; +use PhpList\RestBundle\Common\Validator\RequestValidator; +use PhpList\RestBundle\Identity\Request\CreateAdministratorRequest; +use PhpList\RestBundle\Identity\Request\UpdateAdministratorRequest; +use PhpList\RestBundle\Identity\Serializer\AdministratorNormalizer; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -22,7 +23,7 @@ /** * This controller provides CRUD operations for Administrator entities. */ -#[Route('/administrators')] +#[Route('/administrators', name: 'admin_')] class AdministratorController extends BaseController { private AdministratorManager $administratorManager; @@ -42,10 +43,11 @@ public function __construct( $this->paginatedProvider = $paginatedProvider; } - #[Route('', name: 'get_administrators', methods: ['GET'])] + #[Route('', name: 'get_list', methods: ['GET'])] #[OA\Get( path: '/administrators', - description: 'Get list of administrators.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Get list of administrators.', summary: 'Get Administrators', tags: ['administrators'], parameters: [ @@ -105,10 +107,11 @@ public function getAdministrators(Request $request): JsonResponse ); } - #[Route('', name: 'create_administrator', methods: ['POST'])] + #[Route('', name: 'create', methods: ['POST'])] #[OA\Post( path: '/administrators', - description: 'Create a new administrator.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Create a new administrator.', summary: 'Create Administrator', requestBody: new OA\RequestBody( description: 'Administrator data', @@ -135,18 +138,20 @@ public function createAdministrator( ): JsonResponse { $this->requireAuthentication($request); - /** @var CreateAdministratorRequest $dto */ - $dto = $validator->validate($request, CreateAdministratorRequest::class); - $administrator = $this->administratorManager->createAdministrator($dto); + /** @var CreateAdministratorRequest $createRequest */ + $createRequest = $validator->validate($request, CreateAdministratorRequest::class); + + $administrator = $this->administratorManager->createAdministrator($createRequest->getDto()); $json = $normalizer->normalize($administrator, 'json'); return $this->json($json, Response::HTTP_CREATED); } - #[Route('/{administratorId}', name: 'get_administrator', methods: ['GET'])] + #[Route('/{administratorId}', name: 'get_one', requirements: ['administratorId' => '\d+'], methods: ['GET'])] #[OA\Get( path: '/administrators/{administratorId}', - description: 'Get administrator by ID.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Get administrator by ID.', summary: 'Get Administrator', tags: ['administrators'], parameters: [ @@ -184,10 +189,11 @@ public function getAdministrator( return $this->json($json, Response::HTTP_OK); } - #[Route('/{administratorId}', name: 'update_administrator', methods: ['PUT'])] + #[Route('/{administratorId}', name: 'update', requirements: ['administratorId' => '\d+'], methods: ['PUT'])] #[OA\Put( path: '/administrators/{administratorId}', - description: 'Update an administrator.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Update an administrator.', summary: 'Update Administrator', requestBody: new OA\RequestBody( description: 'Administrator update data', @@ -224,17 +230,18 @@ public function updateAdministrator( if (!$administrator) { throw $this->createNotFoundException('Administrator not found.'); } - /** @var UpdateAdministratorRequest $dto */ - $dto = $this->validator->validate($request, UpdateAdministratorRequest::class); - $this->administratorManager->updateAdministrator($administrator, $dto); + /** @var UpdateAdministratorRequest $updateRequest */ + $updateRequest = $this->validator->validate($request, UpdateAdministratorRequest::class); + $this->administratorManager->updateAdministrator($administrator, $updateRequest->getDto()); - return $this->json(null, Response::HTTP_OK); + return $this->json($this->normalizer->normalize($administrator), Response::HTTP_OK); } - #[Route('/{administratorId}', name: 'delete_administrator', methods: ['DELETE'])] + #[Route('/{administratorId}', name: 'delete', requirements: ['administratorId' => '\d+'], methods: ['DELETE'])] #[OA\Delete( path: '/administrators/{administratorId}', - description: 'Delete an administrator.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Delete an administrator.', summary: 'Delete Administrator', tags: ['administrators'], parameters: [ diff --git a/src/Controller/SessionController.php b/src/Identity/Controller/SessionController.php similarity index 81% rename from src/Controller/SessionController.php rename to src/Identity/Controller/SessionController.php index 9755d69..5de6ab7 100644 --- a/src/Controller/SessionController.php +++ b/src/Identity/Controller/SessionController.php @@ -2,15 +2,16 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Controller; +namespace PhpList\RestBundle\Identity\Controller; use OpenApi\Attributes as OA; -use PhpList\Core\Domain\Model\Identity\AdministratorToken; +use PhpList\Core\Domain\Identity\Model\AdministratorToken; +use PhpList\Core\Domain\Identity\Service\SessionManager; use PhpList\Core\Security\Authentication; -use PhpList\RestBundle\Entity\Request\CreateSessionRequest; -use PhpList\RestBundle\Serializer\AdministratorTokenNormalizer; -use PhpList\RestBundle\Service\Manager\SessionManager; -use PhpList\RestBundle\Validator\RequestValidator; +use PhpList\RestBundle\Common\Controller\BaseController; +use PhpList\RestBundle\Common\Validator\RequestValidator; +use PhpList\RestBundle\Identity\Request\CreateSessionRequest; +use PhpList\RestBundle\Identity\Serializer\AdministratorTokenNormalizer; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -24,7 +25,7 @@ * @author Oliver Klee * @author Tatevik Grigoryan */ -#[Route('/sessions')] +#[Route('/sessions', name: 'session_')] class SessionController extends BaseController { private SessionManager $sessionManager; @@ -39,10 +40,11 @@ public function __construct( $this->sessionManager = $sessionManager; } - #[Route('', name: 'create_session', methods: ['POST'])] + #[Route('', name: 'create', methods: ['POST'])] #[OA\Post( path: '/sessions', - description: 'Given valid login data, this will generate a login token that will be valid for 1 hour.', + description: '✅ **Status: Stable** – This method is stable and safe for production use. ' . + 'Given valid login data, this will generate a login token that will be valid for 1 hour.', summary: 'Log in or create new session.', requestBody: new OA\RequestBody( description: 'Pass session credentials', @@ -90,7 +92,10 @@ public function createSession( ): JsonResponse { /** @var CreateSessionRequest $createSessionRequest */ $createSessionRequest = $this->validator->validate($request, CreateSessionRequest::class); - $token = $this->sessionManager->createSession($createSessionRequest); + $token = $this->sessionManager->createSession( + loginName:$createSessionRequest->loginName, + password: $createSessionRequest->password + ); $json = $normalizer->normalize($token, 'json'); @@ -104,10 +109,11 @@ public function createSession( * * @throws AccessDeniedHttpException */ - #[Route('/{sessionId}', name: 'delete_session', methods: ['DELETE'])] + #[Route('/{sessionId}', name: 'delete', requirements: ['sessionId' => '\d+'], methods: ['DELETE'])] #[OA\Delete( path: '/sessions/{sessionId}', - description: 'Delete the session passed as a parameter.', + description: '✅ **Status: Stable** – This method is stable and safe for production use. ' . + 'Delete the session passed as a parameter.', summary: 'Delete a session.', tags: ['sessions'], parameters: [ diff --git a/src/Identity/OpenApi/SwaggerSchemasRequest.php b/src/Identity/OpenApi/SwaggerSchemasRequest.php new file mode 100644 index 0000000..bc1f096 --- /dev/null +++ b/src/Identity/OpenApi/SwaggerSchemasRequest.php @@ -0,0 +1,89 @@ +loginName, + $this->password, + $this->email, + $this->superUser + ); + } +} diff --git a/src/Identity/Request/CreateAttributeDefinitionRequest.php b/src/Identity/Request/CreateAttributeDefinitionRequest.php new file mode 100644 index 0000000..794d086 --- /dev/null +++ b/src/Identity/Request/CreateAttributeDefinitionRequest.php @@ -0,0 +1,33 @@ +name, + type: $this->type, + listOrder: $this->order, + defaultValue: $this->defaultValue, + required: $this->required, + tableName: $this->tableName, + ); + } +} diff --git a/src/Entity/Request/CreateSessionRequest.php b/src/Identity/Request/CreateSessionRequest.php similarity index 63% rename from src/Entity/Request/CreateSessionRequest.php rename to src/Identity/Request/CreateSessionRequest.php index 9291358..fb61b09 100644 --- a/src/Entity/Request/CreateSessionRequest.php +++ b/src/Identity/Request/CreateSessionRequest.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Entity\Request; +namespace PhpList\RestBundle\Identity\Request; +use PhpList\RestBundle\Common\Request\RequestInterface; use Symfony\Component\Validator\Constraints as Assert; class CreateSessionRequest implements RequestInterface @@ -15,4 +16,9 @@ class CreateSessionRequest implements RequestInterface #[Assert\NotBlank] #[Assert\Type(type: 'string')] public string $password; + + public function getDto(): CreateSessionRequest + { + return $this; + } } diff --git a/src/Identity/Request/UpdateAdministratorRequest.php b/src/Identity/Request/UpdateAdministratorRequest.php new file mode 100644 index 0000000..ce3b8d3 --- /dev/null +++ b/src/Identity/Request/UpdateAdministratorRequest.php @@ -0,0 +1,42 @@ +administratorId, + loginName: $this->loginName, + password: $this->password, + email: $this->email, + superAdmin: $this->superAdmin, + ); + } +} diff --git a/src/Identity/Request/UpdateAttributeDefinitionRequest.php b/src/Identity/Request/UpdateAttributeDefinitionRequest.php new file mode 100644 index 0000000..7313a90 --- /dev/null +++ b/src/Identity/Request/UpdateAttributeDefinitionRequest.php @@ -0,0 +1,33 @@ +name, + type: $this->type, + listOrder: $this->order, + defaultValue: $this->defaultValue, + required: $this->required, + tableName: $this->tableName, + ); + } +} diff --git a/src/Identity/Serializer/AdminAttributeDefinitionNormalizer.php b/src/Identity/Serializer/AdminAttributeDefinitionNormalizer.php new file mode 100644 index 0000000..2c13171 --- /dev/null +++ b/src/Identity/Serializer/AdminAttributeDefinitionNormalizer.php @@ -0,0 +1,39 @@ + $object->getId(), + 'name' => $object->getName(), + 'type' => $object->getType(), + 'list_order' => $object->getListOrder(), + 'default_value' => $object->getDefaultValue(), + 'required' => $object->isRequired(), + 'table_name' => $object->getTableName(), + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function supportsNormalization($data, string $format = null): bool + { + return $data instanceof AdminAttributeDefinition; + } +} diff --git a/src/Identity/Serializer/AdminAttributeValueNormalizer.php b/src/Identity/Serializer/AdminAttributeValueNormalizer.php new file mode 100644 index 0000000..abdc4eb --- /dev/null +++ b/src/Identity/Serializer/AdminAttributeValueNormalizer.php @@ -0,0 +1,41 @@ + $this->adminNormalizer->normalize($object->getAdministrator()), + 'definition' => $this->definitionNormalizer->normalize($object->getAttributeDefinition()), + 'value' => $object->getValue() ?? $object->getAttributeDefinition()->getDefaultValue(), + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function supportsNormalization($data, string $format = null): bool + { + return $data instanceof AdminAttributeValue; + } +} diff --git a/src/Serializer/AdministratorNormalizer.php b/src/Identity/Serializer/AdministratorNormalizer.php similarity index 91% rename from src/Serializer/AdministratorNormalizer.php rename to src/Identity/Serializer/AdministratorNormalizer.php index 1eb5327..35f33ec 100644 --- a/src/Serializer/AdministratorNormalizer.php +++ b/src/Identity/Serializer/AdministratorNormalizer.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Serializer; +namespace PhpList\RestBundle\Identity\Serializer; use DateTimeInterface; use InvalidArgumentException; -use PhpList\Core\Domain\Model\Identity\Administrator; +use PhpList\Core\Domain\Identity\Model\Administrator; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; class AdministratorNormalizer implements NormalizerInterface diff --git a/src/Serializer/AdministratorTokenNormalizer.php b/src/Identity/Serializer/AdministratorTokenNormalizer.php similarity index 88% rename from src/Serializer/AdministratorTokenNormalizer.php rename to src/Identity/Serializer/AdministratorTokenNormalizer.php index 74a45da..7f6df4d 100644 --- a/src/Serializer/AdministratorTokenNormalizer.php +++ b/src/Identity/Serializer/AdministratorTokenNormalizer.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Serializer; +namespace PhpList\RestBundle\Identity\Serializer; -use PhpList\Core\Domain\Model\Identity\AdministratorToken; +use PhpList\Core\Domain\Identity\Model\AdministratorToken; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; class AdministratorTokenNormalizer implements NormalizerInterface diff --git a/src/Validator/Constraint/UniqueEmail.php b/src/Identity/Validator/Constraint/UniqueEmail.php similarity index 90% rename from src/Validator/Constraint/UniqueEmail.php rename to src/Identity/Validator/Constraint/UniqueEmail.php index 72152bc..4a585bb 100644 --- a/src/Validator/Constraint/UniqueEmail.php +++ b/src/Identity/Validator/Constraint/UniqueEmail.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Validator\Constraint; +namespace PhpList\RestBundle\Identity\Validator\Constraint; use Symfony\Component\Validator\Constraint; diff --git a/src/Identity/Validator/Constraint/UniqueEmailValidator.php b/src/Identity/Validator/Constraint/UniqueEmailValidator.php new file mode 100644 index 0000000..404ce13 --- /dev/null +++ b/src/Identity/Validator/Constraint/UniqueEmailValidator.php @@ -0,0 +1,43 @@ +repository->findOneBy(['email' => $value]); + + $dto = $this->context->getObject(); + $updatingId = $dto->administratorId ?? null; + + if ($existingUser && $existingUser->getId() !== $updatingId) { + throw new ConflictHttpException('Email already exists.'); + } + } +} diff --git a/src/Validator/Constraint/UniqueLoginName.php b/src/Identity/Validator/Constraint/UniqueLoginName.php similarity index 91% rename from src/Validator/Constraint/UniqueLoginName.php rename to src/Identity/Validator/Constraint/UniqueLoginName.php index 83c52a7..8c895e9 100644 --- a/src/Validator/Constraint/UniqueLoginName.php +++ b/src/Identity/Validator/Constraint/UniqueLoginName.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Validator\Constraint; +namespace PhpList\RestBundle\Identity\Validator\Constraint; use Symfony\Component\Validator\Constraint; diff --git a/src/Validator/Constraint/UniqueLoginNameValidator.php b/src/Identity/Validator/Constraint/UniqueLoginNameValidator.php similarity index 91% rename from src/Validator/Constraint/UniqueLoginNameValidator.php rename to src/Identity/Validator/Constraint/UniqueLoginNameValidator.php index ded32c2..ba8727e 100644 --- a/src/Validator/Constraint/UniqueLoginNameValidator.php +++ b/src/Identity/Validator/Constraint/UniqueLoginNameValidator.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Validator\Constraint; +namespace PhpList\RestBundle\Identity\Validator\Constraint; -use PhpList\Core\Domain\Repository\Identity\AdministratorRepository; +use PhpList\Core\Domain\Identity\Repository\AdministratorRepository; use Symfony\Component\HttpKernel\Exception\ConflictHttpException; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; diff --git a/src/Controller/CampaignController.php b/src/Messaging/Controller/CampaignController.php similarity index 86% rename from src/Controller/CampaignController.php rename to src/Messaging/Controller/CampaignController.php index 73789bb..d4e74f9 100644 --- a/src/Controller/CampaignController.php +++ b/src/Messaging/Controller/CampaignController.php @@ -2,18 +2,19 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Controller; +namespace PhpList\RestBundle\Messaging\Controller; use OpenApi\Attributes as OA; -use PhpList\Core\Domain\Filter\MessageFilter; -use PhpList\Core\Domain\Model\Messaging\Message; +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\Security\Authentication; -use PhpList\RestBundle\Entity\Request\CreateMessageRequest; -use PhpList\RestBundle\Entity\Request\UpdateMessageRequest; -use PhpList\RestBundle\Serializer\MessageNormalizer; -use PhpList\RestBundle\Service\Manager\MessageManager; -use PhpList\RestBundle\Service\Provider\PaginatedDataProvider; -use PhpList\RestBundle\Validator\RequestValidator; +use PhpList\RestBundle\Common\Controller\BaseController; +use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider; +use PhpList\RestBundle\Common\Validator\RequestValidator; +use PhpList\RestBundle\Messaging\Request\CreateMessageRequest; +use PhpList\RestBundle\Messaging\Request\UpdateMessageRequest; +use PhpList\RestBundle\Messaging\Serializer\MessageNormalizer; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -25,7 +26,7 @@ * * @author Tatevik Grigoryan */ -#[Route('/campaigns')] +#[Route('/campaigns', name: 'campaign_')] class CampaignController extends BaseController { private MessageNormalizer $normalizer; @@ -45,10 +46,11 @@ public function __construct( $this->paginatedProvider = $paginatedProvider; } - #[Route('', name: 'get_campaigns', methods: ['GET'])] + #[Route('', name: 'get_list', methods: ['GET'])] #[OA\Get( path: '/campaigns', - description: 'Returns a JSON list of all campaigns/messages.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a JSON list of all campaigns/messages.', summary: 'Gets a list of all campaigns.', tags: ['campaigns'], parameters: [ @@ -111,10 +113,11 @@ public function getMessages(Request $request): JsonResponse ); } - #[Route('/{messageId}', name: 'get_campaign', methods: ['GET'])] + #[Route('/{messageId}', name: 'get_one', requirements: ['messageId' => '\d+'], methods: ['GET'])] #[OA\Get( path: '/campaigns/{messageId}', - description: 'Returns campaign/message by id.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns campaign/message by id.', summary: 'Gets a campaign by id.', tags: ['campaigns'], parameters: [ @@ -161,10 +164,11 @@ public function getMessage( return $this->json($this->normalizer->normalize($message), Response::HTTP_OK); } - #[Route('', name: 'create_message', methods: ['POST'])] + #[Route('', name: 'create', methods: ['POST'])] #[OA\Post( path: '/campaigns', - description: 'Returns created message.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns created message.', summary: 'Create a message for campaign.', requestBody: new OA\RequestBody( description: 'Create a new message.', @@ -218,15 +222,16 @@ public function createMessage(Request $request, MessageNormalizer $normalizer): /** @var CreateMessageRequest $createMessageRequest */ $createMessageRequest = $this->validator->validate($request, CreateMessageRequest::class); - $data = $this->messageManager->createMessage($createMessageRequest, $authUser); + $data = $this->messageManager->createMessage($createMessageRequest->getDto(), $authUser); return $this->json($normalizer->normalize($data), Response::HTTP_CREATED); } - #[Route('/{messageId}', name: 'update_campaign', methods: ['PUT'])] + #[Route('/{messageId}', name: 'update', requirements: ['messageId' => '\d+'], methods: ['PUT'])] #[OA\Put( path: '/campaigns/{messageId}', - description: 'Updates campaign/message by id.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Updates campaign/message by id.', summary: 'Update campaign by id.', requestBody: new OA\RequestBody( description: 'Update message.', @@ -291,15 +296,16 @@ public function updateMessage( } /** @var UpdateMessageRequest $updateMessageRequest */ $updateMessageRequest = $this->validator->validate($request, UpdateMessageRequest::class); - $data = $this->messageManager->updateMessage($updateMessageRequest, $message, $authUser); + $data = $this->messageManager->updateMessage($updateMessageRequest->getDto(), $message, $authUser); return $this->json($this->normalizer->normalize($data), Response::HTTP_OK); } - #[Route('/{messageId}', name: 'delete_campaign', methods: ['DELETE'])] + #[Route('/{messageId}', name: 'delete', requirements: ['messageId' => '\d+'], methods: ['DELETE'])] #[OA\Delete( path: '/campaigns/{messageId}', - description: 'Delete campaign/message by id.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Delete campaign/message by id.', summary: 'Delete campaign by id.', tags: ['campaigns'], parameters: [ diff --git a/src/Controller/TemplateController.php b/src/Messaging/Controller/TemplateController.php similarity index 87% rename from src/Controller/TemplateController.php rename to src/Messaging/Controller/TemplateController.php index 7914dd3..823b174 100644 --- a/src/Controller/TemplateController.php +++ b/src/Messaging/Controller/TemplateController.php @@ -2,16 +2,17 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Controller; +namespace PhpList\RestBundle\Messaging\Controller; use OpenApi\Attributes as OA; -use PhpList\Core\Domain\Model\Messaging\Template; +use PhpList\Core\Domain\Messaging\Model\Template; +use PhpList\Core\Domain\Messaging\Service\TemplateManager; use PhpList\Core\Security\Authentication; -use PhpList\RestBundle\Entity\Request\CreateTemplateRequest; -use PhpList\RestBundle\Serializer\TemplateNormalizer; -use PhpList\RestBundle\Service\Manager\TemplateManager; -use PhpList\RestBundle\Service\Provider\PaginatedDataProvider; -use PhpList\RestBundle\Validator\RequestValidator; +use PhpList\RestBundle\Common\Controller\BaseController; +use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider; +use PhpList\RestBundle\Common\Validator\RequestValidator; +use PhpList\RestBundle\Messaging\Request\CreateTemplateRequest; +use PhpList\RestBundle\Messaging\Serializer\TemplateNormalizer; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -23,7 +24,7 @@ * * @author Tatevik Grigoryan */ -#[Route('/templates')] +#[Route('/templates', name: 'template_')] class TemplateController extends BaseController { private TemplateNormalizer $normalizer; @@ -43,10 +44,11 @@ public function __construct( $this->paginatedDataProvider = $paginatedDataProvider; } - #[Route('', name: 'get_templates', methods: ['GET'])] + #[Route('', name: 'get_list', methods: ['GET'])] #[OA\Get( path: '/templates', - description: 'Returns a JSON list of all templates.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a JSON list of all templates.', summary: 'Gets a list of all templates.', tags: ['templates'], parameters: [ @@ -111,10 +113,11 @@ public function getTemplates(Request $request): JsonResponse ); } - #[Route('/{templateId}', name: 'get_template', methods: ['GET'])] + #[Route('/{templateId}', name: 'get_one', requirements: ['templateId' => '\d+'], methods: ['GET'])] #[OA\Get( path: '/templates/{templateId}', - description: 'Returns template by id.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns template by id.', summary: 'Gets a templateI by id.', tags: ['templates'], parameters: [ @@ -166,10 +169,11 @@ public function getTemplate( return $this->json($this->normalizer->normalize($template), Response::HTTP_OK); } - #[Route('', name: 'create_template', methods: ['POST'])] + #[Route('', name: 'create', methods: ['POST'])] #[OA\Post( path: '/templates', - description: 'Returns a JSON response of created template.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a JSON response of created template.', summary: 'Create a new template.', requestBody: new OA\RequestBody( description: 'Pass session credentials', @@ -264,15 +268,16 @@ public function createTemplates(Request $request): JsonResponse $createTemplateRequest = $this->validator->validate($request, CreateTemplateRequest::class); return $this->json( - $this->normalizer->normalize($this->templateManager->create($createTemplateRequest)), + $this->normalizer->normalize($this->templateManager->create($createTemplateRequest->getDto())), Response::HTTP_CREATED ); } - #[Route('/{templateId}', name: 'delete_template', methods: ['DELETE'])] + #[Route('/{templateId}', name: 'delete', requirements: ['templateId' => '\d+'], methods: ['DELETE'])] #[OA\Delete( path: '/templates/{templateId}', - description: 'Deletes template by id.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Deletes template by id.', summary: 'Deletes a template.', tags: ['templates'], parameters: [ diff --git a/src/OpenApi/SwaggerSchemasRequestDto.php b/src/Messaging/OpenApi/SwaggerSchemasRequest.php similarity index 61% rename from src/OpenApi/SwaggerSchemasRequestDto.php rename to src/Messaging/OpenApi/SwaggerSchemasRequest.php index 813a89d..8aef402 100644 --- a/src/OpenApi/SwaggerSchemasRequestDto.php +++ b/src/Messaging/OpenApi/SwaggerSchemasRequest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\RestBundle\OpenApi; +namespace PhpList\RestBundle\Messaging\OpenApi; use OpenApi\Attributes as OA; @@ -77,71 +77,6 @@ enum: ['html', 'text', 'invite'], ], type: 'object' )] -#[OA\Schema( - schema: 'CreateAdministratorRequest', - required: ['login_name', 'password', 'email', 'super_user'], - properties: [ - new OA\Property( - property: 'login_name', - type: 'string', - maxLength: 255, - minLength: 3, - example: 'admin' - ), - new OA\Property( - property: 'password', - type: 'string', - format: 'password', - maxLength: 255, - minLength: 6, - example: 'StrongP@ssw0rd' - ), - new OA\Property( - property: 'email', - type: 'string', - format: 'email', - example: 'admin@example.com' - ), - new OA\Property( - property: 'super_user', - type: 'boolean', - example: false - ), - ], - type: 'object' -)] -#[OA\Schema( - schema: 'UpdateAdministratorRequest', - properties: [ - new OA\Property( - property: 'login_name', - type: 'string', - maxLength: 255, - minLength: 3, - example: 'admin' - ), - new OA\Property( - property: 'password', - type: 'string', - format: 'password', - maxLength: 255, - minLength: 6, - example: 'StrongP@ssw0rd' - ), - new OA\Property( - property: 'email', - type: 'string', - format: 'email', - example: 'admin@example.com' - ), - new OA\Property( - property: 'super_user', - type: 'boolean', - example: false - ), - ], - type: 'object' -)] -class SwaggerSchemasRequestDto +class SwaggerSchemasRequest { } diff --git a/src/OpenApi/SwaggerSchemasResponseEntity.php b/src/Messaging/OpenApi/SwaggerSchemasResponse.php similarity index 63% rename from src/OpenApi/SwaggerSchemasResponseEntity.php rename to src/Messaging/OpenApi/SwaggerSchemasResponse.php index faabf0d..fe567c8 100644 --- a/src/OpenApi/SwaggerSchemasResponseEntity.php +++ b/src/Messaging/OpenApi/SwaggerSchemasResponse.php @@ -2,65 +2,10 @@ declare(strict_types=1); -namespace PhpList\RestBundle\OpenApi; +namespace PhpList\RestBundle\Messaging\OpenApi; use OpenApi\Attributes as OA; -#[OA\Schema( - schema: 'SubscriberList', - properties: [ - new OA\Property(property: 'id', type: 'integer', example: 2), - new OA\Property(property: 'name', type: 'string', example: 'Newsletter'), - new OA\Property(property: 'description', type: 'string', example: 'Monthly updates'), - new OA\Property( - property: 'created_at', - type: 'string', - format: 'date-time', - example: '2022-12-01T10:00:00Z' - ), - new OA\Property(property: 'public', type: 'boolean', example: true), - ], - type: 'object' -)] -#[OA\Schema( - schema: 'Subscriber', - properties: [ - new OA\Property(property: 'id', type: 'integer', example: 1), - new OA\Property(property: 'email', type: 'string', example: 'subscriber@example.com'), - new OA\Property( - property: 'created_at', - type: 'string', - format: 'date-time', - example: '2023-01-01T12:00:00Z', - ), - new OA\Property(property: 'confirmed', type: 'boolean', example: true), - new OA\Property(property: 'blacklisted', type: 'boolean', example: false), - new OA\Property(property: 'bounce_count', type: 'integer', example: 0), - new OA\Property(property: 'unique_id', type: 'string', example: '69f4e92cf50eafca9627f35704f030f4'), - new OA\Property(property: 'html_email', type: 'boolean', example: true), - new OA\Property(property: 'disabled', type: 'boolean', example: false), - new OA\Property( - property: 'subscribed_lists', - type: 'array', - items: new OA\Items(ref: '#/components/schemas/SubscriberList') - ), - ], - type: 'object' -)] -#[OA\Schema( - schema: 'Subscription', - properties: [ - new OA\Property(property: 'subscriber', ref: '#/components/schemas/Subscriber'), - new OA\Property(property: 'subscriber_list', ref: '#/components/schemas/SubscriberList'), - new OA\Property( - property: 'subscription_date', - type: 'string', - format: 'date-time', - example: '2023-01-01T12:00:00Z', - ), - ], - type: 'object' -)] #[OA\Schema( schema: 'TemplateImage', properties: [ @@ -174,40 +119,6 @@ ], type: 'object' )] -#[OA\Schema( - schema: 'Administrator', - properties: [ - new OA\Property( - property: 'id', - type: 'integer', - example: 1 - ), - new OA\Property( - property: 'login_name', - type: 'string', - example: 'admin' - ), - new OA\Property( - property: 'email', - type: 'string', - format: 'email', - example: 'admin@example.com' - ), - new OA\Property( - property: 'super_user', - type: 'boolean', - example: true - ), - new OA\Property( - property: 'created_at', - type: 'string', - format: 'date-time', - example: '2025-04-29T12:34:56+00:00' - ), - ], - type: 'object' -)] - -class SwaggerSchemasResponseEntity +class SwaggerSchemasResponse { } diff --git a/src/Messaging/Request/CreateMessageRequest.php b/src/Messaging/Request/CreateMessageRequest.php new file mode 100644 index 0000000..4d07968 --- /dev/null +++ b/src/Messaging/Request/CreateMessageRequest.php @@ -0,0 +1,54 @@ +content->getDto(), + format: $this->format->getDto(), + metadata: $this->metadata->getDto(), + options: $this->options->getDto(), + schedule: $this->schedule->getDto(), + templateId: $this->templateId, + ); + } +} diff --git a/src/Messaging/Request/CreateTemplateRequest.php b/src/Messaging/Request/CreateTemplateRequest.php new file mode 100644 index 0000000..c5bbbcb --- /dev/null +++ b/src/Messaging/Request/CreateTemplateRequest.php @@ -0,0 +1,42 @@ +title, + content: $this->content, + text: $this->text, + fileContent: $this->file instanceof UploadedFile ? file_get_contents($this->file->getPathname()) : null, + shouldCheckLinks: $this->checkLinks, + shouldCheckImages: $this->checkImages, + shouldCheckExternalImages: $this->checkExternalImages, + ); + } +} diff --git a/src/Messaging/Request/Message/MessageContentRequest.php b/src/Messaging/Request/Message/MessageContentRequest.php new file mode 100644 index 0000000..9363058 --- /dev/null +++ b/src/Messaging/Request/Message/MessageContentRequest.php @@ -0,0 +1,33 @@ +subject, + text: $this->text, + textMessage: $this->textMessage, + footer: $this->footer, + ); + } +} diff --git a/src/Entity/Request/Message/MessageFormatRequest.php b/src/Messaging/Request/Message/MessageFormatRequest.php similarity index 54% rename from src/Entity/Request/Message/MessageFormatRequest.php rename to src/Messaging/Request/Message/MessageFormatRequest.php index c46f954..3dd20d8 100644 --- a/src/Entity/Request/Message/MessageFormatRequest.php +++ b/src/Messaging/Request/Message/MessageFormatRequest.php @@ -2,8 +2,9 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Entity\Request\Message; +namespace PhpList\RestBundle\Messaging\Request\Message; +use PhpList\Core\Domain\Messaging\Model\Dto\Message\MessageFormatDto; use Symfony\Component\Validator\Constraints as Assert; class MessageFormatRequest implements RequestDtoInterface @@ -19,4 +20,13 @@ class MessageFormatRequest implements RequestDtoInterface new Assert\Choice(['text', 'html', 'pdf']), ])] public array $formatOptions; + + public function getDto(): MessageFormatDto + { + return new MessageFormatDto( + htmlFormated: $this->htmlFormated, + sendFormat: $this->sendFormat, + formatOptions: $this->formatOptions, + ); + } } diff --git a/src/Messaging/Request/Message/MessageMetadataRequest.php b/src/Messaging/Request/Message/MessageMetadataRequest.php new file mode 100644 index 0000000..ca908e6 --- /dev/null +++ b/src/Messaging/Request/Message/MessageMetadataRequest.php @@ -0,0 +1,21 @@ +status, + ); + } +} diff --git a/src/Messaging/Request/Message/MessageOptionsRequest.php b/src/Messaging/Request/Message/MessageOptionsRequest.php new file mode 100644 index 0000000..f2f3485 --- /dev/null +++ b/src/Messaging/Request/Message/MessageOptionsRequest.php @@ -0,0 +1,33 @@ +fromField, + toField: $this->toField, + replyTo: $this->replyTo, + userSelection: $this->userSelection, + ); + } +} diff --git a/src/Messaging/Request/Message/MessageScheduleRequest.php b/src/Messaging/Request/Message/MessageScheduleRequest.php new file mode 100644 index 0000000..ed6aa8e --- /dev/null +++ b/src/Messaging/Request/Message/MessageScheduleRequest.php @@ -0,0 +1,35 @@ +embargo, + repeatInterval: $this->repeatInterval, + repeatUntil: $this->repeatUntil, + requeueInterval: $this->requeueInterval, + requeueUntil: $this->requeueUntil, + ); + } +} diff --git a/src/Messaging/Request/Message/RequestDtoInterface.php b/src/Messaging/Request/Message/RequestDtoInterface.php new file mode 100644 index 0000000..e85a27e --- /dev/null +++ b/src/Messaging/Request/Message/RequestDtoInterface.php @@ -0,0 +1,10 @@ +messageId, + content: $this->content->getDto(), + format: $this->format->getDto(), + metadata: $this->metadata->getDto(), + options: $this->options->getDto(), + schedule: $this->schedule->getDto(), + templateId: $this->templateId, + ); + } +} diff --git a/src/Serializer/MessageNormalizer.php b/src/Messaging/Serializer/MessageNormalizer.php similarity index 96% rename from src/Serializer/MessageNormalizer.php rename to src/Messaging/Serializer/MessageNormalizer.php index dd3c5ba..b659b3d 100644 --- a/src/Serializer/MessageNormalizer.php +++ b/src/Messaging/Serializer/MessageNormalizer.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Serializer; +namespace PhpList\RestBundle\Messaging\Serializer; -use PhpList\Core\Domain\Model\Messaging\Message; +use PhpList\Core\Domain\Messaging\Model\Message; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; class MessageNormalizer implements NormalizerInterface diff --git a/src/Serializer/TemplateImageNormalizer.php b/src/Messaging/Serializer/TemplateImageNormalizer.php similarity index 90% rename from src/Serializer/TemplateImageNormalizer.php rename to src/Messaging/Serializer/TemplateImageNormalizer.php index 2a84bab..64ced9a 100644 --- a/src/Serializer/TemplateImageNormalizer.php +++ b/src/Messaging/Serializer/TemplateImageNormalizer.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Serializer; +namespace PhpList\RestBundle\Messaging\Serializer; -use PhpList\Core\Domain\Model\Messaging\TemplateImage; +use PhpList\Core\Domain\Messaging\Model\TemplateImage; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; class TemplateImageNormalizer implements NormalizerInterface diff --git a/src/Serializer/TemplateNormalizer.php b/src/Messaging/Serializer/TemplateNormalizer.php similarity index 88% rename from src/Serializer/TemplateNormalizer.php rename to src/Messaging/Serializer/TemplateNormalizer.php index bdf29e4..669f7a4 100644 --- a/src/Serializer/TemplateNormalizer.php +++ b/src/Messaging/Serializer/TemplateNormalizer.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Serializer; +namespace PhpList\RestBundle\Messaging\Serializer; -use PhpList\Core\Domain\Model\Messaging\Template; -use PhpList\Core\Domain\Model\Messaging\TemplateImage; +use PhpList\Core\Domain\Messaging\Model\Template; +use PhpList\Core\Domain\Messaging\Model\TemplateImage; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; class TemplateNormalizer implements NormalizerInterface diff --git a/src/Validator/Constraint/ContainsPlaceholder.php b/src/Messaging/Validator/Constraint/ContainsPlaceholder.php similarity index 86% rename from src/Validator/Constraint/ContainsPlaceholder.php rename to src/Messaging/Validator/Constraint/ContainsPlaceholder.php index f6179ed..613441b 100644 --- a/src/Validator/Constraint/ContainsPlaceholder.php +++ b/src/Messaging/Validator/Constraint/ContainsPlaceholder.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Validator\Constraint; +namespace PhpList\RestBundle\Messaging\Validator\Constraint; use Symfony\Component\Validator\Constraint; diff --git a/src/Validator/Constraint/ContainsPlaceholderValidator.php b/src/Messaging/Validator/Constraint/ContainsPlaceholderValidator.php similarity index 93% rename from src/Validator/Constraint/ContainsPlaceholderValidator.php rename to src/Messaging/Validator/Constraint/ContainsPlaceholderValidator.php index 75877f6..c33a65d 100644 --- a/src/Validator/Constraint/ContainsPlaceholderValidator.php +++ b/src/Messaging/Validator/Constraint/ContainsPlaceholderValidator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Validator\Constraint; +namespace PhpList\RestBundle\Messaging\Validator\Constraint; use InvalidArgumentException; use Symfony\Component\Validator\Constraint; diff --git a/src/Validator/Constraint/TemplateExists.php b/src/Messaging/Validator/Constraint/TemplateExists.php similarity index 91% rename from src/Validator/Constraint/TemplateExists.php rename to src/Messaging/Validator/Constraint/TemplateExists.php index b46b395..7585223 100644 --- a/src/Validator/Constraint/TemplateExists.php +++ b/src/Messaging/Validator/Constraint/TemplateExists.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Validator\Constraint; +namespace PhpList\RestBundle\Messaging\Validator\Constraint; use Symfony\Component\Validator\Constraint; diff --git a/src/Validator/Constraint/TemplateExistsValidator.php b/src/Messaging/Validator/Constraint/TemplateExistsValidator.php similarity index 90% rename from src/Validator/Constraint/TemplateExistsValidator.php rename to src/Messaging/Validator/Constraint/TemplateExistsValidator.php index 0cfdc08..7e211c2 100644 --- a/src/Validator/Constraint/TemplateExistsValidator.php +++ b/src/Messaging/Validator/Constraint/TemplateExistsValidator.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Validator\Constraint; +namespace PhpList\RestBundle\Messaging\Validator\Constraint; -use PhpList\Core\Domain\Repository\Messaging\TemplateRepository; +use PhpList\Core\Domain\Messaging\Repository\TemplateRepository; use Symfony\Component\HttpKernel\Exception\ConflictHttpException; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; diff --git a/src/Service/Builder/BuilderFromDtoInterface.php b/src/Service/Builder/BuilderFromDtoInterface.php deleted file mode 100644 index 2778d9a..0000000 --- a/src/Service/Builder/BuilderFromDtoInterface.php +++ /dev/null @@ -1,11 +0,0 @@ -messageFormatBuilder->buildFromDto($request->format); - $schedule = $this->messageScheduleBuilder->buildFromDto($request->schedule); - $content = $this->messageContentBuilder->buildFromDto($request->content); - $options = $this->messageOptionsBuilder->buildFromDto($request->options); - $template = null; - if (isset($request->templateId)) { - $template = $this->templateRepository->find($request->templateId); - } - - if ($context->getExisting()) { - $context->getExisting()->setFormat($format); - $context->getExisting()->setSchedule($schedule); - $context->getExisting()->setContent($content); - $context->getExisting()->setOptions($options); - $context->getExisting()->setTemplate($template); - return $context->getExisting(); - } - - $metadata = new Message\MessageMetadata($request->metadata->status); - - return new Message($format, $schedule, $metadata, $content, $options, $context->getOwner(), $template); - } -} diff --git a/src/Service/Builder/MessageContentBuilder.php b/src/Service/Builder/MessageContentBuilder.php deleted file mode 100644 index e3c8b47..0000000 --- a/src/Service/Builder/MessageContentBuilder.php +++ /dev/null @@ -1,26 +0,0 @@ -subject, - $dto->text, - $dto->textMessage, - $dto->footer - ); - } -} diff --git a/src/Service/Builder/MessageFormatBuilder.php b/src/Service/Builder/MessageFormatBuilder.php deleted file mode 100644 index f5d43b0..0000000 --- a/src/Service/Builder/MessageFormatBuilder.php +++ /dev/null @@ -1,25 +0,0 @@ -htmlFormated, - $dto->sendFormat, - $dto->formatOptions - ); - } -} diff --git a/src/Service/Builder/MessageOptionsBuilder.php b/src/Service/Builder/MessageOptionsBuilder.php deleted file mode 100644 index 8327157..0000000 --- a/src/Service/Builder/MessageOptionsBuilder.php +++ /dev/null @@ -1,27 +0,0 @@ -fromField ?? '', - $dto->toField ?? '', - $dto->replyTo ?? '', - $dto->userSelection, - null, - ); - } -} diff --git a/src/Service/Builder/MessageScheduleBuilder.php b/src/Service/Builder/MessageScheduleBuilder.php deleted file mode 100644 index 95463ca..0000000 --- a/src/Service/Builder/MessageScheduleBuilder.php +++ /dev/null @@ -1,28 +0,0 @@ -repeatInterval, - new DateTime($dto->repeatUntil), - $dto->requeueInterval, - new DateTime($dto->requeueUntil), - new DateTime($dto->embargo) - ); - } -} diff --git a/src/Service/Manager/AdministratorManager.php b/src/Service/Manager/AdministratorManager.php deleted file mode 100644 index a56a809..0000000 --- a/src/Service/Manager/AdministratorManager.php +++ /dev/null @@ -1,63 +0,0 @@ -entityManager = $entityManager; - $this->hashGenerator = $hashGenerator; - } - - public function createAdministrator(CreateAdministratorRequest $dto): Administrator - { - $administrator = new Administrator(); - $administrator->setLoginName($dto->loginName); - $administrator->setEmail($dto->email); - $administrator->setSuperUser($dto->superUser); - $hashedPassword = $this->hashGenerator->createPasswordHash($dto->password); - $administrator->setPasswordHash($hashedPassword); - - $this->entityManager->persist($administrator); - $this->entityManager->flush(); - - return $administrator; - } - - public function updateAdministrator(Administrator $administrator, UpdateAdministratorRequest $dto): void - { - if ($dto->loginName !== null) { - $administrator->setLoginName($dto->loginName); - } - if ($dto->email !== null) { - $administrator->setEmail($dto->email); - } - if ($dto->superAdmin !== null) { - $administrator->setSuperUser($dto->superAdmin); - } - if ($dto->password !== null) { - $hashedPassword = $this->hashGenerator->createPasswordHash($dto->password); - $administrator->setPasswordHash($hashedPassword); - } - - $this->entityManager->flush(); - } - - public function deleteAdministrator(Administrator $administrator): void - { - $this->entityManager->remove($administrator); - $this->entityManager->flush(); - } -} diff --git a/src/Service/Manager/MessageManager.php b/src/Service/Manager/MessageManager.php deleted file mode 100644 index 96dc11d..0000000 --- a/src/Service/Manager/MessageManager.php +++ /dev/null @@ -1,57 +0,0 @@ -messageRepository = $messageRepository; - $this->messageBuilder = $messageBuilder; - } - - public function createMessage(CreateMessageRequest $createMessageRequest, Administrator $authUser): Message - { - $context = new MessageContext($authUser); - $message = $this->messageBuilder->buildFromRequest($createMessageRequest, $context); - $this->messageRepository->save($message); - - return $message; - } - - public function updateMessage( - UpdateMessageRequest $updateMessageRequest, - Message $message, - Administrator $authUser - ): Message { - $context = new MessageContext($authUser, $message); - $message = $this->messageBuilder->buildFromRequest($updateMessageRequest, $context); - $this->messageRepository->save($message); - - return $message; - } - - public function delete(Message $message): void - { - $this->messageRepository->remove($message); - } - - /** @return Message[] */ - public function getMessagesByOwner(Administrator $owner): array - { - return $this->messageRepository->getByOwnerId($owner->getId()); - } -} diff --git a/src/Service/Manager/SessionManager.php b/src/Service/Manager/SessionManager.php deleted file mode 100644 index f0d02cc..0000000 --- a/src/Service/Manager/SessionManager.php +++ /dev/null @@ -1,49 +0,0 @@ -tokenRepository = $tokenRepository; - $this->administratorRepository = $administratorRepository; - } - - public function createSession(CreateSessionRequest $createSessionRequest): AdministratorToken - { - $administrator = $this->administratorRepository->findOneByLoginCredentials( - $createSessionRequest->loginName, - $createSessionRequest->password - ); - if ($administrator === null) { - throw new UnauthorizedHttpException('', 'Not authorized', null, 1500567098); - } - - $token = new AdministratorToken(); - $token->setAdministrator($administrator); - $token->generateExpiry(); - $token->generateKey(); - $this->tokenRepository->save($token); - - return $token; - } - - public function deleteSession(AdministratorToken $token): void - { - $this->tokenRepository->remove($token); - } -} diff --git a/src/Service/Manager/SubscriberListManager.php b/src/Service/Manager/SubscriberListManager.php deleted file mode 100644 index 7e8346e..0000000 --- a/src/Service/Manager/SubscriberListManager.php +++ /dev/null @@ -1,55 +0,0 @@ -subscriberListRepository = $subscriberListRepository; - } - - public function createSubscriberList( - CreateSubscriberListRequest $subscriberListRequest, - Administrator $authUser - ): SubscriberList { - $subscriberList = (new SubscriberList()) - ->setName($subscriberListRequest->name) - ->setOwner($authUser) - ->setDescription($subscriberListRequest->description) - ->setListPosition($subscriberListRequest->listPosition) - ->setPublic($subscriberListRequest->public); - - $this->subscriberListRepository->save($subscriberList); - - return $subscriberList; - } - - /** - * @return SubscriberList[] - */ - public function getPaginated(PaginationCursorRequest $pagination): array - { - return $this->subscriberListRepository->getAfterId($pagination->afterId, $pagination->limit); - } - - public function getTotalCount(): int - { - return $this->subscriberListRepository->count(); - } - - public function delete(SubscriberList $subscriberList): void - { - $this->subscriberListRepository->remove($subscriberList); - } -} diff --git a/src/Service/Manager/SubscriberManager.php b/src/Service/Manager/SubscriberManager.php deleted file mode 100644 index 174dcdc..0000000 --- a/src/Service/Manager/SubscriberManager.php +++ /dev/null @@ -1,72 +0,0 @@ -subscriberRepository = $subscriberRepository; - $this->entityManager = $entityManager; - } - - public function createSubscriber(CreateSubscriberRequest $subscriberRequest): Subscriber - { - $subscriber = new Subscriber(); - $subscriber->setEmail($subscriberRequest->email); - $confirmed = (bool)$subscriberRequest->requestConfirmation; - $subscriber->setConfirmed(!$confirmed); - $subscriber->setBlacklisted(false); - $subscriber->setHtmlEmail((bool)$subscriberRequest->htmlEmail); - $subscriber->setDisabled(false); - - $this->subscriberRepository->save($subscriber); - - return $subscriber; - } - - public function getSubscriber(int $subscriberId): Subscriber - { - $subscriber = $this->subscriberRepository->findSubscriberWithSubscriptions($subscriberId); - - if (!$subscriber) { - throw new NotFoundHttpException('Subscriber not found'); - } - - return $subscriber; - } - - public function updateSubscriber(UpdateSubscriberRequest $subscriberRequest): Subscriber - { - /** @var Subscriber $subscriber */ - $subscriber = $this->subscriberRepository->find($subscriberRequest->subscriberId); - - $subscriber->setEmail($subscriberRequest->email); - $subscriber->setConfirmed($subscriberRequest->confirmed); - $subscriber->setBlacklisted($subscriberRequest->blacklisted); - $subscriber->setHtmlEmail($subscriberRequest->htmlEmail); - $subscriber->setDisabled($subscriberRequest->disabled); - $subscriber->setExtraData($subscriberRequest->additionalData); - - $this->entityManager->flush(); - - return $subscriber; - } - - public function deleteSubscriber(Subscriber $subscriber): void - { - $this->subscriberRepository->remove($subscriber); - } -} diff --git a/src/Service/Manager/SubscriptionManager.php b/src/Service/Manager/SubscriptionManager.php deleted file mode 100644 index ab410ea..0000000 --- a/src/Service/Manager/SubscriptionManager.php +++ /dev/null @@ -1,90 +0,0 @@ -subscriptionRepository = $subscriptionRepository; - $this->subscriberRepository = $subscriberRepository; - } - - /** @return Subscription[] */ - public function createSubscriptions(SubscriberList $subscriberList, array $emails): array - { - $subscriptions = []; - foreach ($emails as $email) { - $subscriptions[] = $this->createSubscription($subscriberList, $email); - } - - return $subscriptions; - } - - private function createSubscription(SubscriberList $subscriberList, string $email): Subscription - { - $subscriber = $this->subscriberRepository->findOneBy(['email' => $email]); - if (!$subscriber) { - throw new SubscriptionCreationException('Subscriber does not exists.', 404); - } - - $existingSubscription = $this->subscriptionRepository - ->findOneBySubscriberListAndSubscriber($subscriberList, $subscriber); - if ($existingSubscription) { - return $existingSubscription; - } - - $subscription = new Subscription(); - $subscription->setSubscriber($subscriber); - $subscription->setSubscriberList($subscriberList); - - $this->subscriptionRepository->save($subscription); - - return $subscription; - } - - public function deleteSubscriptions(SubscriberList $subscriberList, array $emails): void - { - foreach ($emails as $email) { - try { - $this->deleteSubscription($subscriberList, $email); - } catch (SubscriptionCreationException $e) { - if ($e->getStatusCode() !== 404) { - throw $e; - } - } - } - } - - private function deleteSubscription(SubscriberList $subscriberList, string $email): void - { - $subscription = $this->subscriptionRepository - ->findOneBySubscriberEmailAndListId($subscriberList->getId(), $email); - - if (!$subscription) { - throw new SubscriptionCreationException('Subscription not found for this subscriber and list.', 404); - } - - $this->subscriptionRepository->remove($subscription); - } - - /** @return Subscriber[] */ - public function getSubscriberListMembers(SubscriberList $list): array - { - return $this->subscriberRepository->getSubscribersBySubscribedListId($list->getId()); - } -} diff --git a/src/Service/Manager/TemplateImageManager.php b/src/Service/Manager/TemplateImageManager.php deleted file mode 100644 index 3a594c2..0000000 --- a/src/Service/Manager/TemplateImageManager.php +++ /dev/null @@ -1,104 +0,0 @@ - 'image/gif', - 'jpg' => 'image/jpeg', - 'jpeg' => 'image/jpeg', - 'jpe' => 'image/jpeg', - 'bmp' => 'image/bmp', - 'png' => 'image/png', - 'tif' => 'image/tiff', - 'tiff' => 'image/tiff', - 'swf' => 'application/x-shockwave-flash', - ]; - - private TemplateImageRepository $templateImageRepository; - private EntityManagerInterface $entityManager; - - public function __construct( - TemplateImageRepository $templateImageRepository, - EntityManagerInterface $entityManager - ) { - $this->templateImageRepository = $templateImageRepository; - $this->entityManager = $entityManager; - } - - /** @return TemplateImage[] */ - public function createImagesFromImagePaths(array $imagePaths, Template $template): array - { - $templateImages = []; - foreach ($imagePaths as $path) { - $image = new TemplateImage(); - $image->setTemplate($template); - $image->setFilename($path); - $image->setMimeType($this->guessMimeType($path)); - $image->setData(null); - - $this->entityManager->persist($image); - $templateImages[] = $image; - } - - $this->entityManager->flush(); - - return $templateImages; - } - - private function guessMimeType(string $filename): string - { - $ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); - return self::IMAGE_MIME_TYPES[$ext] ?? 'application/octet-stream'; - } - - public function extractAllImages(string $html): array - { - $fromRegex = array_keys( - $this->extractTemplateImagesFromContent($html) - ); - - $fromDom = $this->extractImagesFromHtml($html); - - return array_values(array_unique(array_merge($fromRegex, $fromDom))); - } - - private function extractTemplateImagesFromContent(string $content): array - { - $regexp = sprintf('/"([^"]+\.(%s))"/Ui', implode('|', array_keys(self::IMAGE_MIME_TYPES))); - preg_match_all($regexp, stripslashes($content), $images); - - return array_count_values($images[1]); - } - - private function extractImagesFromHtml(string $html): array - { - $dom = new DOMDocument(); - // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged - @$dom->loadHTML($html); - $images = []; - - foreach ($dom->getElementsByTagName('img') as $img) { - $src = $img->getAttribute('src'); - if ($src) { - $images[] = $src; - } - } - - return $images; - } - - public function delete(TemplateImage $templateImage): void - { - $this->templateImageRepository->remove($templateImage); - } -} diff --git a/src/Service/Manager/TemplateManager.php b/src/Service/Manager/TemplateManager.php deleted file mode 100644 index 5e85e12..0000000 --- a/src/Service/Manager/TemplateManager.php +++ /dev/null @@ -1,88 +0,0 @@ -templateRepository = $templateRepository; - $this->entityManager = $entityManager; - $this->templateImageManager = $templateImageManager; - $this->templateLinkValidator = $templateLinkValidator; - $this->templateImageValidator = $templateImageValidator; - } - - public function create(CreateTemplateRequest $request): Template - { - $template = (new Template($request->title)) - ->setContent($request->content) - ->setText($request->text); - - if ($request->file instanceof UploadedFile) { - $template->setContent(file_get_contents($request->file->getPathname())); - } - - $context = (new ValidationContext()) - ->set('checkLinks', $request->checkLinks) - ->set('checkImages', $request->checkImages) - ->set('checkExternalImages', $request->checkExternalImages); - - $this->templateLinkValidator->validate($template->getContent() ?? '', $context); - - $imageUrls = $this->templateImageManager->extractAllImages($template->getContent() ?? ''); - $this->templateImageValidator->validate($imageUrls, $context); - - $this->templateRepository->save($template); - - $this->templateImageManager->createImagesFromImagePaths($imageUrls, $template); - - return $template; - } - - public function update(UpdateSubscriberRequest $subscriberRequest): Subscriber - { - /** @var Subscriber $subscriber */ - $subscriber = $this->templateRepository->find($subscriberRequest->subscriberId); - - $subscriber->setEmail($subscriberRequest->email); - $subscriber->setConfirmed($subscriberRequest->confirmed); - $subscriber->setBlacklisted($subscriberRequest->blacklisted); - $subscriber->setHtmlEmail($subscriberRequest->htmlEmail); - $subscriber->setDisabled($subscriberRequest->disabled); - $subscriber->setExtraData($subscriberRequest->additionalData); - - $this->entityManager->flush(); - - return $subscriber; - } - - public function delete(Template $template): void - { - $this->templateRepository->remove($template); - } -} diff --git a/src/Controller/ListMembersController.php b/src/Subscription/Controller/ListMembersController.php similarity index 82% rename from src/Controller/ListMembersController.php rename to src/Subscription/Controller/ListMembersController.php index f499a82..e64c7de 100644 --- a/src/Controller/ListMembersController.php +++ b/src/Subscription/Controller/ListMembersController.php @@ -2,23 +2,24 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Controller; +namespace PhpList\RestBundle\Subscription\Controller; use OpenApi\Attributes as OA; -use PhpList\Core\Domain\Filter\SubscriberFilter; -use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Model\Subscription\SubscriberList; +use PhpList\Core\Domain\Subscription\Model\Filter\SubscriberFilter; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; use PhpList\Core\Security\Authentication; -use PhpList\RestBundle\Serializer\SubscriberNormalizer; -use PhpList\RestBundle\Service\Provider\PaginatedDataProvider; -use PhpList\RestBundle\Validator\RequestValidator; +use PhpList\RestBundle\Common\Controller\BaseController; +use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider; +use PhpList\RestBundle\Common\Validator\RequestValidator; +use PhpList\RestBundle\Subscription\Serializer\SubscriberNormalizer; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Attribute\Route; -#[Route('/lists')] +#[Route('/lists', name: 'list_members_')] class ListMembersController extends BaseController { private SubscriberNormalizer $subscriberNormalizer; @@ -35,10 +36,11 @@ public function __construct( $this->paginatedProvider = $paginatedProvider; } - #[Route('/{listId}/subscribers', name: 'get_subscriber_from_list', methods: ['GET'])] + #[Route('/{listId}/subscribers', name: 'get_list', requirements: ['listId' => '\d+'], methods: ['GET'])] #[OA\Get( path: '/lists/{listId}/subscribers', - description: 'Returns a JSON list of all subscribers for a subscriber list.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a JSON list of all subscribers for a subscriber list.', summary: 'Gets a list of all subscribers of a subscriber list.', tags: ['subscriptions'], parameters: [ @@ -54,7 +56,7 @@ public function __construct( description: 'List ID', in: 'path', required: true, - schema: new OA\Schema(type: 'string') + schema: new OA\Schema(type: 'integer') ), new OA\Parameter( name: 'after_id', @@ -114,16 +116,17 @@ public function getListMembers( $request, $this->subscriberNormalizer, Subscriber::class, - (new SubscriberFilter())->setListId($list->getId()) + new SubscriberFilter($list->getId()) ), Response::HTTP_OK ); } - #[Route('/{listId}/subscribers/count', name: 'get_subscribers_count_from_list', methods: ['GET'])] + #[Route('/{listId}/subscribers/count', name: 'get_count', requirements: ['listId' => '\d+'], methods: ['GET'])] #[OA\Get( path: '/lists/{listId}/count', - description: 'Returns a count of all subscribers in a given list.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a count of all subscribers in a given list.', summary: 'Gets the total number of subscribers of a list', tags: ['subscriptions'], parameters: [ diff --git a/src/Subscription/Controller/SubscriberAttributeDefinitionController.php b/src/Subscription/Controller/SubscriberAttributeDefinitionController.php new file mode 100644 index 0000000..b176982 --- /dev/null +++ b/src/Subscription/Controller/SubscriberAttributeDefinitionController.php @@ -0,0 +1,351 @@ +definitionManager = $definitionManager; + $this->normalizer = $normalizer; + $this->paginatedDataProvider = $paginatedDataProvider; + } + + #[Route('', name: 'create', methods: ['POST'])] + #[OA\Post( + path: '/subscriber/attributes', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns created subscriber attribute definition.', + summary: 'Create a subscriber attribute definition.', + requestBody: new OA\RequestBody( + description: 'Pass parameters to create subscriber attribute.', + required: true, + content: new OA\JsonContent(ref: '#/components/schemas/CreateSubscriberAttributeDefinitionRequest') + ), + tags: ['subscriber-attributes'], + parameters: [ + new OA\Parameter( + name: 'session', + description: 'Session ID obtained from authentication', + 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/AttributeDefinition') + ), + 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 create(Request $request): JsonResponse + { + $this->requireAuthentication($request); + + /** @var CreateAttributeDefinitionRequest $definitionRequest */ + $definitionRequest = $this->validator->validate($request, CreateAttributeDefinitionRequest::class); + + $attributeDefinition = $this->definitionManager->create($definitionRequest->getDto()); + $json = $this->normalizer->normalize($attributeDefinition, 'json'); + + return $this->json($json, Response::HTTP_CREATED); + } + + #[Route('/{definitionId}', name: 'update', requirements: ['definitionId' => '\d+'], methods: ['PUT'])] + #[OA\Put( + path: '/subscriber/attributes/{definitionId}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns updated subscriber attribute definition.', + summary: 'Update a subscriber attribute definition.', + requestBody: new OA\RequestBody( + description: 'Pass parameters to update subscriber attribute.', + required: true, + content: new OA\JsonContent(ref: '#/components/schemas/CreateSubscriberAttributeDefinitionRequest') + ), + tags: ['subscriber-attributes'], + parameters: [ + new OA\Parameter( + name: 'definitionId', + description: 'Definition ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/AttributeDefinition') + ), + 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 update( + Request $request, + #[MapEntity(mapping: ['definitionId' => 'id'])] ?SubscriberAttributeDefinition $attributeDefinition, + ): JsonResponse { + $this->requireAuthentication($request); + if (!$attributeDefinition) { + throw $this->createNotFoundException('Attribute definition not found.'); + } + + /** @var UpdateAttributeDefinitionRequest $definitionRequest */ + $definitionRequest = $this->validator->validate($request, UpdateAttributeDefinitionRequest::class); + + $attributeDefinition = $this->definitionManager->update( + attributeDefinition: $attributeDefinition, + attributeDefinitionDto: $definitionRequest->getDto(), + ); + $json = $this->normalizer->normalize($attributeDefinition, 'json'); + + return $this->json($json, Response::HTTP_OK); + } + + #[Route('/{definitionId}', name: 'delete', requirements: ['definitionId' => '\d+'], methods: ['DELETE'])] + #[OA\Delete( + path: '/subscriber/attributes/{definitionId}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Deletes a single subscriber attribute definition.', + summary: 'Deletes an attribute definition.', + tags: ['subscriber-attributes'], + parameters: [ + new OA\Parameter( + name: 'session', + description: 'Session ID', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'definitionId', + description: 'Definition ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + 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, + #[MapEntity(mapping: ['definitionId' => 'id'])] ?SubscriberAttributeDefinition $attributeDefinition, + ): JsonResponse { + $this->requireAuthentication($request); + if (!$attributeDefinition) { + throw $this->createNotFoundException('Attribute definition not found.'); + } + + $this->definitionManager->delete($attributeDefinition); + + return $this->json(null, Response::HTTP_NO_CONTENT); + } + + #[Route('', name: 'get_list', methods: ['GET'])] + #[OA\Get( + path: '/subscriber/attributes', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a JSON list of all subscriber attribute definitions.', + summary: 'Gets a list of all subscriber attribute definitions.', + tags: ['subscriber-attributes'], + parameters: [ + new OA\Parameter( + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', + required: true, + schema: new OA\Schema( + type: 'string' + ) + ), + new OA\Parameter( + name: 'after_id', + description: 'Last id (starting from 0)', + in: 'query', + required: false, + schema: new OA\Schema(type: 'integer', default: 1, minimum: 1) + ), + new OA\Parameter( + name: 'limit', + description: 'Number of results per page', + in: 'query', + required: false, + schema: new OA\Schema(type: 'integer', default: 25, maximum: 100, minimum: 1) + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: 'items', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/AttributeDefinition') + ), + new OA\Property(property: 'pagination', ref: '#/components/schemas/CursorPagination') + ], + type: 'object' + ) + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ) + ] + )] + public function getPaginated(Request $request): JsonResponse + { + $this->requireAuthentication($request); + + return $this->json( + $this->paginatedDataProvider->getPaginatedList( + $request, + $this->normalizer, + SubscriberAttributeDefinition::class, + ), + Response::HTTP_OK + ); + } + + #[Route('/{definitionId}', name: 'get_one', requirements: ['definitionId' => '\d+'], methods: ['GET'])] + #[OA\Get( + path: '/subscriber/attributes/{definitionId}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a single attribute with specified ID.', + summary: 'Gets attribute with specified ID.', + tags: ['subscriber-attributes'], + parameters: [ + new OA\Parameter( + name: 'definitionId', + description: 'Definition ID', + in: 'path', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/AttributeDefinition') + ), + 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( + properties: [ + new OA\Property( + property: 'message', + type: 'string', + example: 'There is no attribute with that ID.' + ) + ], + type: 'object' + ) + ) + ] + )] + public function getAttributeDefinition( + Request $request, + #[MapEntity(mapping: ['definitionId' => 'id'])] ?SubscriberAttributeDefinition $attributeDefinition, + ): JsonResponse { + $this->requireAuthentication($request); + if (!$attributeDefinition) { + throw $this->createNotFoundException('Attribute definition not found.'); + } + + return $this->json( + $this->normalizer->normalize($attributeDefinition), + Response::HTTP_OK + ); + } +} diff --git a/src/Subscription/Controller/SubscriberAttributeValueController.php b/src/Subscription/Controller/SubscriberAttributeValueController.php new file mode 100644 index 0000000..cd7e1e4 --- /dev/null +++ b/src/Subscription/Controller/SubscriberAttributeValueController.php @@ -0,0 +1,358 @@ +attributeManager = $attributeManager; + $this->normalizer = $normalizer; + $this->paginatedDataProvider = $paginatedDataProvider; + } + + #[Route( + path: '/{subscriberId}/{definitionId}', + name: 'create', + requirements: ['subscriberId' => '\d+', 'definitionId' => '\d+'], + methods: ['POST', 'PUT'] + )] + #[OA\Post( + path: '/subscriber/attribute-values/{subscriberId}/{definitionId}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns created/updated subscriber attribute.', + summary: 'Create/update a subscriber attribute.', + requestBody: new OA\RequestBody( + description: 'Pass parameters to create subscriber attribute.', + required: true, + content: new OA\JsonContent( + properties: [ + new OA\Property(property: 'value', type: 'string', example: 'United States'), + ] + ) + ), + tags: ['subscriber-attributes'], + parameters: [ + new OA\Parameter( + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'definitionId', + description: 'attribute definition id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + new OA\Parameter( + name: 'subscriberId', + description: 'Subscriber id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + ], + responses: [ + new OA\Response( + response: 201, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/SubscriberAttributeValue') + ), + 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, + #[MapEntity(mapping: ['definitionId' => 'id'])] ?SubscriberAttributeDefinition $definition = null, + #[MapEntity(mapping: ['subscriberId' => 'id'])] ?Subscriber $subscriber = null, + ): JsonResponse { + $this->requireAuthentication($request); + + if (!$definition) { + throw $this->createNotFoundException('Attribute definition not found.'); + } + if (!$subscriber) { + throw $this->createNotFoundException('Subscriber not found.'); + } + + $attributeDefinition = $this->attributeManager->createOrUpdate( + subscriber:$subscriber, + definition: $definition, + value: $request->toArray()['value'] ?? null + ); + $json = $this->normalizer->normalize($attributeDefinition, 'json'); + + return $this->json($json, Response::HTTP_CREATED); + } + + #[Route( + path: '/{subscriberId}/{definitionId}', + name: 'delete', + requirements: ['subscriberId' => '\d+', 'definitionId' => '\d+'], + methods: ['DELETE'] + )] + #[OA\Delete( + path: '/subscriber/attribute-values/{subscriberId}/{definitionId}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Deletes a single subscriber attribute.', + summary: 'Deletes an attribute.', + tags: ['subscriber-attributes'], + parameters: [ + new OA\Parameter( + name: 'session', + description: 'Session ID', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + new OA\Parameter( + name: 'definitionId', + description: 'attribute definition id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + new OA\Parameter( + name: 'subscriberId', + description: 'Subscriber id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + ], + responses: [ + new OA\Response( + response: 200, + 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, + #[MapEntity(mapping: ['definitionId' => 'id'])] ?SubscriberAttributeDefinition $definition = null, + #[MapEntity(mapping: ['subscriberId' => 'id'])] ?Subscriber $subscriber = null, + ): JsonResponse { + $this->requireAuthentication($request); + if (!$definition || !$subscriber) { + throw $this->createNotFoundException('Subscriber attribute not found.'); + } + $attribute = $this->attributeManager->getSubscriberAttribute($subscriber->getId(), $definition->getId()); + if ($attribute === null) { + throw $this->createNotFoundException('Subscriber attribute not found.'); + } + $this->attributeManager->delete($attribute); + + return $this->json(null, Response::HTTP_NO_CONTENT); + } + + #[Route('/{subscriberId}', name: 'get_list', requirements: ['subscriberId' => '\d+'], methods: ['GET'])] + #[OA\Get( + path: '/subscribers/attribute-values/{subscriberId}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a JSON list of all subscriber attributes.', + summary: 'Gets a list of all subscriber attributes.', + tags: ['subscriber-attributes'], + parameters: [ + new OA\Parameter( + name: 'session', + description: 'Session ID obtained from authentication', + 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: 'integer') + ), + new OA\Parameter( + name: 'after_id', + description: 'Last id (starting from 0)', + in: 'query', + required: false, + schema: new OA\Schema(type: 'integer', default: 1, minimum: 1) + ), + new OA\Parameter( + name: 'limit', + description: 'Number of results per page', + in: 'query', + required: false, + schema: new OA\Schema(type: 'integer', default: 25, maximum: 100, minimum: 1) + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent( + properties: [ + new OA\Property( + property: 'items', + type: 'array', + items: new OA\Items(ref: '#/components/schemas/SubscriberAttributeValue') + ), + new OA\Property(property: 'pagination', ref: '#/components/schemas/CursorPagination') + ], + type: 'object' + ) + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ) + ] + )] + public function getPaginated( + Request $request, + #[MapEntity(mapping: ['subscriberId' => 'id'])] ?Subscriber $subscriber = null, + ): JsonResponse { + $this->requireAuthentication($request); + + $filter = (new SubscriberAttributeValueFilter())->setSubscriberId($subscriber->getId()); + + return $this->json( + $this->paginatedDataProvider->getPaginatedList( + $request, + $this->normalizer, + SubscriberAttributeValue::class, + $filter + ), + Response::HTTP_OK + ); + } + + #[Route( + path: '/{subscriberId}/{definitionId}', + name: 'get_one', + requirements: ['subscriberId' => '\d+', 'definitionId' => '\d+'], + methods: ['GET'] + )] + #[OA\Get( + path: '/subscribers/attribute-values/{subscriberId}/{definitionId}', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a single attribute.', + summary: 'Gets subscriber attribute.', + tags: ['subscriber-attributes'], + parameters: [ + new OA\Parameter( + name: 'definitionId', + description: 'attribute definition id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + new OA\Parameter( + name: 'subscriberId', + description: 'Subscriber id', + in: 'path', + required: true, + schema: new OA\Schema(type: 'integer') + ), + new OA\Parameter( + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ) + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\JsonContent(ref: '#/components/schemas/SubscriberAttributeValue') + ), + 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( + properties: [ + new OA\Property( + property: 'message', + type: 'string', + example: 'There is no attribute with that ID.' + ) + ], + type: 'object' + ) + ) + ] + )] + public function getAttributeDefinition( + Request $request, + #[MapEntity(mapping: ['subscriberId' => 'id'])] ?SubscriberAttributeDefinition $subscriber, + #[MapEntity(mapping: ['definitionId' => 'id'])] ?SubscriberAttributeDefinition $definition, + ): JsonResponse { + $this->requireAuthentication($request); + if (!$definition || !$subscriber) { + throw $this->createNotFoundException('Subscriber attribute not found.'); + } + $attribute = $this->attributeManager->getSubscriberAttribute($subscriber->getId(), $definition->getId()); + $this->attributeManager->delete($attribute); + + return $this->json( + $this->normalizer->normalize($attribute), + Response::HTTP_OK + ); + } +} diff --git a/src/Controller/SubscriberController.php b/src/Subscription/Controller/SubscriberController.php similarity index 78% rename from src/Controller/SubscriberController.php rename to src/Subscription/Controller/SubscriberController.php index 7ed809d..bad2924 100644 --- a/src/Controller/SubscriberController.php +++ b/src/Subscription/Controller/SubscriberController.php @@ -2,16 +2,17 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Controller; +namespace PhpList\RestBundle\Subscription\Controller; use OpenApi\Attributes as OA; -use PhpList\Core\Domain\Model\Subscription\Subscriber; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberManager; use PhpList\Core\Security\Authentication; -use PhpList\RestBundle\Entity\Request\CreateSubscriberRequest; -use PhpList\RestBundle\Entity\Request\UpdateSubscriberRequest; -use PhpList\RestBundle\Serializer\SubscriberNormalizer; -use PhpList\RestBundle\Service\Manager\SubscriberManager; -use PhpList\RestBundle\Validator\RequestValidator; +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\Serializer\SubscriberNormalizer; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -24,7 +25,7 @@ * @author Oliver Klee * @author Tatevik Grigoryan */ -#[Route('/subscribers')] +#[Route('/subscribers', name: 'subscriber_')] class SubscriberController extends BaseController { private SubscriberManager $subscriberManager; @@ -42,22 +43,16 @@ public function __construct( $this->subscriberNormalizer = $subscriberNormalizer; } - #[Route('', name: 'create_subscriber', methods: ['POST'])] + #[Route('', name: 'create', methods: ['POST'])] #[OA\Post( path: '/subscribers', - description: 'Creates a new subscriber (if there is no subscriber with the given email address yet).', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Creates a new subscriber (if there is no subscriber with the given email address yet).', summary: 'Create a subscriber', requestBody: new OA\RequestBody( description: 'Pass session credentials', required: true, - content: new OA\JsonContent( - required: ['email'], - properties: [ - new OA\Property(property: 'email', type: 'string', format: 'string', example: 'admin@example.com'), - new OA\Property(property: 'request_confirmation', type: 'boolean', example: false), - new OA\Property(property: 'html_email', type: 'boolean', example: false), - ] - ) + content: new OA\JsonContent(ref: '#/components/schemas/CreateSubscriberRequest') ), tags: ['subscribers'], parameters: [ @@ -98,7 +93,7 @@ public function createSubscriber(Request $request): JsonResponse /** @var CreateSubscriberRequest $subscriberRequest */ $subscriberRequest = $this->validator->validate($request, CreateSubscriberRequest::class); - $subscriber = $this->subscriberManager->createSubscriber($subscriberRequest); + $subscriber = $this->subscriberManager->createSubscriber($subscriberRequest->getDto()); return $this->json( $this->subscriberNormalizer->normalize($subscriber, 'json'), @@ -106,25 +101,16 @@ public function createSubscriber(Request $request): JsonResponse ); } - #[Route('/{subscriberId}', name: 'update_subscriber', requirements: ['subscriberId' => '\d+'], methods: ['PUT'])] + #[Route('/{subscriberId}', name: 'update', requirements: ['subscriberId' => '\d+'], methods: ['PUT'])] #[OA\Put( path: '/subscribers/{subscriberId}', - description: 'Update subscriber data by id.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Update subscriber data by id.', summary: 'Update subscriber', requestBody: new OA\RequestBody( description: 'Pass session credentials', required: true, - content: new OA\JsonContent( - required: ['email'], - properties: [ - new OA\Property(property: 'email', type: 'string', format: 'string', example: 'admin@example.com'), - new OA\Property(property: 'confirmed', type: 'boolean', example: false), - new OA\Property(property: 'blacklisted', type: 'boolean', example: false), - new OA\Property(property: 'html_email', type: 'boolean', example: false), - new OA\Property(property: 'disabled', type: 'boolean', example: false), - new OA\Property(property: 'additional_data', type: 'string', example: 'asdf'), - ] - ) + content: new OA\JsonContent(ref: '#/components/schemas/UpdateSubscriberRequest') ), tags: ['subscribers'], parameters: [ @@ -175,17 +161,18 @@ public function updateSubscriber( if (!$subscriber) { throw $this->createNotFoundException('Subscriber not found.'); } - /** @var UpdateSubscriberRequest $dto */ - $dto = $this->validator->validate($request, UpdateSubscriberRequest::class); - $subscriber = $this->subscriberManager->updateSubscriber($dto); + /** @var UpdateSubscriberRequest $updateSubscriberRequest */ + $updateSubscriberRequest = $this->validator->validate($request, UpdateSubscriberRequest::class); + $subscriber = $this->subscriberManager->updateSubscriber($updateSubscriberRequest->getDto()); return $this->json($this->subscriberNormalizer->normalize($subscriber, 'json'), Response::HTTP_OK); } - #[Route('/{subscriberId}', name: 'get_subscriber_by_id', methods: ['GET'])] + #[Route('/{subscriberId}', name: 'get_one', requirements: ['subscriberId' => '\d+'], methods: ['GET'])] #[OA\Get( path: '/subscribers/{subscriberId}', - description: 'Get subscriber data by id.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Get subscriber data by id.', summary: 'Get a subscriber', tags: ['subscribers'], parameters: [ @@ -231,10 +218,11 @@ public function getSubscriber(Request $request, int $subscriberId): JsonResponse return $this->json($this->subscriberNormalizer->normalize($subscriber), Response::HTTP_OK); } - #[Route('/{subscriberId}', name: 'delete_subscriber', requirements: ['subscriberId' => '\d+'], methods: ['DELETE'])] + #[Route('/{subscriberId}', name: 'delete', requirements: ['subscriberId' => '\d+'], methods: ['DELETE'])] #[OA\Delete( path: '/subscribers/{subscriberId}', - description: 'Delete subscriber by id.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Delete subscriber by id.', summary: 'Delete subscriber', tags: ['subscribers'], parameters: [ diff --git a/src/Subscription/Controller/SubscriberExportController.php b/src/Subscription/Controller/SubscriberExportController.php new file mode 100644 index 0000000..4827bd5 --- /dev/null +++ b/src/Subscription/Controller/SubscriberExportController.php @@ -0,0 +1,77 @@ +exportManager = $exportManager; + } + + #[Route('/export', name: 'csv', methods: ['POST'])] + #[OA\Post( + path: '/subscribers/export', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Export subscribers to CSV file.', + summary: 'Export subscribers', + requestBody: new OA\RequestBody( + description: 'Filter parameters for subscribers to export. ', + required: true, + content: new OA\JsonContent(ref: '#/components/schemas/ExportSubscriberRequest') + ), + tags: ['subscribers'], + parameters: [ + new OA\Parameter( + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', + required: true, + schema: new OA\Schema(type: 'string') + ), + ], + responses: [ + new OA\Response( + response: 200, + description: 'Success', + content: new OA\MediaType( + mediaType: 'text/csv', + schema: new OA\Schema(type: 'string', format: 'binary') + ) + ), + new OA\Response( + response: 403, + description: 'Failure', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ) + ] + )] + public function exportSubscribers(Request $request): Response + { + $this->requireAuthentication($request); + + /** @var SubscribersExportRequest $exportRequest */ + $exportRequest = $this->validator->validate($request, SubscribersExportRequest::class); + + return $this->exportManager->exportToCsv($exportRequest->getDto()); + } +} diff --git a/src/Subscription/Controller/SubscriberImportController.php b/src/Subscription/Controller/SubscriberImportController.php new file mode 100644 index 0000000..dcc60d7 --- /dev/null +++ b/src/Subscription/Controller/SubscriberImportController.php @@ -0,0 +1,139 @@ +importManager = $importManager; + } + + #[Route('/import', name: 'csv', methods: ['POST'])] + #[OA\Post( + path: '/subscribers/import', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Import subscribers from CSV file.', + summary: 'Import subscribers', + requestBody: new OA\RequestBody( + required: true, + content: new OA\MediaType( + mediaType: 'multipart/form-data', + schema: new OA\Schema( + properties: [ + new OA\Property( + property: 'file', + description: 'CSV file with subscribers data', + type: 'string', + format: 'binary' + ), + new OA\Property( + property: 'list_id', + description: 'List id to add imported subscribers to', + type: 'integer', + default: null + ), + new OA\Property( + property: 'update_existing', + description: 'Weather to update existing subscribers or not', + type: 'boolean', + default: false + ) + ], + type: 'object' + ) + ) + ), + tags: ['subscribers'], + parameters: [ + new OA\Parameter( + name: 'session', + description: 'Session ID obtained from authentication', + in: 'header', + 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: 'imported', type: 'integer'), + new OA\Property(property: 'skipped', type: 'integer'), + new OA\Property( + property: 'errors', + type: 'array', + items: new OA\Items(type: 'string') + ) + ] + ) + ), + new OA\Response( + response: 400, + description: 'Bad Request', + content: new OA\JsonContent(ref: '#/components/schemas/BadRequestResponse') + ), + new OA\Response( + response: 403, + description: 'Unauthorized', + content: new OA\JsonContent(ref: '#/components/schemas/UnauthorizedResponse') + ) + ] + )] + public function importSubscribers(Request $request): JsonResponse + { + $this->requireAuthentication($request); + + /** @var UploadedFile|null $file */ + $file = $request->files->get('file'); + + if (!$file) { + return $this->json(['message' => 'No file uploaded'], Response::HTTP_BAD_REQUEST); + } + + if ($file->getClientMimeType() !== 'text/csv' && $file->getClientOriginalExtension() !== 'csv') { + return $this->json(['message' => 'File must be a CSV'], Response::HTTP_BAD_REQUEST); + } + + try { + $stats = $this->importManager->importFromCsv( + $file, + new SubscriberImportOptions($request->getPayload()->getBoolean('update_existing')) + ); + + return $this->json([ + 'imported' => $stats['created'], + 'skipped' => $stats['skipped'], + 'errors' => $stats['errors'] + ]); + } catch (Exception $e) { + return $this->json([ + 'message' => $e->getMessage() + ], Response::HTTP_BAD_REQUEST); + } + } +} diff --git a/src/Controller/ListController.php b/src/Subscription/Controller/SubscriberListController.php similarity index 83% rename from src/Controller/ListController.php rename to src/Subscription/Controller/SubscriberListController.php index c7af395..30e6958 100644 --- a/src/Controller/ListController.php +++ b/src/Subscription/Controller/SubscriberListController.php @@ -2,16 +2,17 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Controller; +namespace PhpList\RestBundle\Subscription\Controller; use OpenApi\Attributes as OA; -use PhpList\Core\Domain\Model\Subscription\SubscriberList; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; +use PhpList\Core\Domain\Subscription\Service\Manager\SubscriberListManager; use PhpList\Core\Security\Authentication; -use PhpList\RestBundle\Entity\Request\CreateSubscriberListRequest; -use PhpList\RestBundle\Serializer\SubscriberListNormalizer; -use PhpList\RestBundle\Service\Manager\SubscriberListManager; -use PhpList\RestBundle\Service\Provider\PaginatedDataProvider; -use PhpList\RestBundle\Validator\RequestValidator; +use PhpList\RestBundle\Common\Controller\BaseController; +use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider; +use PhpList\RestBundle\Common\Validator\RequestValidator; +use PhpList\RestBundle\Subscription\Request\CreateSubscriberListRequest; +use PhpList\RestBundle\Subscription\Serializer\SubscriberListNormalizer; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -25,8 +26,8 @@ * @author Xheni Myrtaj * @author Tatevik Grigoryan */ -#[Route('/lists')] -class ListController extends BaseController +#[Route('/lists', name: 'subscriber_list_')] +class SubscriberListController extends BaseController { private SubscriberListNormalizer $normalizer; private SubscriberListManager $subscriberListManager; @@ -45,10 +46,11 @@ public function __construct( $this->paginatedDataProvider = $paginatedDataProvider; } - #[Route('', name: 'get_lists', methods: ['GET'])] + #[Route('', name: 'get_list', methods: ['GET'])] #[OA\Get( path: '/lists', - description: 'Returns a JSON list of all subscriber lists.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a JSON list of all subscriber lists.', summary: 'Gets a list of all subscriber lists.', tags: ['lists'], parameters: [ @@ -109,10 +111,11 @@ public function getLists(Request $request): JsonResponse ); } - #[Route('/{listId}', name: 'get_list', methods: ['GET'])] + #[Route('/{listId}', name: 'get_one', requirements: ['listId' => '\d+'], methods: ['GET'])] #[OA\Get( path: '/lists/{listId}', - description: 'Returns a single subscriber list with specified ID.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns a single subscriber list with specified ID.', summary: 'Gets a subscriber list.', tags: ['lists'], parameters: [ @@ -171,10 +174,11 @@ public function getList( return $this->json($this->normalizer->normalize($list), Response::HTTP_OK); } - #[Route('/{listId}', name: 'delete_list', methods: ['DELETE'])] + #[Route('/{listId}', name: 'delete', requirements: ['listId' => '\d+'], methods: ['DELETE'])] #[OA\Delete( path: '/lists/{listId}', - description: 'Deletes a single subscriber list.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Deletes a single subscriber list.', summary: 'Deletes a list.', tags: ['lists'], parameters: [ @@ -225,23 +229,16 @@ public function deleteList( return $this->json(null, Response::HTTP_NO_CONTENT); } - #[Route('', name: 'create_list', methods: ['POST'])] + #[Route('', name: 'create', methods: ['POST'])] #[OA\Post( path: '/lists', - description: 'Returns created list.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Returns created list.', summary: 'Create a subscriber list.', requestBody: new OA\RequestBody( description: 'Pass parameters to create a new subscriber list.', required: true, - content: new OA\JsonContent( - required: ['name'], - properties: [ - new OA\Property(property: 'name', type: 'string', format: 'string', example: 'News'), - new OA\Property(property: 'description', type: 'string', example: 'News (and some fun stuff)'), - new OA\Property(property: 'list_position', type: 'number', example: 12), - new OA\Property(property: 'public', type: 'boolean', example: true), - ] - ) + content: new OA\JsonContent(ref: '#/components/schemas/CreateSubscriberListRequest') ), tags: ['lists'], parameters: [ @@ -279,7 +276,7 @@ public function createList(Request $request, SubscriberListNormalizer $normalize /** @var CreateSubscriberListRequest $subscriberListRequest */ $subscriberListRequest = $this->validator->validate($request, CreateSubscriberListRequest::class); - $data = $this->subscriberListManager->createSubscriberList($subscriberListRequest, $authUser); + $data = $this->subscriberListManager->createSubscriberList($subscriberListRequest->getDto(), $authUser); return $this->json($normalizer->normalize($data), Response::HTTP_CREATED); } diff --git a/src/Controller/SubscriptionController.php b/src/Subscription/Controller/SubscriptionController.php similarity index 86% rename from src/Controller/SubscriptionController.php rename to src/Subscription/Controller/SubscriptionController.php index ea2dacc..2db3ac4 100644 --- a/src/Controller/SubscriptionController.php +++ b/src/Subscription/Controller/SubscriptionController.php @@ -2,15 +2,16 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Controller; +namespace PhpList\RestBundle\Subscription\Controller; use OpenApi\Attributes as OA; -use PhpList\Core\Domain\Model\Subscription\SubscriberList; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; +use PhpList\Core\Domain\Subscription\Service\Manager\SubscriptionManager; use PhpList\Core\Security\Authentication; -use PhpList\RestBundle\Entity\Request\SubscriptionRequest; -use PhpList\RestBundle\Serializer\SubscriptionNormalizer; -use PhpList\RestBundle\Service\Manager\SubscriptionManager; -use PhpList\RestBundle\Validator\RequestValidator; +use PhpList\RestBundle\Common\Controller\BaseController; +use PhpList\RestBundle\Common\Validator\RequestValidator; +use PhpList\RestBundle\Subscription\Request\SubscriptionRequest; +use PhpList\RestBundle\Subscription\Serializer\SubscriptionNormalizer; use Symfony\Bridge\Doctrine\Attribute\MapEntity; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; @@ -22,7 +23,7 @@ * * @author Tatevik Grigoryan */ -#[Route('/lists')] +#[Route('/lists', name: 'subscription_')] class SubscriptionController extends BaseController { private SubscriptionManager $subscriptionManager; @@ -39,10 +40,11 @@ public function __construct( $this->subscriptionNormalizer = $subscriptionNormalizer; } - #[Route('/{listId}/subscribers', name: 'create_subscription', methods: ['POST'])] + #[Route('/{listId}/subscribers', name: 'create', requirements: ['listId' => '\d+'], methods: ['POST'])] #[OA\Post( path: '/lists/{listId}/subscribers', - description: 'Subscribe subscriber to a list.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Subscribe subscriber to a list.', summary: 'Create subscription', requestBody: new OA\RequestBody( description: 'Pass session credentials', @@ -130,10 +132,11 @@ public function createSubscription( return $this->json($normalized, Response::HTTP_CREATED); } - #[Route('/{listId}/subscribers', name: 'delete_subscription', methods: ['DELETE'])] + #[Route('/{listId}/subscribers', name: 'delete', requirements: ['listId' => '\d+'], methods: ['DELETE'])] #[OA\Delete( path: '/lists/{listId}/subscribers', - description: 'Delete subscription.', + description: '🚧 **Status: Beta** – This method is under development. Avoid using in production. ' . + 'Delete subscription.', summary: 'Delete subscription', tags: ['subscriptions'], parameters: [ diff --git a/src/Subscription/OpenApi/SwaggerSchemasRequest.php b/src/Subscription/OpenApi/SwaggerSchemasRequest.php new file mode 100644 index 0000000..ac7b12b --- /dev/null +++ b/src/Subscription/OpenApi/SwaggerSchemasRequest.php @@ -0,0 +1,107 @@ +name, + $this->type, + $this->order, + $this->defaultValue, + $this->required, + $this->tableName, + ); + } +} diff --git a/src/Subscription/Request/CreateSubscriberListRequest.php b/src/Subscription/Request/CreateSubscriberListRequest.php new file mode 100644 index 0000000..960bf54 --- /dev/null +++ b/src/Subscription/Request/CreateSubscriberListRequest.php @@ -0,0 +1,32 @@ +name, + isPublic: $this->public, + listPosition: $this->listPosition, + description: $this->description, + ); + } +} diff --git a/src/Subscription/Request/CreateSubscriberRequest.php b/src/Subscription/Request/CreateSubscriberRequest.php new file mode 100644 index 0000000..c8b9432 --- /dev/null +++ b/src/Subscription/Request/CreateSubscriberRequest.php @@ -0,0 +1,34 @@ +email, + requestConfirmation: $this->requestConfirmation, + htmlEmail: $this->htmlEmail, + ); + } +} diff --git a/src/Subscription/Request/SubscribersExportRequest.php b/src/Subscription/Request/SubscribersExportRequest.php new file mode 100644 index 0000000..aad7464 --- /dev/null +++ b/src/Subscription/Request/SubscribersExportRequest.php @@ -0,0 +1,82 @@ +dateFrom ? new DateTimeImmutable($this->dateFrom) : null; + $dateTo = $this->dateTo ? new DateTimeImmutable($this->dateTo) : null; + + return match ($this->dateType) { + 'subscribed' => [$dateFrom, $dateTo, null, null, null, null], + 'signup' => [null, null, $dateFrom, $dateTo, null, null], + 'changed' => [null, null, null, null, $dateFrom, $dateTo], + 'any', 'changelog' => [null, null, null, null, null, null], + default => [null, null, null, null, null, null], + }; + } + + public function getDto(): SubscriberFilter + { + [$subscribedFrom, $subscribedTo, $signupFrom, $signupTo, $changedFrom, $changedTo] = $this->resolveDates(); + + return new SubscriberFilter( + $this->listId ?? null, + $subscribedFrom, + $subscribedTo, + $signupFrom, + $signupTo, + $changedFrom, + $changedTo, + $this->columns + ); + } +} diff --git a/src/Entity/Request/SubscriptionRequest.php b/src/Subscription/Request/SubscriptionRequest.php similarity index 56% rename from src/Entity/Request/SubscriptionRequest.php rename to src/Subscription/Request/SubscriptionRequest.php index c31e087..2e4abed 100644 --- a/src/Entity/Request/SubscriptionRequest.php +++ b/src/Subscription/Request/SubscriptionRequest.php @@ -2,9 +2,10 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Entity\Request; +namespace PhpList\RestBundle\Subscription\Request; -use PhpList\RestBundle\Validator\Constraint\EmailExists; +use PhpList\RestBundle\Common\Request\RequestInterface; +use PhpList\RestBundle\Subscription\Validator\Constraint\EmailExists; use Symfony\Component\Validator\Constraints as Assert; class SubscriptionRequest implements RequestInterface @@ -17,4 +18,9 @@ class SubscriptionRequest implements RequestInterface new EmailExists() ])] public array $emails = []; + + public function getDto(): SubscriptionRequest + { + return $this; + } } diff --git a/src/Subscription/Request/UpdateAttributeDefinitionRequest.php b/src/Subscription/Request/UpdateAttributeDefinitionRequest.php new file mode 100644 index 0000000..ba4fa95 --- /dev/null +++ b/src/Subscription/Request/UpdateAttributeDefinitionRequest.php @@ -0,0 +1,33 @@ +name, + $this->type, + $this->order, + $this->defaultValue, + $this->required, + $this->tableName, + ); + } +} diff --git a/src/Subscription/Request/UpdateSubscriberRequest.php b/src/Subscription/Request/UpdateSubscriberRequest.php new file mode 100644 index 0000000..1bce0d7 --- /dev/null +++ b/src/Subscription/Request/UpdateSubscriberRequest.php @@ -0,0 +1,49 @@ +subscriberId, + email: $this->email, + confirmed: $this->confirmed, + blacklisted: $this->blacklisted, + htmlEmail: $this->htmlEmail, + disabled: $this->disabled, + additionalData: $this->additionalData, + ); + } +} diff --git a/src/Subscription/Serializer/AttributeDefinitionNormalizer.php b/src/Subscription/Serializer/AttributeDefinitionNormalizer.php new file mode 100644 index 0000000..598bfad --- /dev/null +++ b/src/Subscription/Serializer/AttributeDefinitionNormalizer.php @@ -0,0 +1,39 @@ + $object->getId(), + 'name' => $object->getName(), + 'type' => $object->getType(), + 'list_order' => $object->getListOrder(), + 'default_value' => $object->getDefaultValue(), + 'required' => $object->isRequired(), + 'table_name' => $object->getTableName(), + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function supportsNormalization($data, string $format = null): bool + { + return $data instanceof SubscriberAttributeDefinition; + } +} diff --git a/src/Subscription/Serializer/SubscriberAttributeValueNormalizer.php b/src/Subscription/Serializer/SubscriberAttributeValueNormalizer.php new file mode 100644 index 0000000..18000dc --- /dev/null +++ b/src/Subscription/Serializer/SubscriberAttributeValueNormalizer.php @@ -0,0 +1,41 @@ + $this->subscriberNormalizer->normalize($object->getSubscriber()), + 'definition' => $this->definitionNormalizer->normalize($object->getAttributeDefinition()), + 'value' => $object->getValue(), + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function supportsNormalization($data, string $format = null): bool + { + return $data instanceof SubscriberAttributeValue; + } +} diff --git a/src/Serializer/SubscriberListNormalizer.php b/src/Subscription/Serializer/SubscriberListNormalizer.php similarity index 90% rename from src/Serializer/SubscriberListNormalizer.php rename to src/Subscription/Serializer/SubscriberListNormalizer.php index 3bf93ec..601f9db 100644 --- a/src/Serializer/SubscriberListNormalizer.php +++ b/src/Subscription/Serializer/SubscriberListNormalizer.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Serializer; +namespace PhpList\RestBundle\Subscription\Serializer; -use PhpList\Core\Domain\Model\Subscription\SubscriberList; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; class SubscriberListNormalizer implements NormalizerInterface diff --git a/src/Serializer/SubscriberNormalizer.php b/src/Subscription/Serializer/SubscriberNormalizer.php similarity index 63% rename from src/Serializer/SubscriberNormalizer.php rename to src/Subscription/Serializer/SubscriberNormalizer.php index fb175b5..84353ff 100644 --- a/src/Serializer/SubscriberNormalizer.php +++ b/src/Subscription/Serializer/SubscriberNormalizer.php @@ -2,14 +2,18 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Serializer; +namespace PhpList\RestBundle\Subscription\Serializer; -use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Model\Subscription\Subscription; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Model\Subscription; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; class SubscriberNormalizer implements NormalizerInterface { + public function __construct(private readonly SubscriberListNormalizer $subscriberListNormalizer) + { + } + /** * @SuppressWarnings(PHPMD.UnusedFormalParameter) */ @@ -30,14 +34,7 @@ public function normalize($object, string $format = null, array $context = []): 'html_email' => $object->hasHtmlEmail(), 'disabled' => $object->isDisabled(), 'subscribed_lists' => array_map(function (Subscription $subscription) { - return [ - 'id' => $subscription->getSubscriberList()->getId(), - 'name' => $subscription->getSubscriberList()->getName(), - 'description' => $subscription->getSubscriberList()->getDescription(), - 'created_at' => $subscription->getSubscriberList()->getCreatedAt()->format('Y-m-d\TH:i:sP'), - 'public' => $subscription->getSubscriberList()->isPublic(), - 'subscription_date' => $subscription->getCreatedAt()->format('Y-m-d\TH:i:sP'), - ]; + return $this->subscriberListNormalizer->normalize($subscription->getSubscriberList()); }, $object->getSubscriptions()->toArray()), ]; } diff --git a/src/Subscription/Serializer/SubscriberOnlyNormalizer.php b/src/Subscription/Serializer/SubscriberOnlyNormalizer.php new file mode 100644 index 0000000..e4cc95a --- /dev/null +++ b/src/Subscription/Serializer/SubscriberOnlyNormalizer.php @@ -0,0 +1,41 @@ + $object->getId(), + 'email' => $object->getEmail(), + 'created_at' => $object->getCreatedAt()->format('Y-m-d\TH:i:sP'), + 'confirmed' => $object->isConfirmed(), + 'blacklisted' => $object->isBlacklisted(), + 'bounce_count' => $object->getBounceCount(), + 'unique_id' => $object->getUniqueId(), + 'html_email' => $object->hasHtmlEmail(), + 'disabled' => $object->isDisabled(), + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function supportsNormalization($data, string $format = null): bool + { + return $data instanceof Subscriber; + } +} diff --git a/src/Subscription/Serializer/SubscribersExportRequestNormalizer.php b/src/Subscription/Serializer/SubscribersExportRequestNormalizer.php new file mode 100644 index 0000000..122ee80 --- /dev/null +++ b/src/Subscription/Serializer/SubscribersExportRequestNormalizer.php @@ -0,0 +1,37 @@ + $object->dateType, + 'list_id' => $object->listId, + 'date_from' => $object->dateFrom, + 'date_to' => $object->dateTo, + 'columns' => $object->columns, + ]; + } + + /** + * @SuppressWarnings(PHPMD.UnusedFormalParameter) + */ + public function supportsNormalization($data, string $format = null): bool + { + return $data instanceof SubscribersExportRequest; + } +} diff --git a/src/Serializer/SubscriptionNormalizer.php b/src/Subscription/Serializer/SubscriptionNormalizer.php similarity index 84% rename from src/Serializer/SubscriptionNormalizer.php rename to src/Subscription/Serializer/SubscriptionNormalizer.php index 58df4fb..7f68a7e 100644 --- a/src/Serializer/SubscriptionNormalizer.php +++ b/src/Subscription/Serializer/SubscriptionNormalizer.php @@ -2,18 +2,18 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Serializer; +namespace PhpList\RestBundle\Subscription\Serializer; -use PhpList\Core\Domain\Model\Subscription\Subscription; +use PhpList\Core\Domain\Subscription\Model\Subscription; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; class SubscriptionNormalizer implements NormalizerInterface { - private SubscriberNormalizer $subscriberNormalizer; + private SubscriberOnlyNormalizer $subscriberNormalizer; private SubscriberListNormalizer $subscriberListNormalizer; public function __construct( - SubscriberNormalizer $subscriberNormalizer, + SubscriberOnlyNormalizer $subscriberNormalizer, SubscriberListNormalizer $subscriberListNormalizer ) { $this->subscriberNormalizer = $subscriberNormalizer; diff --git a/src/Validator/Constraint/EmailExists.php b/src/Subscription/Validator/Constraint/EmailExists.php similarity index 90% rename from src/Validator/Constraint/EmailExists.php rename to src/Subscription/Validator/Constraint/EmailExists.php index 48d0045..2d35b44 100644 --- a/src/Validator/Constraint/EmailExists.php +++ b/src/Subscription/Validator/Constraint/EmailExists.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Validator\Constraint; +namespace PhpList\RestBundle\Subscription\Validator\Constraint; use Symfony\Component\Validator\Constraint; diff --git a/src/Validator/Constraint/EmailExistsValidator.php b/src/Subscription/Validator/Constraint/EmailExistsValidator.php similarity index 90% rename from src/Validator/Constraint/EmailExistsValidator.php rename to src/Subscription/Validator/Constraint/EmailExistsValidator.php index 9c4f5b5..dc2fa25 100644 --- a/src/Validator/Constraint/EmailExistsValidator.php +++ b/src/Subscription/Validator/Constraint/EmailExistsValidator.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Validator\Constraint; +namespace PhpList\RestBundle\Subscription\Validator\Constraint; -use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; +use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Validator\Constraint; use Symfony\Component\Validator\ConstraintValidator; diff --git a/src/Subscription/Validator/Constraint/ListExists.php b/src/Subscription/Validator/Constraint/ListExists.php new file mode 100644 index 0000000..6811ff2 --- /dev/null +++ b/src/Subscription/Validator/Constraint/ListExists.php @@ -0,0 +1,22 @@ +mode = $mode ?? $this->mode; + $this->message = $message ?? $this->message; + } +} diff --git a/src/Subscription/Validator/Constraint/ListExistsValidator.php b/src/Subscription/Validator/Constraint/ListExistsValidator.php new file mode 100644 index 0000000..68f1b37 --- /dev/null +++ b/src/Subscription/Validator/Constraint/ListExistsValidator.php @@ -0,0 +1,43 @@ +subscriberListRepository = $subscriberListRepository; + } + + public function validate($value, Constraint $constraint): void + { + if (!$constraint instanceof ListExists) { + throw new UnexpectedTypeException($constraint, ListExists::class); + } + + if (null === $value || '' === $value) { + return; + } + + if (!is_string($value)) { + throw new UnexpectedValueException($value, 'string'); + } + + $existingList = $this->subscriberListRepository->find($value); + + if (!$existingList) { + throw new NotFoundHttpException('Subscriber list does not exists.'); + } + } +} diff --git a/src/Subscription/Validator/Constraint/UniqueEmail.php b/src/Subscription/Validator/Constraint/UniqueEmail.php new file mode 100644 index 0000000..f3a028f --- /dev/null +++ b/src/Subscription/Validator/Constraint/UniqueEmail.php @@ -0,0 +1,25 @@ +entityClass = $entityClass; + } + + public function validatedBy(): string + { + return UniqueEmailValidator::class; + } +} diff --git a/src/Validator/Constraint/UniqueEmailValidator.php b/src/Subscription/Validator/Constraint/UniqueEmailValidator.php similarity index 91% rename from src/Validator/Constraint/UniqueEmailValidator.php rename to src/Subscription/Validator/Constraint/UniqueEmailValidator.php index 906a526..46bf52b 100644 --- a/src/Validator/Constraint/UniqueEmailValidator.php +++ b/src/Subscription/Validator/Constraint/UniqueEmailValidator.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Validator\Constraint; +namespace PhpList\RestBundle\Subscription\Validator\Constraint; use Doctrine\ORM\EntityManagerInterface; use Symfony\Component\HttpKernel\Exception\ConflictHttpException; @@ -36,7 +36,7 @@ public function validate($value, Constraint $constraint): void ->findOneBy(['email' => $value]); $dto = $this->context->getObject(); - $updatingId = $dto->subscriberId ?? $dto->administratorId ?? null; + $updatingId = $dto->subscriberId ?? null; if ($existingUser && $existingUser->getId() !== $updatingId) { throw new ConflictHttpException('Email already exists.'); diff --git a/src/Validator/TemplateImageValidator.php b/src/Validator/TemplateImageValidator.php deleted file mode 100644 index b89da9a..0000000 --- a/src/Validator/TemplateImageValidator.php +++ /dev/null @@ -1,72 +0,0 @@ -get('checkImages', false); - $checkExist = $context?->get('checkExternalImages', false); - - $errors = array_merge( - $checkFull ? $this->validateFullUrls($value) : [], - $checkExist ? $this->validateExistence($value) : [] - ); - - if (!empty($errors)) { - throw new ValidatorException(implode("\n", $errors)); - } - } - - private function validateFullUrls(array $urls): array - { - $errors = []; - - foreach ($urls as $url) { - if (!preg_match('#^https?://#i', $url)) { - $errors[] = sprintf('Image "%s" is not a full URL.', $url); - } - } - - return $errors; - } - - private function validateExistence(array $urls): array - { - $errors = []; - - foreach ($urls as $url) { - if (!preg_match('#^https?://#i', $url)) { - continue; - } - - try { - $response = $this->httpClient->request('HEAD', $url); - if ($response->getStatusCode() !== 200) { - $errors[] = sprintf('Image "%s" does not exist (HTTP %s)', $url, $response->getStatusCode()); - } - } catch (Throwable $e) { - $errors[] = sprintf('Image "%s" could not be validated: %s', $url, $e->getMessage()); - } - } - - return $errors; - } -} diff --git a/src/Validator/TemplateLinkValidator.php b/src/Validator/TemplateLinkValidator.php deleted file mode 100644 index 78916bc..0000000 --- a/src/Validator/TemplateLinkValidator.php +++ /dev/null @@ -1,62 +0,0 @@ -get('checkLinks', false)) { - return; - } - $links = $this->extractLinks($value); - $invalid = []; - - foreach ($links as $link) { - if (!preg_match('#^https?://#i', $link) && - !preg_match('#^mailto:#i', $link) && - !in_array(strtoupper($link), self::PLACEHOLDERS, true) - ) { - $invalid[] = $link; - } - } - - if (!empty($invalid)) { - throw new ValidatorException(sprintf( - 'Not full URLs: %s', - implode(', ', $invalid) - )); - } - } - - private function extractLinks(string $html): array - { - $dom = new DOMDocument(); - // phpcs:ignore Generic.PHP.NoSilencedErrors.Discouraged - @$dom->loadHTML($html); - $links = []; - - foreach ($dom->getElementsByTagName('a') as $node) { - $href = $node->getAttribute('href'); - if ($href) { - $links[] = $href; - } - } - - return $links; - } -} diff --git a/src/Validator/ValidatorInterface.php b/src/Validator/ValidatorInterface.php deleted file mode 100644 index 9b496fa..0000000 --- a/src/Validator/ValidatorInterface.php +++ /dev/null @@ -1,12 +0,0 @@ -createMock(HttpKernelInterface::class); $request = new Request(); diff --git a/tests/Integration/EventListener/ResponseListenerTest.php b/tests/Integration/Common/EventListener/ResponseListenerTest.php similarity index 91% rename from tests/Integration/EventListener/ResponseListenerTest.php rename to tests/Integration/Common/EventListener/ResponseListenerTest.php index 709291a..c1c2d84 100644 --- a/tests/Integration/EventListener/ResponseListenerTest.php +++ b/tests/Integration/Common/EventListener/ResponseListenerTest.php @@ -1,8 +1,10 @@ getModuleRoutesConfigurationFilePath()); @@ -119,7 +120,9 @@ public static function moduleRoutingDataProvider(): array { return [ 'route name' => ['phplist/rest-api.rest-api'], - 'resource' => ["resource: '@PhpListRestBundle/Controller/'"], + 'identity' => ["resource: '@PhpListRestBundle/Identity/Controller/'"], + 'messaging' => ["resource: '@PhpListRestBundle/Messaging/Controller/'"], + 'subscription' => ["resource: '@PhpListRestBundle/Subscription/Controller/'"], 'type' => ['type: attribute'], ]; } diff --git a/tests/Integration/Identity/Controller/AdminAttributeDefinitionControllerTest.php b/tests/Integration/Identity/Controller/AdminAttributeDefinitionControllerTest.php new file mode 100644 index 0000000..dfa6618 --- /dev/null +++ b/tests/Integration/Identity/Controller/AdminAttributeDefinitionControllerTest.php @@ -0,0 +1,129 @@ +definitionRepository = self::getContainer()->get(AdminAttributeDefinitionRepository::class); + } + + public function testControllerIsAvailableViaContainer(): void + { + self::assertInstanceOf( + AdminAttributeDefinitionController::class, + self::getContainer()->get(AdminAttributeDefinitionController::class) + ); + } + + public function testCreateAttributeDefinitionWithValidDataReturnsCreated(): void + { + $this->authenticatedJsonRequest('post', '/api/v2/administrators/attributes', [], [], [], json_encode([ + 'name' => 'Test Attribute', + 'type' => 'text', + 'order' => 1, + 'defaultValue' => 'default', + 'required' => true, + 'tableName' => 'test_table', + ])); + + $this->assertHttpCreated(); + $data = $this->getDecodedJsonResponseContent(); + self::assertSame('Test Attribute', $data['name']); + self::assertSame('text', $data['type']); + self::assertSame(1, $data['list_order']); + self::assertSame('default', $data['default_value']); + self::assertTrue($data['required']); + self::assertSame('test_table', $data['table_name']); + } + + public function testUpdateAttributeDefinitionReturnsOk(): void + { + $this->loadFixtures([AdminAttributeDefinitionFixture::class]); + $id = 2; + + $this->authenticatedJsonRequest('put', '/api/v2/administrators/attributes/' . $id, [], [], [], json_encode([ + 'name' => 'Updated Attribute', + 'type' => 'checkbox', + 'required' => true, + ])); + + $this->assertHttpOkay(); + $data = $this->getDecodedJsonResponseContent(); + self::assertSame('Updated Attribute', $data['name']); + self::assertSame('checkbox', $data['type']); + self::assertTrue($data['required']); + } + + public function testDeleteAttributeDefinitionReturnsNoContent(): void + { + $this->loadFixtures([AdminAttributeDefinitionFixture::class]); + $id = 3; + + $this->authenticatedJsonRequest('delete', '/api/v2/administrators/attributes/' . $id); + $this->assertHttpNoContent(); + + self::assertNull($this->definitionRepository->find($id)); + } + + public function testGetPaginatedReturnsOk(): void + { + $this->loadFixtures([AdminAttributeDefinitionFixture::class]); + + $this->authenticatedJsonRequest('get', '/api/v2/administrators/attributes'); + $this->assertHttpOkay(); + $data = $this->getDecodedJsonResponseContent(); + self::assertArrayHasKey('items', $data); + self::assertArrayHasKey('pagination', $data); + self::assertGreaterThanOrEqual(2, count($data['items'])); + } + + public function testGetAttributeDefinitionReturnsData(): void + { + $this->loadFixtures([AdminAttributeDefinitionFixture::class]); + $id = 6; + + $this->authenticatedJsonRequest('get', '/api/v2/administrators/attributes/' . $id); + $this->assertHttpOkay(); + $data = $this->getDecodedJsonResponseContent(); + self::assertSame('Test Get Attribute', $data['name']); + self::assertSame('text', $data['type']); + } + + public function testGetAttributeDefinitionNotFound(): void + { + $this->authenticatedJsonRequest('get', '/api/v2/administrators/attributes/999999'); + $this->assertHttpNotFound(); + } + + public function testCreateAttributeDefinitionWithInvalidJsonReturns400(): void + { + $this->authenticatedJsonRequest('post', '/api/v2/administrators/attributes', [], [], [], 'not json'); + $this->assertHttpBadRequest(); + } + + public function testCreateAttributeDefinitionWithMissingFieldsReturns422(): void + { + $this->authenticatedJsonRequest('post', '/api/v2/administrators/attributes', [], [], [], json_encode([])); + $this->assertHttpUnprocessableEntity(); + } + + public function testUpdateAttributeDefinitionWithInvalidIdReturns404(): void + { + $this->authenticatedJsonRequest('put', '/api/v2/administrators/attributes/999999', [], [], [], json_encode([ + 'name' => 'Updated Name' + ])); + $this->assertHttpNotFound(); + } +} diff --git a/tests/Integration/Identity/Controller/AdminAttributeValueControllerTest.php b/tests/Integration/Identity/Controller/AdminAttributeValueControllerTest.php new file mode 100644 index 0000000..f350315 --- /dev/null +++ b/tests/Integration/Identity/Controller/AdminAttributeValueControllerTest.php @@ -0,0 +1,185 @@ +get(AdminAttributeValueController::class) + ); + } + + public function testCreateOrUpdateAttributeValueWithValidDataReturnsCreated(): void + { + $this->loadFixtures([AdministratorFixture::class, AdminAttributeDefinitionFixture::class]); + $definitionId = 1; + $adminId = 1; + + $this->authenticatedJsonRequest( + 'post', + '/api/v2/administrators/attribute-values/' . $adminId . '/' . $definitionId, + [], + [], + [], + json_encode(['value' => 'test value']) + ); + + $this->assertHttpCreated(); + $data = $this->getDecodedJsonResponseContent(); + self::assertSame('test value', $data['value']); + self::assertSame($definitionId, $data['definition']['id']); + self::assertSame($adminId, $data['administrator']['id']); + } + + public function testUpdateAttributeValueReturnsOk(): void + { + $this->loadFixtures([ + AdministratorFixture::class, + AdminAttributeDefinitionFixture::class, + AdminAttributeValueFixture::class + ]); + $definitionId = 7; + $adminId = 1; + + $this->authenticatedJsonRequest( + 'post', + '/api/v2/administrators/attribute-values/' . $adminId . '/' . $definitionId, + [], + [], + [], + json_encode(['value' => 'updated value']) + ); + + $this->assertHttpCreated(); + $data = $this->getDecodedJsonResponseContent(); + self::assertSame('updated value', $data['value']); + } + + public function testDeleteAttributeValueReturnsNoContent(): void + { + $this->loadFixtures([ + AdministratorFixture::class, + AdminAttributeDefinitionFixture::class, + AdminAttributeValueFixture::class + ]); + $definitionId = 8; + $adminId = 1; + + $this->authenticatedJsonRequest( + 'delete', + '/api/v2/administrators/attribute-values/' . $adminId . '/' . $definitionId + ); + $this->assertHttpNoContent(); + } + + public function testGetPaginatedReturnsOk(): void + { + $this->loadFixtures([ + AdministratorFixture::class, + AdminAttributeDefinitionFixture::class, + AdminAttributeValueFixture::class + ]); + $adminId = 1; + + $this->authenticatedJsonRequest('get', '/api/v2/administrators/attribute-values/' . $adminId); + $this->assertHttpOkay(); + $data = $this->getDecodedJsonResponseContent(); + self::assertArrayHasKey('items', $data); + self::assertArrayHasKey('pagination', $data); + self::assertGreaterThanOrEqual(2, count($data['items'])); + } + + public function testGetAttributeValueReturnsData(): void + { + $this->loadFixtures([ + AdministratorFixture::class, + AdminAttributeDefinitionFixture::class, + AdminAttributeValueFixture::class + ]); + $definitionId = 11; + $adminId = 1; + + $this->authenticatedJsonRequest( + 'get', + '/api/v2/administrators/attribute-values/' . $adminId . '/' . $definitionId + ); + $this->assertHttpOkay(); + $data = $this->getDecodedJsonResponseContent(); + self::assertSame('test get value', $data['value']); + } + + public function testGetAttributeValueNotFound(): void + { + $this->loadFixtures([AdministratorFixture::class]); + $adminId = 1; + + $this->authenticatedJsonRequest( + 'get', + '/api/v2/administrators/attribute-values/' . $adminId . '/999999' + ); + $this->assertHttpNotFound(); + } + + public function testCreateAttributeValueWithInvalidJsonReturns400(): void + { + $this->loadFixtures([ + AdministratorFixture::class, + AdminAttributeDefinitionFixture::class + ]); + $definitionId = 12; + $adminId = 1; + + $this->authenticatedJsonRequest( + 'post', + '/api/v2/administrators/attribute-values/' . $adminId . '/' . $definitionId, + [], + [], + [], + 'not json' + ); + $this->assertHttpBadRequest(); + } + + public function testCreateAttributeValueWithInvalidDefinitionIdReturns404(): void + { + $this->loadFixtures([AdministratorFixture::class]); + $adminId = 1; + + $this->authenticatedJsonRequest( + 'post', + '/api/v2/administrators/attribute-values/' . $adminId . '/999999', + [], + [], + [], + json_encode(['value' => 'test value']) + ); + $this->assertHttpNotFound(); + } + + public function testCreateAttributeValueWithInvalidAdminIdReturns404(): void + { + $this->loadFixtures([AdminAttributeDefinitionFixture::class]); + $definitionId = 13; + + $this->authenticatedJsonRequest( + 'post', + '/api/v2/administrators/attribute-values/999999/' . $definitionId, + [], + [], + [], + json_encode(['value' => 'test value']) + ); + $this->assertHttpNotFound(); + } +} diff --git a/tests/Integration/Identity/Controller/AdministratorControllerTest.php b/tests/Integration/Identity/Controller/AdministratorControllerTest.php new file mode 100644 index 0000000..b9fa249 --- /dev/null +++ b/tests/Integration/Identity/Controller/AdministratorControllerTest.php @@ -0,0 +1,119 @@ +administratorRepository = self::getContainer()->get(AdministratorRepository::class); + } + + public function testControllerIsAvailableViaContainer(): void + { + self::assertInstanceOf( + AdministratorController::class, + self::getContainer()->get(AdministratorController::class) + ); + } + + public function testGetAdministratorsReturnsOk(): void + { + $this->loadFixtures([AdministratorFixture::class]); + $this->authenticatedJsonRequest('get', '/api/v2/administrators'); + + $this->assertHttpOkay(); + $data = $this->getDecodedJsonResponseContent(); + self::assertArrayHasKey('items', $data); + self::assertArrayHasKey('pagination', $data); + } + + public function testGetAdministratorReturnsData(): void + { + $this->loadFixtures([AdministratorFixture::class]); + $this->authenticatedJsonRequest('get', '/api/v2/administrators/1'); + + $this->assertHttpOkay(); + $data = $this->getDecodedJsonResponseContent(); + self::assertSame('john.doe', $data['login_name']); + } + + public function testGetAdministratorNotFound(): void + { + $this->authenticatedJsonRequest('get', '/api/v2/administrators/999'); + $this->assertHttpNotFound(); + } + + public function testCreateAdministratorWithValidDataReturnsCreated(): void + { + $this->authenticatedJsonRequest('post', '/api/v2/administrators', [], [], [], json_encode([ + 'loginName' => 'new.admin', + 'password' => 'NewPassword123!', + 'email' => 'new.admin@example.com', + ])); + + $this->assertHttpCreated(); + $data = $this->getDecodedJsonResponseContent(); + self::assertSame('new.admin', $data['login_name']); + } + + public function testUpdateAdministratorReturnsOk(): void + { + $this->loadFixtures([AdministratorFixture::class]); + + $this->authenticatedJsonRequest('put', '/api/v2/administrators/1', [], [], [], json_encode([ + 'email' => 'updated@example.com', + ])); + + $this->assertHttpOkay(); + $data = $this->getDecodedJsonResponseContent(); + self::assertSame('updated@example.com', $data['email']); + } + + public function testDeleteAdministratorReturnsNoContent(): void + { + $this->loadFixtures([AdministratorFixture::class]); + + $this->authenticatedJsonRequest('delete', '/api/v2/administrators/1'); + $this->assertHttpNoContent(); + + self::assertNull($this->administratorRepository->find(1)); + } + + public function testDeleteAdministratorNotFound(): void + { + $this->authenticatedJsonRequest('delete', '/api/v2/administrators/99999'); + $this->assertHttpNotFound(); + } + + public function testCreateAdministratorWithInvalidJsonReturns400(): void + { + $this->authenticatedJsonRequest('post', '/api/v2/administrators', [], [], [], 'not json'); + $this->assertHttpBadRequest(); + } + + public function testCreateAdministratorWithMissingFieldsReturns422(): void + { + $this->authenticatedJsonRequest('post', '/api/v2/administrators', [], [], [], json_encode([])); + $this->assertHttpUnprocessableEntity(); + } + + public function testPutAdministratorWithInvalidIdReturns404(): void + { + $this->authenticatedJsonRequest('put', '/api/v2/administrators/9999', [], [], [], json_encode([ + 'email' => 'example@example.com' + ])); + + $this->assertHttpNotFound(); + } +} diff --git a/tests/Integration/Controller/SessionControllerTest.php b/tests/Integration/Identity/Controller/SessionControllerTest.php similarity index 93% rename from tests/Integration/Controller/SessionControllerTest.php rename to tests/Integration/Identity/Controller/SessionControllerTest.php index eb7c8a7..5f8b2b0 100644 --- a/tests/Integration/Controller/SessionControllerTest.php +++ b/tests/Integration/Identity/Controller/SessionControllerTest.php @@ -2,14 +2,15 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Integration\Controller; +namespace PhpList\RestBundle\Tests\Integration\Identity\Controller; use DateTime; -use PhpList\Core\Domain\Model\Identity\AdministratorToken; -use PhpList\Core\Domain\Repository\Identity\AdministratorTokenRepository; -use PhpList\RestBundle\Controller\SessionController; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Identity\AdministratorFixture; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Identity\AdministratorTokenFixture; +use PhpList\Core\Domain\Identity\Model\AdministratorToken; +use PhpList\Core\Domain\Identity\Repository\AdministratorTokenRepository; +use PhpList\RestBundle\Identity\Controller\SessionController; +use PhpList\RestBundle\Tests\Integration\Common\AbstractTestController; +use PhpList\RestBundle\Tests\Integration\Identity\Fixtures\AdministratorFixture; +use PhpList\RestBundle\Tests\Integration\Identity\Fixtures\AdministratorTokenFixture; /** * Testcase. diff --git a/tests/Integration/Identity/Fixtures/AdminAttributeDefinition.csv b/tests/Integration/Identity/Fixtures/AdminAttributeDefinition.csv new file mode 100644 index 0000000..52c942a --- /dev/null +++ b/tests/Integration/Identity/Fixtures/AdminAttributeDefinition.csv @@ -0,0 +1,14 @@ +id,name,type,list_order,default_value,required,table_name +1,"Test Attribute","text",1,"default",1,"test_table" +2,"Original Attribute","text",2,"",0,"" +3,"To Be Deleted","text",3,"",0,"" +4,"Attribute 1","text",4,"",0,"" +5,"Attribute 2","text",5,"",0,"" +6,"Test Get Attribute","text",6,"",0,"" +7,"Update Test Attribute","text",7,"",0,"" +8,"Delete Test Attribute","text",8,"",0,"" +9,"Paginated Test 1","text",9,"",0,"" +10,"Paginated Test 2","text",10,"",0,"" +11,"Get Test Attribute","text",11,"",0,"" +12,"Invalid JSON Test","text",12,"",0,"" +13,"Invalid Admin Test","text",13,"",0,"" diff --git a/tests/Integration/Identity/Fixtures/AdminAttributeDefinitionFixture.php b/tests/Integration/Identity/Fixtures/AdminAttributeDefinitionFixture.php new file mode 100644 index 0000000..f03bc3f --- /dev/null +++ b/tests/Integration/Identity/Fixtures/AdminAttributeDefinitionFixture.php @@ -0,0 +1,52 @@ +setSubjectId($definition, (int)$row['id']); + $definition->setType($row['type']); + $definition->setListOrder((int)$row['list_order']); + $definition->setDefaultValue($row['default_value']); + $definition->setRequired((bool)$row['required']); + $definition->setTableName($row['table_name']); + + $manager->persist($definition); + } while (true); + + fclose($handle); + } +} diff --git a/tests/Integration/Identity/Fixtures/AdminAttributeValue.csv b/tests/Integration/Identity/Fixtures/AdminAttributeValue.csv new file mode 100644 index 0000000..a900d86 --- /dev/null +++ b/tests/Integration/Identity/Fixtures/AdminAttributeValue.csv @@ -0,0 +1,7 @@ +admin_id,definition_id,value +1,1,"test value" +1,7,"original value" +1,8,"to be deleted" +1,9,"value 1" +1,10,"value 2" +1,11,"test get value" diff --git a/tests/Integration/Identity/Fixtures/AdminAttributeValueFixture.php b/tests/Integration/Identity/Fixtures/AdminAttributeValueFixture.php new file mode 100644 index 0000000..3f829be --- /dev/null +++ b/tests/Integration/Identity/Fixtures/AdminAttributeValueFixture.php @@ -0,0 +1,72 @@ +getRepository(Administrator::class); + $definitionRepository = $manager->getRepository(AdminAttributeDefinition::class); + + do { + $data = fgetcsv($handle); + if ($data === false) { + break; + } + $row = array_combine($headers, $data); + + $admin = $adminRepository->find($row['admin_id']); + if ($admin === null) { + throw new RuntimeException(sprintf('Administrator with ID %d not found.', $row['admin_id'])); + } + + $definition = $definitionRepository->find($row['definition_id']); + if ($definition === null) { + throw new RuntimeException( + sprintf('AdminAttributeDefinition with ID %d not found.', $row['definition_id']) + ); + } + + $value = new AdminAttributeValue($definition, $admin); + $value->setValue($row['value']); + + $manager->persist($value); + } while (true); + + fclose($handle); + } + + public function getDependencies(): array + { + return [ + AdministratorFixture::class, + AdminAttributeDefinitionFixture::class, + ]; + } +} diff --git a/tests/Integration/Controller/Fixtures/Identity/Administrator.csv b/tests/Integration/Identity/Fixtures/Administrator.csv similarity index 100% rename from tests/Integration/Controller/Fixtures/Identity/Administrator.csv rename to tests/Integration/Identity/Fixtures/Administrator.csv diff --git a/tests/Integration/Controller/Fixtures/Identity/AdministratorFixture.php b/tests/Integration/Identity/Fixtures/AdministratorFixture.php similarity index 92% rename from tests/Integration/Controller/Fixtures/Identity/AdministratorFixture.php rename to tests/Integration/Identity/Fixtures/AdministratorFixture.php index 282c79c..16be665 100644 --- a/tests/Integration/Controller/Fixtures/Identity/AdministratorFixture.php +++ b/tests/Integration/Identity/Fixtures/AdministratorFixture.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Identity; +namespace PhpList\RestBundle\Tests\Integration\Identity\Fixtures; use DateTime; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -use PhpList\Core\Domain\Model\Identity\Administrator; +use PhpList\Core\Domain\Identity\Model\Administrator; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use RuntimeException; diff --git a/tests/Integration/Controller/Fixtures/Identity/AdministratorToken.csv b/tests/Integration/Identity/Fixtures/AdministratorToken.csv similarity index 100% rename from tests/Integration/Controller/Fixtures/Identity/AdministratorToken.csv rename to tests/Integration/Identity/Fixtures/AdministratorToken.csv diff --git a/tests/Integration/Controller/Fixtures/Identity/AdministratorTokenFixture.php b/tests/Integration/Identity/Fixtures/AdministratorTokenFixture.php similarity index 90% rename from tests/Integration/Controller/Fixtures/Identity/AdministratorTokenFixture.php rename to tests/Integration/Identity/Fixtures/AdministratorTokenFixture.php index b22998d..3a47138 100644 --- a/tests/Integration/Controller/Fixtures/Identity/AdministratorTokenFixture.php +++ b/tests/Integration/Identity/Fixtures/AdministratorTokenFixture.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Identity; +namespace PhpList\RestBundle\Tests\Integration\Identity\Fixtures; use DateTime; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Model\Identity\AdministratorToken; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Identity\Model\AdministratorToken; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use RuntimeException; diff --git a/tests/Integration/Controller/CampaignControllerTest.php b/tests/Integration/Messaging/Controller/CampaignControllerTest.php similarity index 85% rename from tests/Integration/Controller/CampaignControllerTest.php rename to tests/Integration/Messaging/Controller/CampaignControllerTest.php index 9949b7c..301dee3 100644 --- a/tests/Integration/Controller/CampaignControllerTest.php +++ b/tests/Integration/Messaging/Controller/CampaignControllerTest.php @@ -2,12 +2,13 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Integration\Controller; +namespace PhpList\RestBundle\Tests\Integration\Messaging\Controller; -use PhpList\RestBundle\Controller\CampaignController; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Identity\AdministratorFixture; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Identity\AdministratorTokenFixture; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Messaging\MessageFixture; +use PhpList\RestBundle\Messaging\Controller\CampaignController; +use PhpList\RestBundle\Tests\Integration\Common\AbstractTestController; +use PhpList\RestBundle\Tests\Integration\Identity\Fixtures\AdministratorFixture; +use PhpList\RestBundle\Tests\Integration\Identity\Fixtures\AdministratorTokenFixture; +use PhpList\RestBundle\Tests\Integration\Messaging\Fixtures\MessageFixture; class CampaignControllerTest extends AbstractTestController { diff --git a/tests/Integration/Controller/TemplateControllerTest.php b/tests/Integration/Messaging/Controller/TemplateControllerTest.php similarity index 88% rename from tests/Integration/Controller/TemplateControllerTest.php rename to tests/Integration/Messaging/Controller/TemplateControllerTest.php index af65097..8dd73cc 100644 --- a/tests/Integration/Controller/TemplateControllerTest.php +++ b/tests/Integration/Messaging/Controller/TemplateControllerTest.php @@ -2,13 +2,14 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Integration\Controller; - -use PhpList\Core\Domain\Repository\Messaging\TemplateRepository; -use PhpList\RestBundle\Controller\TemplateController; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Identity\AdministratorFixture; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Identity\AdministratorTokenFixture; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Messaging\TemplateFixture; +namespace PhpList\RestBundle\Tests\Integration\Messaging\Controller; + +use PhpList\Core\Domain\Messaging\Repository\TemplateRepository; +use PhpList\RestBundle\Messaging\Controller\TemplateController; +use PhpList\RestBundle\Tests\Integration\Common\AbstractTestController; +use PhpList\RestBundle\Tests\Integration\Identity\Fixtures\AdministratorFixture; +use PhpList\RestBundle\Tests\Integration\Identity\Fixtures\AdministratorTokenFixture; +use PhpList\RestBundle\Tests\Integration\Messaging\Fixtures\TemplateFixture; class TemplateControllerTest extends AbstractTestController { diff --git a/tests/Integration/Controller/Fixtures/Messaging/Message.csv b/tests/Integration/Messaging/Fixtures/Message.csv similarity index 100% rename from tests/Integration/Controller/Fixtures/Messaging/Message.csv rename to tests/Integration/Messaging/Fixtures/Message.csv diff --git a/tests/Integration/Controller/Fixtures/Messaging/MessageFixture.php b/tests/Integration/Messaging/Fixtures/MessageFixture.php similarity index 85% rename from tests/Integration/Controller/Fixtures/Messaging/MessageFixture.php rename to tests/Integration/Messaging/Fixtures/MessageFixture.php index 0986b4f..3e6b4a9 100644 --- a/tests/Integration/Controller/Fixtures/Messaging/MessageFixture.php +++ b/tests/Integration/Messaging/Fixtures/MessageFixture.php @@ -2,19 +2,19 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Messaging; +namespace PhpList\RestBundle\Tests\Integration\Messaging\Fixtures; use DateTime; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Model\Messaging\Message; -use PhpList\Core\Domain\Model\Messaging\Message\MessageContent; -use PhpList\Core\Domain\Model\Messaging\Message\MessageFormat; -use PhpList\Core\Domain\Model\Messaging\Message\MessageMetadata; -use PhpList\Core\Domain\Model\Messaging\Message\MessageOptions; -use PhpList\Core\Domain\Model\Messaging\Message\MessageSchedule; -use PhpList\Core\Domain\Model\Messaging\Template; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Messaging\Model\Message; +use PhpList\Core\Domain\Messaging\Model\Message\MessageContent; +use PhpList\Core\Domain\Messaging\Model\Message\MessageFormat; +use PhpList\Core\Domain\Messaging\Model\Message\MessageMetadata; +use PhpList\Core\Domain\Messaging\Model\Message\MessageOptions; +use PhpList\Core\Domain\Messaging\Model\Message\MessageSchedule; +use PhpList\Core\Domain\Messaging\Model\Template; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use RuntimeException; diff --git a/tests/Integration/Controller/Fixtures/Messaging/Template.csv b/tests/Integration/Messaging/Fixtures/Template.csv similarity index 100% rename from tests/Integration/Controller/Fixtures/Messaging/Template.csv rename to tests/Integration/Messaging/Fixtures/Template.csv diff --git a/tests/Integration/Controller/Fixtures/Messaging/TemplateFixture.php b/tests/Integration/Messaging/Fixtures/TemplateFixture.php similarity index 89% rename from tests/Integration/Controller/Fixtures/Messaging/TemplateFixture.php rename to tests/Integration/Messaging/Fixtures/TemplateFixture.php index 8daf6e5..9f26d96 100644 --- a/tests/Integration/Controller/Fixtures/Messaging/TemplateFixture.php +++ b/tests/Integration/Messaging/Fixtures/TemplateFixture.php @@ -1,10 +1,12 @@ get(SubscriberAttributeDefinitionController::class) + ); + } + + public function testGetAttributesWithoutSessionKeyReturnsForbidden() + { + self::getClient()->request('GET', '/api/v2/subscribers/attributes'); + $this->assertHttpForbidden(); + } + + public function testGetAttributesWithSessionKeyReturnsOk() + { + $this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class]); + $this->authenticatedJsonRequest('GET', '/api/v2/subscribers/attributes'); + $this->assertHttpOkay(); + } + + public function testGetAttributeWithInvalidIdReturnsNotFound() + { + $this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class]); + $this->authenticatedJsonRequest('GET', '/api/v2/subscribers/attributes/999'); + $this->assertHttpNotFound(); + } + + public function testCreateAttributeDefinition() + { + $this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class]); + + $payload = json_encode([ + 'name' => 'Country', + 'type' => 'checkbox', + 'order' => 12, + 'default_value' => 'United States', + 'required' => true, + 'table_name' => 'list_attributes', + ]); + + $this->authenticatedJsonRequest('POST', '/api/v2/subscribers/attributes', [], [], [], $payload); + + $this->assertHttpCreated(); + + $response = $this->getDecodedJsonResponseContent(); + self::assertSame('Country', $response['name']); + } + + public function testUpdateAttributeDefinition() + { + $this->loadFixtures([ + AdministratorFixture::class, + AdministratorTokenFixture::class, + SubscriberAttributeDefinitionFixture::class, + ]); + + $payload = json_encode([ + 'name' => 'Updated Country', + 'type' => 'checkbox', + 'order' => 10, + 'default_value' => 'Canada', + 'required' => false, + 'table_name' => 'list_attributes', + ]); + + $this->authenticatedJsonRequest('PUT', '/api/v2/subscribers/attributes/1', [], [], [], $payload); + $this->assertHttpOkay(); + $response = $this->getDecodedJsonResponseContent(); + self::assertSame('Updated Country', $response['name']); + } + + public function testDeleteAttributeDefinition() + { + $this->loadFixtures([ + AdministratorFixture::class, + AdministratorTokenFixture::class, + SubscriberAttributeDefinitionFixture::class, + ]); + + $this->authenticatedJsonRequest('DELETE', '/api/v2/subscribers/attributes/1'); + $this->assertHttpNoContent(); + + $repo = self::getContainer()->get(SubscriberAttributeDefinitionRepository::class); + self::assertNull($repo->find(1)); + } + + public function testCreateAttributeDefinitionMissingNameReturnsValidationError(): void + { + $this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class]); + + $payload = json_encode([ + 'type' => 'text', + 'order' => 1, + 'required' => false + ]); + + $this->authenticatedJsonRequest('POST', '/api/v2/subscribers/attributes', [], [], [], $payload); + $this->assertHttpUnprocessableEntity(); + } +} diff --git a/tests/Integration/Subscription/Controller/SubscriberAttributeValueControllerTest.php b/tests/Integration/Subscription/Controller/SubscriberAttributeValueControllerTest.php new file mode 100644 index 0000000..4b1a84c --- /dev/null +++ b/tests/Integration/Subscription/Controller/SubscriberAttributeValueControllerTest.php @@ -0,0 +1,91 @@ +get(SubscriberAttributeValueController::class) + ); + } + + public function testCreateOrUpdateAttributeValue(): void + { + $this->loadFixtures([ + SubscriberFixture::class, + SubscriberAttributeDefinitionFixture::class, + ]); + + $subscriberId = 1; + $definitionId = 1; + $json = json_encode(['value' => 'Test Country']); + + $this->authenticatedJsonRequest( + 'post', + '/api/v2/subscribers/attribute-values/' . $subscriberId . '/' . $definitionId, + [], + [], + [], + $json + ); + + $this->assertHttpCreated(); + $response = $this->getDecodedJsonResponseContent(); + self::assertSame('Test Country', $response['value']); + } + + public function testDeleteAttributeValue(): void + { + $this->loadFixtures([ + SubscriberFixture::class, + SubscriberAttributeValueFixture::class, + ]); + + $this->authenticatedJsonRequest( + 'delete', + '/api/v2/subscribers/attribute-values/1/1' + ); + + $this->assertHttpNoContent(); + } + + public function testGetPaginatedAttributes(): void + { + $this->loadFixtures([ + SubscriberFixture::class, + SubscriberAttributeDefinitionFixture::class, + ]); + + $this->authenticatedJsonRequest( + 'get', + '/api/v2/subscribers/attribute-values/1' + ); + + $this->assertHttpOkay(); + $response = $this->getDecodedJsonResponseContent(); + self::assertArrayHasKey('items', $response); + self::assertArrayHasKey('pagination', $response); + } + + public function testAttributeValueNotFoundReturns404(): void + { + $this->authenticatedJsonRequest( + 'get', + '/api/v2/subscribers/attribute-values/999/999' + ); + + $this->assertHttpNotFound(); + } +} diff --git a/tests/Integration/Controller/SubscriberControllerTest.php b/tests/Integration/Subscription/Controller/SubscriberControllerTest.php similarity index 92% rename from tests/Integration/Controller/SubscriberControllerTest.php rename to tests/Integration/Subscription/Controller/SubscriberControllerTest.php index 80de32c..240cfa9 100644 --- a/tests/Integration/Controller/SubscriberControllerTest.php +++ b/tests/Integration/Subscription/Controller/SubscriberControllerTest.php @@ -2,12 +2,13 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Integration\Controller; +namespace PhpList\RestBundle\Tests\Integration\Subscription\Controller; -use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; -use PhpList\RestBundle\Controller\SubscriberController; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Subscription\SubscriberFixture; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; +use PhpList\RestBundle\Subscription\Controller\SubscriberController; +use PhpList\RestBundle\Tests\Integration\Common\AbstractTestController; +use PhpList\RestBundle\Tests\Integration\Subscription\Fixtures\SubscriberFixture; /** * Testcase. diff --git a/tests/Integration/Subscription/Controller/SubscriberExportControllerTest.php b/tests/Integration/Subscription/Controller/SubscriberExportControllerTest.php new file mode 100644 index 0000000..39ad2e6 --- /dev/null +++ b/tests/Integration/Subscription/Controller/SubscriberExportControllerTest.php @@ -0,0 +1,109 @@ +get(SubscriberExportController::class) + ); + } + + public function testExportSubscribersWithoutSessionKeyReturnsForbiddenStatus(): void + { + $this->jsonRequest('POST', '/api/v2/subscribers/export'); + + $this->assertHttpForbidden(); + } + + public function testExportSubscribersWithInvalidRequestReturnsUnprocessableEntityStatus(): void + { + $this->authenticatedJsonRequest( + 'POST', + '/api/v2/subscribers/export', + [], + [], + [], + json_encode(['dateType' => 'invalid_type']) + ); + + $this->assertHttpUnprocessableEntity(); + } + + public function testExportSubscribersWithValidRequest(): void + { + $this->authenticatedJsonRequest( + 'POST', + '/api/v2/subscribers/export', + [], + [], + [], + json_encode([ + 'dateType' => 'any', + 'columns' => ['email', 'confirmed', 'blacklisted'] + ]) + ); + + $response = self::getClient()->getResponse(); + self::assertSame(Response::HTTP_OK, $response->getStatusCode()); + self::assertStringContainsString('text/csv', $response->headers->get('Content-Type')); + self::assertStringContainsString( + 'attachment; filename=subscribers_export_', + $response->headers->get('Content-Disposition') + ); + } + + public function testExportSubscribersWithoutListIdFilter(): void + { + $this->authenticatedJsonRequest( + 'POST', + '/api/v2/subscribers/export', + [], + [], + [], + json_encode([ + 'dateType' => 'any', + 'columns' => ['email', 'confirmed', 'blacklisted'] + ]) + ); + + $response = self::getClient()->getResponse(); + self::assertSame(Response::HTTP_OK, $response->getStatusCode()); + self::assertStringContainsString('text/csv', $response->headers->get('Content-Type')); + } + + public function testExportSubscribersWithSpecificColumns(): void + { + $this->authenticatedJsonRequest( + 'POST', + '/api/v2/subscribers/export', + [], + [], + [], + json_encode([ + 'dateType' => 'any', + 'columns' => ['email', 'confirmed'] + ]) + ); + + $response = self::getClient()->getResponse(); + self::assertSame(Response::HTTP_OK, $response->getStatusCode()); + self::assertStringContainsString('text/csv', $response->headers->get('Content-Type')); + } + + public function testGetMethodIsNotAllowed(): void + { + $this->authenticatedJsonRequest('GET', '/api/v2/subscribers/export'); + + $this->assertHttpMethodNotAllowed(); + } +} diff --git a/tests/Integration/Subscription/Controller/SubscriberImportControllerTest.php b/tests/Integration/Subscription/Controller/SubscriberImportControllerTest.php new file mode 100644 index 0000000..8acd62f --- /dev/null +++ b/tests/Integration/Subscription/Controller/SubscriberImportControllerTest.php @@ -0,0 +1,144 @@ +tempDir = sys_get_temp_dir(); + } + + public function testControllerIsAvailableViaContainer(): void + { + self::assertInstanceOf( + SubscriberImportController::class, + self::getContainer()->get(SubscriberImportController::class) + ); + } + + public function testImportSubscribersWithoutSessionKeyReturnsForbiddenStatus(): void + { + self::getClient()->request('POST', '/api/v2/subscribers/import'); + + $this->assertHttpForbidden(); + } + + public function testImportSubscribersWithoutFileReturnsBadRequestStatus(): void + { + $this->authenticatedJsonRequest('POST', '/api/v2/subscribers/import'); + + $this->assertHttpBadRequest(); + $responseContent = $this->getDecodedJsonResponseContent(); + self::assertStringContainsString('No file uploaded', $responseContent['message']); + } + + public function testImportSubscribersWithNonCsvFileReturnsBadRequestStatus(): void + { + $filePath = $this->tempDir . '/test.txt'; + file_put_contents($filePath, 'This is not a CSV file'); + + $file = new UploadedFile( + $filePath, + 'test.txt', + 'text/plain', + null, + true + ); + + $this->authenticatedJsonRequest( + 'POST', + '/api/v2/subscribers/import', + [], + ['file' => $file] + ); + + $this->assertHttpBadRequest(); + $responseContent = $this->getDecodedJsonResponseContent(); + self::assertStringContainsString('File must be a CSV', $responseContent['message']); + } + + public function testImportSubscribersWithValidCsvFile(): void + { + $filePath = $this->tempDir . '/subscribers.csv'; + $csvContent = "email,name\ntest@example.com,Test User\ntest2@example.com,Test User 2"; + file_put_contents($filePath, $csvContent); + + $file = new UploadedFile( + $filePath, + 'subscribers.csv', + 'text/csv', + null, + true + ); + + $this->authenticatedJsonRequest( + 'POST', + '/api/v2/subscribers/import', + [], + ['file' => $file] + ); + + $response = self::getClient()->getResponse(); + self::assertSame(Response::HTTP_OK, $response->getStatusCode()); + + $responseContent = $this->getDecodedJsonResponseContent(); + self::assertArrayHasKey('imported', $responseContent); + self::assertArrayHasKey('skipped', $responseContent); + self::assertArrayHasKey('errors', $responseContent); + } + + public function testImportSubscribersWithOptions(): void + { + $filePath = $this->tempDir . '/subscribers.csv'; + $csvContent = "email,name\ntest@example.com,Test User"; + file_put_contents($filePath, $csvContent); + + $file = new UploadedFile( + $filePath, + 'subscribers.csv', + 'text/csv', + null, + true + ); + + $this->authenticatedJsonRequest( + 'POST', + '/api/v2/subscribers/import', + [ + 'request_confirmation' => 'true', + 'html_email' => 'false' + ], + ['file' => $file] + ); + + $response = self::getClient()->getResponse(); + self::assertSame(Response::HTTP_OK, $response->getStatusCode()); + + $responseContent = $this->getDecodedJsonResponseContent(); + self::assertArrayHasKey('imported', $responseContent); + self::assertArrayHasKey('skipped', $responseContent); + self::assertArrayHasKey('errors', $responseContent); + } + + public function testGetMethodIsNotAllowed(): void + { + $this->authenticatedJsonRequest('GET', '/api/v2/subscribers/import'); + + $this->assertHttpMethodNotAllowed(); + } +} diff --git a/tests/Integration/Controller/ListControllerTest.php b/tests/Integration/Subscription/Controller/SubscriberListControllerTest.php similarity index 88% rename from tests/Integration/Controller/ListControllerTest.php rename to tests/Integration/Subscription/Controller/SubscriberListControllerTest.php index 97dea15..eba6821 100644 --- a/tests/Integration/Controller/ListControllerTest.php +++ b/tests/Integration/Subscription/Controller/SubscriberListControllerTest.php @@ -2,15 +2,16 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Integration\Controller; +namespace PhpList\RestBundle\Tests\Integration\Subscription\Controller; -use PhpList\Core\Domain\Repository\Subscription\SubscriberListRepository; -use PhpList\RestBundle\Controller\ListController; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Identity\AdministratorFixture; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Identity\AdministratorTokenFixture; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Messaging\SubscriberListFixture; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Subscription\SubscriberFixture; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Subscription\SubscriptionFixture; +use PhpList\Core\Domain\Subscription\Repository\SubscriberListRepository; +use PhpList\RestBundle\Subscription\Controller\SubscriberListController; +use PhpList\RestBundle\Tests\Integration\Common\AbstractTestController; +use PhpList\RestBundle\Tests\Integration\Identity\Fixtures\AdministratorFixture; +use PhpList\RestBundle\Tests\Integration\Identity\Fixtures\AdministratorTokenFixture; +use PhpList\RestBundle\Tests\Integration\Subscription\Fixtures\SubscriberFixture; +use PhpList\RestBundle\Tests\Integration\Subscription\Fixtures\SubscriberListFixture; +use PhpList\RestBundle\Tests\Integration\Subscription\Fixtures\SubscriptionFixture; /** * Testcase. @@ -18,11 +19,14 @@ * @author Oliver Klee * @author Xheni Myrtaj */ -class ListControllerTest extends AbstractTestController +class SubscriberListControllerTest extends AbstractTestController { public function testControllerIsAvailableViaContainer() { - self::assertInstanceOf(ListController::class, self::getContainer()->get(ListController::class)); + self::assertInstanceOf( + SubscriberListController::class, + self::getContainer()->get(SubscriberListController::class) + ); } public function testGetListsWithoutSessionKeyReturnsForbiddenStatus() @@ -266,10 +270,12 @@ public function testGetListMembersWithCurrentSessionKeyForExistingListWithSubscr [ 'id' => 2, 'name' => 'More news', - 'description' => '', 'created_at' => '2016-06-22T15:01:17+00:00', + 'description' => '', + 'list_position' => 12, + 'subject_prefix' => '', 'public' => true, - 'subscription_date' => '2016-07-22T15:01:17+00:00', + 'category' => '', ], ], ], [ @@ -286,18 +292,22 @@ public function testGetListMembersWithCurrentSessionKeyForExistingListWithSubscr [ 'id' => 2, 'name' => 'More news', - 'description' => '', 'created_at' => '2016-06-22T15:01:17+00:00', + 'description' => '', + 'list_position' => 12, + 'subject_prefix' => '', 'public' => true, - 'subscription_date' => '2016-08-22T15:01:17+00:00', + 'category' => '', ], [ 'id' => 1, 'name' => 'News', - 'description' => 'News (and some fun stuff)', 'created_at' => '2016-06-22T15:01:17+00:00', + 'description' => 'News (and some fun stuff)', + 'list_position' => 12, + 'subject_prefix' => 'phpList', 'public' => true, - 'subscription_date' => '2016-09-22T15:01:17+00:00', + 'category' => 'news', ], ], ], diff --git a/tests/Integration/Controller/SubscriptionControllerTest.php b/tests/Integration/Subscription/Controller/SubscriptionControllerTest.php similarity index 73% rename from tests/Integration/Controller/SubscriptionControllerTest.php rename to tests/Integration/Subscription/Controller/SubscriptionControllerTest.php index f43d396..a50ec84 100644 --- a/tests/Integration/Controller/SubscriptionControllerTest.php +++ b/tests/Integration/Subscription/Controller/SubscriptionControllerTest.php @@ -2,14 +2,15 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Integration\Controller; +namespace PhpList\RestBundle\Tests\Integration\Subscription\Controller; -use PhpList\RestBundle\Controller\SubscriptionController; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Identity\AdministratorFixture; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Identity\AdministratorTokenFixture; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Messaging\SubscriberListFixture; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Subscription\SubscriberFixture; -use PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Subscription\SubscriptionFixture; +use PhpList\RestBundle\Subscription\Controller\SubscriptionController; +use PhpList\RestBundle\Tests\Integration\Common\AbstractTestController; +use PhpList\RestBundle\Tests\Integration\Identity\Fixtures\AdministratorFixture; +use PhpList\RestBundle\Tests\Integration\Identity\Fixtures\AdministratorTokenFixture; +use PhpList\RestBundle\Tests\Integration\Subscription\Fixtures\SubscriberFixture; +use PhpList\RestBundle\Tests\Integration\Subscription\Fixtures\SubscriberListFixture; +use PhpList\RestBundle\Tests\Integration\Subscription\Fixtures\SubscriptionFixture; class SubscriptionControllerTest extends AbstractTestController { diff --git a/tests/Integration/Controller/Fixtures/Subscription/Subscriber.csv b/tests/Integration/Subscription/Fixtures/Subscriber.csv similarity index 100% rename from tests/Integration/Controller/Fixtures/Subscription/Subscriber.csv rename to tests/Integration/Subscription/Fixtures/Subscriber.csv diff --git a/tests/Integration/Subscription/Fixtures/SubscriberAttributeDefinitionFixture.php b/tests/Integration/Subscription/Fixtures/SubscriberAttributeDefinitionFixture.php new file mode 100644 index 0000000..c3386b4 --- /dev/null +++ b/tests/Integration/Subscription/Fixtures/SubscriberAttributeDefinitionFixture.php @@ -0,0 +1,27 @@ +setName('Country'); + $definition->setType('checkbox'); + $definition->setListOrder(1); + $definition->setDefaultValue('US'); + $definition->setRequired(true); + $definition->setTableName('list_attributes'); + + $manager->persist($definition); + $manager->flush(); + } +} diff --git a/tests/Integration/Subscription/Fixtures/SubscriberAttributeValueFixture.php b/tests/Integration/Subscription/Fixtures/SubscriberAttributeValueFixture.php new file mode 100644 index 0000000..c373e5d --- /dev/null +++ b/tests/Integration/Subscription/Fixtures/SubscriberAttributeValueFixture.php @@ -0,0 +1,36 @@ +setName('Country'); + $definition->setType('checkbox'); + $definition->setListOrder(1); + $definition->setDefaultValue('US'); + $definition->setRequired(true); + $definition->setTableName('list_attributes'); + + $manager->persist($definition); + + $subscriberRepository = $manager->getRepository(Subscriber::class); + $value = new SubscriberAttributeValue($definition, $subscriberRepository->find(1)); + $value->setValue('test value'); + + $manager->persist($value); + + $manager->flush(); + } +} diff --git a/tests/Integration/Controller/Fixtures/Subscription/SubscriberFixture.php b/tests/Integration/Subscription/Fixtures/SubscriberFixture.php similarity index 92% rename from tests/Integration/Controller/Fixtures/Subscription/SubscriberFixture.php rename to tests/Integration/Subscription/Fixtures/SubscriberFixture.php index 7992ea8..6b49d0f 100644 --- a/tests/Integration/Controller/Fixtures/Subscription/SubscriberFixture.php +++ b/tests/Integration/Subscription/Fixtures/SubscriberFixture.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Subscription; +namespace PhpList\RestBundle\Tests\Integration\Subscription\Fixtures; use DateTime; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -use PhpList\Core\Domain\Model\Subscription\Subscriber; +use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use RuntimeException; diff --git a/tests/Integration/Controller/Fixtures/Messaging/SubscriberList.csv b/tests/Integration/Subscription/Fixtures/SubscriberList.csv similarity index 100% rename from tests/Integration/Controller/Fixtures/Messaging/SubscriberList.csv rename to tests/Integration/Subscription/Fixtures/SubscriberList.csv diff --git a/tests/Integration/Controller/Fixtures/Messaging/SubscriberListFixture.php b/tests/Integration/Subscription/Fixtures/SubscriberListFixture.php similarity index 92% rename from tests/Integration/Controller/Fixtures/Messaging/SubscriberListFixture.php rename to tests/Integration/Subscription/Fixtures/SubscriberListFixture.php index 4d743f1..c1c4d11 100644 --- a/tests/Integration/Controller/Fixtures/Messaging/SubscriberListFixture.php +++ b/tests/Integration/Subscription/Fixtures/SubscriberListFixture.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Messaging; +namespace PhpList\RestBundle\Tests\Integration\Subscription\Fixtures; use DateTime; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Model\Subscription\SubscriberList; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use RuntimeException; diff --git a/tests/Integration/Controller/Fixtures/Subscription/Subscription.csv b/tests/Integration/Subscription/Fixtures/Subscription.csv similarity index 100% rename from tests/Integration/Controller/Fixtures/Subscription/Subscription.csv rename to tests/Integration/Subscription/Fixtures/Subscription.csv diff --git a/tests/Integration/Controller/Fixtures/Subscription/SubscriptionFixture.php b/tests/Integration/Subscription/Fixtures/SubscriptionFixture.php similarity index 87% rename from tests/Integration/Controller/Fixtures/Subscription/SubscriptionFixture.php rename to tests/Integration/Subscription/Fixtures/SubscriptionFixture.php index a87eed3..8ff65f3 100644 --- a/tests/Integration/Controller/Fixtures/Subscription/SubscriptionFixture.php +++ b/tests/Integration/Subscription/Fixtures/SubscriptionFixture.php @@ -2,14 +2,14 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Integration\Controller\Fixtures\Subscription; +namespace PhpList\RestBundle\Tests\Integration\Subscription\Fixtures; use DateTime; use Doctrine\Bundle\FixturesBundle\Fixture; use Doctrine\Persistence\ObjectManager; -use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Model\Subscription\SubscriberList; -use PhpList\Core\Domain\Model\Subscription\Subscription; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; +use PhpList\Core\Domain\Subscription\Model\Subscription; use PhpList\Core\TestingSupport\Traits\ModelTestTrait; use RuntimeException; diff --git a/tests/Integration/ViewHandler/SecuredViewHandlerTest.php b/tests/Integration/ViewHandler/SecuredViewHandlerTest.php index fbe0cc7..0b3c0cc 100644 --- a/tests/Integration/ViewHandler/SecuredViewHandlerTest.php +++ b/tests/Integration/ViewHandler/SecuredViewHandlerTest.php @@ -4,7 +4,7 @@ namespace PhpList\RestBundle\Tests\Integration\ViewHandler; -use PhpList\RestBundle\Tests\Integration\Controller\AbstractTestController; +use PhpList\RestBundle\Tests\Integration\Common\AbstractTestController; /** * Test for security headers diff --git a/tests/Unit/Serializer/CursorPaginationNormalizerTest.php b/tests/Unit/Common/Serializer/CursorPaginationNormalizerTest.php similarity index 91% rename from tests/Unit/Serializer/CursorPaginationNormalizerTest.php rename to tests/Unit/Common/Serializer/CursorPaginationNormalizerTest.php index 49054e7..61495c6 100644 --- a/tests/Unit/Serializer/CursorPaginationNormalizerTest.php +++ b/tests/Unit/Common/Serializer/CursorPaginationNormalizerTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Unit\Serializer; +namespace PhpList\RestBundle\Tests\Unit\Common\Serializer; -use PhpList\RestBundle\Entity\Dto\CursorPaginationResult; -use PhpList\RestBundle\Serializer\CursorPaginationNormalizer; +use PhpList\RestBundle\Common\Dto\CursorPaginationResult; +use PhpList\RestBundle\Common\Serializer\CursorPaginationNormalizer; use PHPUnit\Framework\TestCase; class CursorPaginationNormalizerTest extends TestCase diff --git a/tests/Unit/Service/Factory/PaginationCursorRequestFactoryTest.php b/tests/Unit/Common/Service/Factory/PaginationCursorRequestFactoryTest.php similarity index 90% rename from tests/Unit/Service/Factory/PaginationCursorRequestFactoryTest.php rename to tests/Unit/Common/Service/Factory/PaginationCursorRequestFactoryTest.php index 1cb6b35..cc969b6 100644 --- a/tests/Unit/Service/Factory/PaginationCursorRequestFactoryTest.php +++ b/tests/Unit/Common/Service/Factory/PaginationCursorRequestFactoryTest.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Unit\Service\Factory; +namespace PhpList\RestBundle\Tests\Unit\Common\Service\Factory; -use PhpList\RestBundle\Service\Factory\PaginationCursorRequestFactory; +use PhpList\RestBundle\Common\Service\Factory\PaginationCursorRequestFactory; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; diff --git a/tests/Unit/Service/Provider/PaginatedDataProviderTest.php b/tests/Unit/Common/Service/Provider/PaginatedDataProviderTest.php similarity index 89% rename from tests/Unit/Service/Provider/PaginatedDataProviderTest.php rename to tests/Unit/Common/Service/Provider/PaginatedDataProviderTest.php index 1175354..ed05555 100644 --- a/tests/Unit/Service/Provider/PaginatedDataProviderTest.php +++ b/tests/Unit/Common/Service/Provider/PaginatedDataProviderTest.php @@ -2,20 +2,20 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Unit\Service\Provider; +namespace PhpList\RestBundle\Tests\Unit\Common\Service\Provider; use Doctrine\ORM\EntityManagerInterface; -use PhpList\RestBundle\Entity\Dto\CursorPaginationResult; -use PhpList\RestBundle\Entity\Request\PaginationCursorRequest; -use PhpList\RestBundle\Service\Factory\PaginationCursorRequestFactory; -use PhpList\RestBundle\Service\Provider\PaginatedDataProvider; -use PhpList\RestBundle\Serializer\CursorPaginationNormalizer; +use PhpList\RestBundle\Common\Dto\CursorPaginationResult; +use PhpList\RestBundle\Common\Request\PaginationCursorRequest; +use PhpList\RestBundle\Common\Serializer\CursorPaginationNormalizer; +use PhpList\RestBundle\Common\Service\Factory\PaginationCursorRequestFactory; +use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider; use PhpList\RestBundle\Tests\Helpers\DummyPaginatableRepository; use PhpList\RestBundle\Tests\Helpers\DummyRepository; use PHPUnit\Framework\TestCase; +use RuntimeException; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; -use RuntimeException; class PaginatedDataProviderTest extends TestCase { diff --git a/tests/Unit/Validator/RequestValidatorTest.php b/tests/Unit/Common/Validator/RequestValidatorTest.php similarity index 96% rename from tests/Unit/Validator/RequestValidatorTest.php rename to tests/Unit/Common/Validator/RequestValidatorTest.php index d8ed2f3..ce9adef 100644 --- a/tests/Unit/Validator/RequestValidatorTest.php +++ b/tests/Unit/Common/Validator/RequestValidatorTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Unit\Validator; +namespace PhpList\RestBundle\Tests\Unit\Common\Validator; -use PhpList\RestBundle\Entity\Request\RequestInterface; +use PhpList\RestBundle\Common\Request\RequestInterface; +use PhpList\RestBundle\Common\Validator\RequestValidator; use PhpList\RestBundle\Tests\Helpers\DummyRequestDto; -use PhpList\RestBundle\Validator\RequestValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; diff --git a/tests/Unit/Identity/Request/CreateAdministratorRequestTest.php b/tests/Unit/Identity/Request/CreateAdministratorRequestTest.php new file mode 100644 index 0000000..6d43406 --- /dev/null +++ b/tests/Unit/Identity/Request/CreateAdministratorRequestTest.php @@ -0,0 +1,42 @@ +loginName = 'testuser'; + $request->password = 'password123'; + $request->email = 'test@example.com'; + $request->superUser = true; + + $dto = $request->getDto(); + + $this->assertEquals('testuser', $dto->loginName); + $this->assertEquals('password123', $dto->password); + $this->assertEquals('test@example.com', $dto->email); + $this->assertTrue($dto->isSuperUser); + } + + public function testGetDtoWithDefaultSuperUserValue(): void + { + $request = new CreateAdministratorRequest(); + $request->loginName = 'testuser'; + $request->password = 'password123'; + $request->email = 'test@example.com'; + + $dto = $request->getDto(); + + $this->assertEquals('testuser', $dto->loginName); + $this->assertEquals('password123', $dto->password); + $this->assertEquals('test@example.com', $dto->email); + $this->assertFalse($dto->isSuperUser); + } +} diff --git a/tests/Unit/Identity/Request/CreateAttributeDefinitionRequestTest.php b/tests/Unit/Identity/Request/CreateAttributeDefinitionRequestTest.php new file mode 100644 index 0000000..8246938 --- /dev/null +++ b/tests/Unit/Identity/Request/CreateAttributeDefinitionRequestTest.php @@ -0,0 +1,49 @@ +name = 'Test Attribute'; + $request->type = 'text'; + $request->order = 5; + $request->defaultValue = 'default'; + $request->required = true; + $request->tableName = 'test_table'; + + $dto = $request->getDto(); + + $this->assertInstanceOf(AdminAttributeDefinitionDto::class, $dto); + $this->assertEquals('Test Attribute', $dto->name); + $this->assertEquals('text', $dto->type); + $this->assertEquals(5, $dto->listOrder); + $this->assertEquals('default', $dto->defaultValue); + $this->assertTrue($dto->required); + $this->assertEquals('test_table', $dto->tableName); + } + + public function testGetDtoWithDefaultValues(): void + { + $request = new CreateAttributeDefinitionRequest(); + $request->name = 'Test Attribute'; + + $dto = $request->getDto(); + + $this->assertInstanceOf(AdminAttributeDefinitionDto::class, $dto); + $this->assertEquals('Test Attribute', $dto->name); + $this->assertNull($dto->type); + $this->assertNull($dto->listOrder); + $this->assertNull($dto->defaultValue); + $this->assertFalse($dto->required); + $this->assertNull($dto->tableName); + } +} diff --git a/tests/Unit/Identity/Request/CreateSessionRequestTest.php b/tests/Unit/Identity/Request/CreateSessionRequestTest.php new file mode 100644 index 0000000..9f9580d --- /dev/null +++ b/tests/Unit/Identity/Request/CreateSessionRequestTest.php @@ -0,0 +1,24 @@ +loginName = 'testuser'; + $request->password = 'password123'; + + $dto = $request->getDto(); + + $this->assertSame($request, $dto); + $this->assertEquals('testuser', $dto->loginName); + $this->assertEquals('password123', $dto->password); + } +} diff --git a/tests/Unit/Identity/Request/UpdateAdministratorRequestTest.php b/tests/Unit/Identity/Request/UpdateAdministratorRequestTest.php new file mode 100644 index 0000000..5bdbc1d --- /dev/null +++ b/tests/Unit/Identity/Request/UpdateAdministratorRequestTest.php @@ -0,0 +1,44 @@ +administratorId = 123; + $request->loginName = 'testuser'; + $request->password = 'password123'; + $request->email = 'test@example.com'; + $request->superAdmin = true; + + $dto = $request->getDto(); + + $this->assertEquals(123, $dto->administratorId); + $this->assertEquals('testuser', $dto->loginName); + $this->assertEquals('password123', $dto->password); + $this->assertEquals('test@example.com', $dto->email); + $this->assertTrue($dto->superAdmin); + } + + public function testGetDtoWithNullValues(): void + { + $request = new UpdateAdministratorRequest(); + $request->administratorId = 456; + + $dto = $request->getDto(); + + $this->assertEquals(456, $dto->administratorId); + $this->assertNull($dto->loginName); + $this->assertNull($dto->password); + $this->assertNull($dto->email); + $this->assertNull($dto->superAdmin); + } +} diff --git a/tests/Unit/Identity/Request/UpdateAttributeDefinitionRequestTest.php b/tests/Unit/Identity/Request/UpdateAttributeDefinitionRequestTest.php new file mode 100644 index 0000000..41ea27e --- /dev/null +++ b/tests/Unit/Identity/Request/UpdateAttributeDefinitionRequestTest.php @@ -0,0 +1,49 @@ +name = 'Updated Attribute'; + $request->type = 'checkbox'; + $request->order = 10; + $request->defaultValue = 'updated_default'; + $request->required = true; + $request->tableName = 'updated_table'; + + $dto = $request->getDto(); + + $this->assertInstanceOf(AdminAttributeDefinitionDto::class, $dto); + $this->assertEquals('Updated Attribute', $dto->name); + $this->assertEquals('checkbox', $dto->type); + $this->assertEquals(10, $dto->listOrder); + $this->assertEquals('updated_default', $dto->defaultValue); + $this->assertTrue($dto->required); + $this->assertEquals('updated_table', $dto->tableName); + } + + public function testGetDtoWithDefaultValues(): void + { + $request = new UpdateAttributeDefinitionRequest(); + $request->name = 'Updated Attribute'; + + $dto = $request->getDto(); + + $this->assertInstanceOf(AdminAttributeDefinitionDto::class, $dto); + $this->assertEquals('Updated Attribute', $dto->name); + $this->assertNull($dto->type); + $this->assertNull($dto->listOrder); + $this->assertNull($dto->defaultValue); + $this->assertFalse($dto->required); + $this->assertNull($dto->tableName); + } +} diff --git a/tests/Unit/Identity/Serializer/AdminAttributeDefinitionNormalizerTest.php b/tests/Unit/Identity/Serializer/AdminAttributeDefinitionNormalizerTest.php new file mode 100644 index 0000000..f7ad295 --- /dev/null +++ b/tests/Unit/Identity/Serializer/AdminAttributeDefinitionNormalizerTest.php @@ -0,0 +1,56 @@ +createMock(AdminAttributeDefinition::class); + $definition->method('getId')->willReturn(123); + $definition->method('getName')->willReturn('test_attribute'); + $definition->method('getType')->willReturn('text'); + $definition->method('getListOrder')->willReturn(5); + $definition->method('getDefaultValue')->willReturn('default'); + $definition->method('isRequired')->willReturn(true); + $definition->method('getTableName')->willReturn('test_table'); + + $normalizer = new AdminAttributeDefinitionNormalizer(); + $data = $normalizer->normalize($definition); + + $this->assertIsArray($data); + $this->assertEquals([ + 'id' => 123, + 'name' => 'test_attribute', + 'type' => 'text', + 'list_order' => 5, + 'default_value' => 'default', + 'required' => true, + 'table_name' => 'test_table', + ], $data); + } + + public function testNormalizeWithInvalidObjectReturnsEmptyArray(): void + { + $normalizer = new AdminAttributeDefinitionNormalizer(); + $data = $normalizer->normalize(new \stdClass()); + + $this->assertIsArray($data); + $this->assertEmpty($data); + } + + public function testSupportsNormalization(): void + { + $normalizer = new AdminAttributeDefinitionNormalizer(); + + $definition = $this->createMock(AdminAttributeDefinition::class); + $this->assertTrue($normalizer->supportsNormalization($definition)); + $this->assertFalse($normalizer->supportsNormalization(new \stdClass())); + } +} diff --git a/tests/Unit/Identity/Serializer/AdminAttributeValueNormalizerTest.php b/tests/Unit/Identity/Serializer/AdminAttributeValueNormalizerTest.php new file mode 100644 index 0000000..3299cd7 --- /dev/null +++ b/tests/Unit/Identity/Serializer/AdminAttributeValueNormalizerTest.php @@ -0,0 +1,103 @@ +adminNormalizer = $this->createMock(AdministratorNormalizer::class); + $this->definitionNormalizer = $this->createMock(AdminAttributeDefinitionNormalizer::class); + $this->normalizer = new AdminAttributeValueNormalizer( + $this->definitionNormalizer, + $this->adminNormalizer + ); + } + + public function testNormalizeReturnsExpectedArray(): void + { + $admin = $this->createMock(Administrator::class); + $definition = $this->createMock(AdminAttributeDefinition::class); + $definition->method('getDefaultValue')->willReturn('default_value'); + + $attributeValue = $this->createMock(AdminAttributeValue::class); + $attributeValue->method('getAdministrator')->willReturn($admin); + $attributeValue->method('getAttributeDefinition')->willReturn($definition); + $attributeValue->method('getValue')->willReturn('test_value'); + + $this->adminNormalizer->method('normalize')->willReturn(['id' => 1, 'login_name' => 'admin']); + $this->definitionNormalizer->method('normalize')->willReturn(['id' => 2, 'name' => 'test_attribute']); + + $data = $this->normalizer->normalize($attributeValue); + + $this->assertIsArray($data); + $this->assertEquals([ + 'administrator' => ['id' => 1, 'login_name' => 'admin'], + 'definition' => ['id' => 2, 'name' => 'test_attribute'], + 'value' => 'test_value', + ], $data); + } + + public function testNormalizeUsesDefaultValueWhenValueIsNull(): void + { + $admin = $this->createMock(Administrator::class); + $definition = $this->createMock(AdminAttributeDefinition::class); + $definition->method('getDefaultValue')->willReturn('default_value'); + + $attributeValue = $this->createMock(AdminAttributeValue::class); + $attributeValue->method('getAdministrator')->willReturn($admin); + $attributeValue->method('getAttributeDefinition')->willReturn($definition); + $attributeValue->method('getValue')->willReturn(null); + + $this->adminNormalizer->method('normalize')->willReturn(['id' => 1, 'login_name' => 'admin']); + $this->definitionNormalizer->method('normalize')->willReturn(['id' => 2, 'name' => 'test_attribute']); + + $data = $this->normalizer->normalize($attributeValue); + + $this->assertIsArray($data); + $this->assertEquals([ + 'administrator' => ['id' => 1, 'login_name' => 'admin'], + 'definition' => ['id' => 2, 'name' => 'test_attribute'], + 'value' => 'default_value', + ], $data); + } + + public function testNormalizeWithInvalidObjectReturnsEmptyArray(): void + { + $data = $this->normalizer->normalize(new \stdClass()); + + $this->assertIsArray($data); + $this->assertEmpty($data); + } + + public function testSupportsNormalization(): void + { + $attributeValue = $this->createMock(AdminAttributeValue::class); + $this->assertTrue($this->normalizer->supportsNormalization($attributeValue)); + $this->assertFalse($this->normalizer->supportsNormalization(new \stdClass())); + } +} diff --git a/tests/Unit/Serializer/AdministratorNormalizerTest.php b/tests/Unit/Identity/Serializer/AdministratorNormalizerTest.php similarity index 90% rename from tests/Unit/Serializer/AdministratorNormalizerTest.php rename to tests/Unit/Identity/Serializer/AdministratorNormalizerTest.php index adcb4c9..0fa075a 100644 --- a/tests/Unit/Serializer/AdministratorNormalizerTest.php +++ b/tests/Unit/Identity/Serializer/AdministratorNormalizerTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Unit\Serializer; +namespace PhpList\RestBundle\Tests\Unit\Identity\Serializer; use DateTime; use InvalidArgumentException; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\RestBundle\Serializer\AdministratorNormalizer; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\RestBundle\Identity\Serializer\AdministratorNormalizer; use PHPUnit\Framework\TestCase; class AdministratorNormalizerTest extends TestCase diff --git a/tests/Unit/Serializer/AdministratorTokenNormalizerTest.php b/tests/Unit/Identity/Serializer/AdministratorTokenNormalizerTest.php similarity index 87% rename from tests/Unit/Serializer/AdministratorTokenNormalizerTest.php rename to tests/Unit/Identity/Serializer/AdministratorTokenNormalizerTest.php index 5e1da82..1a9b534 100644 --- a/tests/Unit/Serializer/AdministratorTokenNormalizerTest.php +++ b/tests/Unit/Identity/Serializer/AdministratorTokenNormalizerTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Unit\Serializer; +namespace PhpList\RestBundle\Tests\Unit\Identity\Serializer; use DateTime; -use PhpList\Core\Domain\Model\Identity\AdministratorToken; -use PhpList\RestBundle\Serializer\AdministratorTokenNormalizer; +use PhpList\Core\Domain\Identity\Model\AdministratorToken; +use PhpList\RestBundle\Identity\Serializer\AdministratorTokenNormalizer; use PHPUnit\Framework\TestCase; class AdministratorTokenNormalizerTest extends TestCase diff --git a/tests/Unit/Validator/Constraint/UniqueEmailValidatorTest.php b/tests/Unit/Identity/Vallidator/Constraint/UniqueEmailValidatorTest.php similarity index 93% rename from tests/Unit/Validator/Constraint/UniqueEmailValidatorTest.php rename to tests/Unit/Identity/Vallidator/Constraint/UniqueEmailValidatorTest.php index 4645c0e..5f0ab2f 100644 --- a/tests/Unit/Validator/Constraint/UniqueEmailValidatorTest.php +++ b/tests/Unit/Identity/Vallidator/Constraint/UniqueEmailValidatorTest.php @@ -2,13 +2,13 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Unit\Validator\Constraint; +namespace PhpList\RestBundle\Tests\Unit\Identity\Vallidator\Constraint; use Doctrine\ORM\EntityManagerInterface; -use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; -use PhpList\RestBundle\Validator\Constraint\UniqueEmail; -use PhpList\RestBundle\Validator\Constraint\UniqueEmailValidator; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; +use PhpList\RestBundle\Subscription\Validator\Constraint\UniqueEmail; +use PhpList\RestBundle\Subscription\Validator\Constraint\UniqueEmailValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\Exception\ConflictHttpException; diff --git a/tests/Unit/Validator/Constraint/UniqueLoginNameValidatorTest.php b/tests/Unit/Identity/Vallidator/Constraint/UniqueLoginNameValidatorTest.php similarity index 90% rename from tests/Unit/Validator/Constraint/UniqueLoginNameValidatorTest.php rename to tests/Unit/Identity/Vallidator/Constraint/UniqueLoginNameValidatorTest.php index af86b85..897a7fd 100644 --- a/tests/Unit/Validator/Constraint/UniqueLoginNameValidatorTest.php +++ b/tests/Unit/Identity/Vallidator/Constraint/UniqueLoginNameValidatorTest.php @@ -4,13 +4,13 @@ namespace PhpList\RestBundle\Tests\Unit\Validator\Constraint; -use PhpList\Core\Domain\Model\Identity\Administrator; -use PhpList\Core\Domain\Repository\Identity\AdministratorRepository; -use PhpList\RestBundle\Validator\Constraint\UniqueLoginName; -use PhpList\RestBundle\Validator\Constraint\UniqueLoginNameValidator; +use PhpList\Core\Domain\Identity\Model\Administrator; +use PhpList\Core\Domain\Identity\Repository\AdministratorRepository; +use PhpList\RestBundle\Identity\Validator\Constraint\UniqueLoginName; +use PhpList\RestBundle\Identity\Validator\Constraint\UniqueLoginNameValidator; use PHPUnit\Framework\TestCase; -use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\HttpKernel\Exception\ConflictHttpException; +use Symfony\Component\Validator\Context\ExecutionContextInterface; class UniqueLoginNameValidatorTest extends TestCase { diff --git a/tests/Unit/Messaging/Request/CreateMessageRequestTest.php b/tests/Unit/Messaging/Request/CreateMessageRequestTest.php new file mode 100644 index 0000000..be0ff32 --- /dev/null +++ b/tests/Unit/Messaging/Request/CreateMessageRequestTest.php @@ -0,0 +1,92 @@ +contentDto = $this->createMock(MessageContentDto::class); + $this->formatDto = $this->createMock(MessageFormatDto::class); + $this->metadataDto = $this->createMock(MessageMetadataDto::class); + $this->optionsDto = $this->createMock(MessageOptionsDto::class); + $this->scheduleDto = $this->createMock(MessageScheduleDto::class); + + $contentRequest = $this->createMock(MessageContentRequest::class); + $contentRequest->method('getDto')->willReturn($this->contentDto); + + $formatRequest = $this->createMock(MessageFormatRequest::class); + $formatRequest->method('getDto')->willReturn($this->formatDto); + + $metadataRequest = $this->createMock(MessageMetadataRequest::class); + $metadataRequest->method('getDto')->willReturn($this->metadataDto); + + $optionsRequest = $this->createMock(MessageOptionsRequest::class); + $optionsRequest->method('getDto')->willReturn($this->optionsDto); + + $scheduleRequest = $this->createMock(MessageScheduleRequest::class); + $scheduleRequest->method('getDto')->willReturn($this->scheduleDto); + + $this->request = new CreateMessageRequest(); + $this->request->content = $contentRequest; + $this->request->format = $formatRequest; + $this->request->metadata = $metadataRequest; + $this->request->options = $optionsRequest; + $this->request->schedule = $scheduleRequest; + } + + public function testGetDtoReturnsCorrectDto(): void + { + $this->request->templateId = 123; + + $dto = $this->request->getDto(); + + $this->assertInstanceOf(CreateMessageDto::class, $dto); + $this->assertSame($this->contentDto, $dto->content); + $this->assertSame($this->formatDto, $dto->format); + $this->assertSame($this->metadataDto, $dto->metadata); + $this->assertSame($this->optionsDto, $dto->options); + $this->assertSame($this->scheduleDto, $dto->schedule); + $this->assertEquals(123, $dto->templateId); + } + + public function testGetDtoWithNullTemplateId(): void + { + $this->request->templateId = null; + + $dto = $this->request->getDto(); + + $this->assertInstanceOf(CreateMessageDto::class, $dto); + $this->assertSame($this->contentDto, $dto->content); + $this->assertSame($this->formatDto, $dto->format); + $this->assertSame($this->metadataDto, $dto->metadata); + $this->assertSame($this->optionsDto, $dto->options); + $this->assertSame($this->scheduleDto, $dto->schedule); + $this->assertNull($dto->templateId); + } +} diff --git a/tests/Unit/Messaging/Request/CreateTemplateRequestTest.php b/tests/Unit/Messaging/Request/CreateTemplateRequestTest.php new file mode 100644 index 0000000..8b2b847 --- /dev/null +++ b/tests/Unit/Messaging/Request/CreateTemplateRequestTest.php @@ -0,0 +1,80 @@ +request = new CreateTemplateRequest(); + } + + public function testGetDtoReturnsCorrectDto(): void + { + $this->request->title = 'Test Template'; + $this->request->content = 'Test content with [CONTENT] placeholder'; + $this->request->text = 'Plain text with [TEXT] placeholder'; + $this->request->checkLinks = true; + $this->request->checkImages = true; + $this->request->checkExternalImages = true; + + $dto = $this->request->getDto(); + + $this->assertInstanceOf(CreateTemplateDto::class, $dto); + $this->assertEquals('Test Template', $dto->title); + $this->assertEquals('Test content with [CONTENT] placeholder', $dto->content); + $this->assertEquals('Plain text with [TEXT] placeholder', $dto->text); + $this->assertNull($dto->fileContent); + $this->assertTrue($dto->shouldCheckLinks); + $this->assertTrue($dto->shouldCheckImages); + $this->assertTrue($dto->shouldCheckExternalImages); + } + + public function testGetDtoWithDefaultValues(): void + { + $this->request->title = 'Test Template'; + $this->request->content = 'Test content with [CONTENT] placeholder'; + + $dto = $this->request->getDto(); + + $this->assertInstanceOf(CreateTemplateDto::class, $dto); + $this->assertEquals('Test Template', $dto->title); + $this->assertEquals('Test content with [CONTENT] placeholder', $dto->content); + $this->assertNull($dto->text); + $this->assertNull($dto->fileContent); + $this->assertFalse($dto->shouldCheckLinks); + $this->assertFalse($dto->shouldCheckImages); + $this->assertFalse($dto->shouldCheckExternalImages); + } + + public function testGetDtoWithUploadedFile(): void + { + $tempFile = tempnam(sys_get_temp_dir(), 'test'); + file_put_contents($tempFile, 'Test file content'); + + $uploadedFile = $this->createMock(UploadedFile::class); + $uploadedFile->method('getPathname')->willReturn($tempFile); + + $this->request->title = 'Test Template'; + $this->request->content = 'Test content with [CONTENT] placeholder'; + $this->request->file = $uploadedFile; + + $dto = $this->request->getDto(); + + $this->assertInstanceOf(CreateTemplateDto::class, $dto); + $this->assertEquals('Test Template', $dto->title); + $this->assertEquals('Test content with [CONTENT] placeholder', $dto->content); + $this->assertEquals('Test file content', $dto->fileContent); + + unlink($tempFile); + } +} diff --git a/tests/Unit/Messaging/Request/UpdateMessageRequestTest.php b/tests/Unit/Messaging/Request/UpdateMessageRequestTest.php new file mode 100644 index 0000000..1b24254 --- /dev/null +++ b/tests/Unit/Messaging/Request/UpdateMessageRequestTest.php @@ -0,0 +1,95 @@ +contentDto = $this->createMock(MessageContentDto::class); + $this->formatDto = $this->createMock(MessageFormatDto::class); + $this->metadataDto = $this->createMock(MessageMetadataDto::class); + $this->optionsDto = $this->createMock(MessageOptionsDto::class); + $this->scheduleDto = $this->createMock(MessageScheduleDto::class); + + $contentRequest = $this->createMock(MessageContentRequest::class); + $contentRequest->method('getDto')->willReturn($this->contentDto); + + $formatRequest = $this->createMock(MessageFormatRequest::class); + $formatRequest->method('getDto')->willReturn($this->formatDto); + + $metadataRequest = $this->createMock(MessageMetadataRequest::class); + $metadataRequest->method('getDto')->willReturn($this->metadataDto); + + $optionsRequest = $this->createMock(MessageOptionsRequest::class); + $optionsRequest->method('getDto')->willReturn($this->optionsDto); + + $scheduleRequest = $this->createMock(MessageScheduleRequest::class); + $scheduleRequest->method('getDto')->willReturn($this->scheduleDto); + + $this->request = new UpdateMessageRequest(); + $this->request->messageId = 123; + $this->request->content = $contentRequest; + $this->request->format = $formatRequest; + $this->request->metadata = $metadataRequest; + $this->request->options = $optionsRequest; + $this->request->schedule = $scheduleRequest; + } + + public function testGetDtoReturnsCorrectDto(): void + { + $this->request->templateId = 456; + + $dto = $this->request->getDto(); + + $this->assertInstanceOf(UpdateMessageDto::class, $dto); + $this->assertEquals(123, $dto->messageId); + $this->assertSame($this->contentDto, $dto->content); + $this->assertSame($this->formatDto, $dto->format); + $this->assertSame($this->metadataDto, $dto->metadata); + $this->assertSame($this->optionsDto, $dto->options); + $this->assertSame($this->scheduleDto, $dto->schedule); + $this->assertEquals(456, $dto->templateId); + } + + public function testGetDtoWithNullTemplateId(): void + { + $this->request->templateId = null; + + $dto = $this->request->getDto(); + + $this->assertInstanceOf(UpdateMessageDto::class, $dto); + $this->assertEquals(123, $dto->messageId); + $this->assertSame($this->contentDto, $dto->content); + $this->assertSame($this->formatDto, $dto->format); + $this->assertSame($this->metadataDto, $dto->metadata); + $this->assertSame($this->optionsDto, $dto->options); + $this->assertSame($this->scheduleDto, $dto->schedule); + $this->assertNull($dto->templateId); + } +} diff --git a/tests/Unit/Serializer/MessageNormalizerTest.php b/tests/Unit/Messaging/Serializer/MessageNormalizerTest.php similarity index 74% rename from tests/Unit/Serializer/MessageNormalizerTest.php rename to tests/Unit/Messaging/Serializer/MessageNormalizerTest.php index 12c6b06..df5a33d 100644 --- a/tests/Unit/Serializer/MessageNormalizerTest.php +++ b/tests/Unit/Messaging/Serializer/MessageNormalizerTest.php @@ -2,19 +2,14 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Unit\Serializer; +namespace PhpList\RestBundle\Tests\Unit\Messaging\Serializer; use DateTime; -use PhpList\Core\Domain\Model\Messaging\Message; -use PhpList\Core\Domain\Model\Messaging\Message\MessageContent; -use PhpList\Core\Domain\Model\Messaging\Message\MessageFormat; -use PhpList\Core\Domain\Model\Messaging\Message\MessageMetadata; -use PhpList\Core\Domain\Model\Messaging\Message\MessageOptions; -use PhpList\Core\Domain\Model\Messaging\Message\MessageSchedule; -use PhpList\Core\Domain\Model\Messaging\Template; -use PhpList\RestBundle\Serializer\MessageNormalizer; -use PhpList\RestBundle\Serializer\TemplateImageNormalizer; -use PhpList\RestBundle\Serializer\TemplateNormalizer; +use PhpList\Core\Domain\Messaging\Model\Message; +use PhpList\Core\Domain\Messaging\Model\Template; +use PhpList\RestBundle\Messaging\Serializer\MessageNormalizer; +use PhpList\RestBundle\Messaging\Serializer\TemplateImageNormalizer; +use PhpList\RestBundle\Messaging\Serializer\TemplateNormalizer; use PHPUnit\Framework\TestCase; class MessageNormalizerTest extends TestCase @@ -44,21 +39,21 @@ public function testNormalizeReturnsExpectedArray(): void 'getListOrder' => 1, ]); - $content = new MessageContent('Subject', 'Text', 'TextMsg', 'Footer'); - $format = new MessageFormat(true, 'html'); + $content = new Message\MessageContent('Subject', 'Text', 'TextMsg', 'Footer'); + $format = new Message\MessageFormat(true, 'html'); $format->setFormatOptions(['text', 'html']); $entered = new DateTime('2025-01-01T10:00:00+00:00'); $sent = new DateTime('2025-01-02T10:00:00+00:00'); - $metadata = new MessageMetadata('draft'); + $metadata = new Message\MessageMetadata('draft'); $metadata->setProcessed(true); $metadata->setViews(10); $metadata->setBounceCount(3); $metadata->setEntered($entered); $metadata->setSent($sent); - $schedule = new MessageSchedule( + $schedule = new Message\MessageSchedule( 24, new DateTime('2025-01-10T00:00:00+00:00'), 12, @@ -66,7 +61,7 @@ public function testNormalizeReturnsExpectedArray(): void new DateTime('2025-01-01T00:00:00+00:00') ); - $options = new MessageOptions('from@example.com', 'to@example.com', 'reply@example.com', 'group'); + $options = new Message\MessageOptions('from@example.com', 'to@example.com', 'reply@example.com', 'group'); $message = $this->createMock(Message::class); $message->method('getId')->willReturn(1); diff --git a/tests/Unit/Serializer/TemplateImageNormalizerTest.php b/tests/Unit/Messaging/Serializer/TemplateImageNormalizerTest.php similarity index 89% rename from tests/Unit/Serializer/TemplateImageNormalizerTest.php rename to tests/Unit/Messaging/Serializer/TemplateImageNormalizerTest.php index 3a3a7dd..b5a4f57 100644 --- a/tests/Unit/Serializer/TemplateImageNormalizerTest.php +++ b/tests/Unit/Messaging/Serializer/TemplateImageNormalizerTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Unit\Serializer; +namespace PhpList\RestBundle\Tests\Unit\Messaging\Serializer; -use PhpList\Core\Domain\Model\Messaging\Template; -use PhpList\Core\Domain\Model\Messaging\TemplateImage; -use PhpList\RestBundle\Serializer\TemplateImageNormalizer; +use PhpList\Core\Domain\Messaging\Model\Template; +use PhpList\Core\Domain\Messaging\Model\TemplateImage; +use PhpList\RestBundle\Messaging\Serializer\TemplateImageNormalizer; use PHPUnit\Framework\TestCase; class TemplateImageNormalizerTest extends TestCase diff --git a/tests/Unit/Serializer/TemplateNormalizerTest.php b/tests/Unit/Messaging/Serializer/TemplateNormalizerTest.php similarity index 91% rename from tests/Unit/Serializer/TemplateNormalizerTest.php rename to tests/Unit/Messaging/Serializer/TemplateNormalizerTest.php index 23bac70..4ea3def 100644 --- a/tests/Unit/Serializer/TemplateNormalizerTest.php +++ b/tests/Unit/Messaging/Serializer/TemplateNormalizerTest.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Unit\Serializer; +namespace PhpList\RestBundle\Tests\Unit\Messaging\Serializer; -use PhpList\Core\Domain\Model\Messaging\Template; -use PhpList\Core\Domain\Model\Messaging\TemplateImage; -use PhpList\RestBundle\Serializer\TemplateImageNormalizer; -use PhpList\RestBundle\Serializer\TemplateNormalizer; +use Doctrine\Common\Collections\ArrayCollection; +use PhpList\Core\Domain\Messaging\Model\Template; +use PhpList\Core\Domain\Messaging\Model\TemplateImage; +use PhpList\RestBundle\Messaging\Serializer\TemplateImageNormalizer; +use PhpList\RestBundle\Messaging\Serializer\TemplateNormalizer; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use Doctrine\Common\Collections\ArrayCollection; class TemplateNormalizerTest extends TestCase { diff --git a/tests/Unit/Validator/Constraint/ContainsPlaceholderValidatorTest.php b/tests/Unit/Messaging/Validator/Constraint/ContainsPlaceholderValidatorTest.php similarity index 88% rename from tests/Unit/Validator/Constraint/ContainsPlaceholderValidatorTest.php rename to tests/Unit/Messaging/Validator/Constraint/ContainsPlaceholderValidatorTest.php index a4249db..bdd2105 100644 --- a/tests/Unit/Validator/Constraint/ContainsPlaceholderValidatorTest.php +++ b/tests/Unit/Messaging/Validator/Constraint/ContainsPlaceholderValidatorTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Unit\Validator\Constraint; +namespace PhpList\RestBundle\Tests\Unit\Messaging\Validator\Constraint; -use PhpList\RestBundle\Validator\Constraint\ContainsPlaceholder; -use PhpList\RestBundle\Validator\Constraint\ContainsPlaceholderValidator; +use PhpList\RestBundle\Messaging\Validator\Constraint\ContainsPlaceholder; +use PhpList\RestBundle\Messaging\Validator\Constraint\ContainsPlaceholderValidator; use PHPUnit\Framework\TestCase; use Symfony\Component\Validator\Context\ExecutionContextInterface; use Symfony\Component\Validator\Violation\ConstraintViolationBuilderInterface; diff --git a/tests/Unit/Validator/Constraint/TemplateExistsValidatorTest.php b/tests/Unit/Messaging/Validator/Constraint/TemplateExistsValidatorTest.php similarity index 88% rename from tests/Unit/Validator/Constraint/TemplateExistsValidatorTest.php rename to tests/Unit/Messaging/Validator/Constraint/TemplateExistsValidatorTest.php index 04e478d..8d6ca8f 100644 --- a/tests/Unit/Validator/Constraint/TemplateExistsValidatorTest.php +++ b/tests/Unit/Messaging/Validator/Constraint/TemplateExistsValidatorTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Unit\Validator\Constraint; +namespace PhpList\RestBundle\Tests\Unit\Messaging\Validator\Constraint; -use PhpList\Core\Domain\Model\Messaging\Template; -use PhpList\Core\Domain\Repository\Messaging\TemplateRepository; -use PhpList\RestBundle\Validator\Constraint\TemplateExists; -use PhpList\RestBundle\Validator\Constraint\TemplateExistsValidator; +use PhpList\Core\Domain\Messaging\Model\Template; +use PhpList\Core\Domain\Messaging\Repository\TemplateRepository; +use PhpList\RestBundle\Messaging\Validator\Constraint\TemplateExists; +use PhpList\RestBundle\Messaging\Validator\Constraint\TemplateExistsValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\Exception\ConflictHttpException; diff --git a/tests/Unit/Service/Builder/MessageBuilderTest.php b/tests/Unit/Service/Builder/MessageBuilderTest.php deleted file mode 100644 index ba2ead4..0000000 --- a/tests/Unit/Service/Builder/MessageBuilderTest.php +++ /dev/null @@ -1,148 +0,0 @@ -createMock(TemplateRepository::class); - $this->formatBuilder = $this->createMock(MessageFormatBuilder::class); - $this->scheduleBuilder = $this->createMock(MessageScheduleBuilder::class); - $this->contentBuilder = $this->createMock(MessageContentBuilder::class); - $this->optionsBuilder = $this->createMock(MessageOptionsBuilder::class); - - $this->builder = new MessageBuilder( - $templateRepository, - $this->formatBuilder, - $this->scheduleBuilder, - $this->contentBuilder, - $this->optionsBuilder - ); - } - - private function createRequest(): CreateMessageRequest - { - $request = new CreateMessageRequest(); - $request->format = new MessageFormatRequest(); - $request->schedule = new MessageScheduleRequest(); - $request->content = new MessageContentRequest(); - $request->metadata = new MessageMetadataRequest(); - $request->metadata->status = 'draft'; - $request->options = new MessageOptionsRequest(); - $request->templateId = 0; - - return $request; - } - - private function mockBuildFromDtoCalls(CreateMessageRequest $request): void - { - $this->formatBuilder->expects($this->once()) - ->method('buildFromDto') - ->with($request->format) - ->willReturn($this->createMock(Message\MessageFormat::class)); - - $this->scheduleBuilder->expects($this->once()) - ->method('buildFromDto') - ->with($request->schedule) - ->willReturn($this->createMock(Message\MessageSchedule::class)); - - $this->contentBuilder->expects($this->once()) - ->method('buildFromDto') - ->with($request->content) - ->willReturn($this->createMock(Message\MessageContent::class)); - - $this->optionsBuilder->expects($this->once()) - ->method('buildFromDto') - ->with($request->options) - ->willReturn($this->createMock(Message\MessageOptions::class)); - } - - public function testBuildsNewMessage(): void - { - $request = $this->createRequest(); - $admin = $this->createMock(Administrator::class); - $context = new MessageContext($admin); - - $this->mockBuildFromDtoCalls($request); - - $this->builder->buildFromRequest($request, $context); - } - - public function testThrowsExceptionOnInvalidRequest(): void - { - $this->expectException(InvalidArgumentException::class); - - $this->builder->buildFromRequest( - $this->createMock(RequestInterface::class), - new MessageContext($this->createMock(Administrator::class)) - ); - } - - public function testThrowsExceptionOnInvalidContext(): void - { - $this->expectException(InvalidArgumentException::class); - - $this->builder->buildFromRequest(new CreateMessageRequest(), new \stdClass()); - } - - public function testUpdatesExistingMessage(): void - { - $request = $this->createRequest(); - $admin = $this->createMock(Administrator::class); - $existingMessage = $this->createMock(Message::class); - $context = new MessageContext($admin, $existingMessage); - - $this->mockBuildFromDtoCalls($request); - - $existingMessage - ->expects($this->once()) - ->method('setFormat') - ->with($this->isInstanceOf(Message\MessageFormat::class)); - $existingMessage - ->expects($this->once()) - ->method('setSchedule') - ->with($this->isInstanceOf(Message\MessageSchedule::class)); - $existingMessage - ->expects($this->once()) - ->method('setContent') - ->with($this->isInstanceOf(Message\MessageContent::class)); - $existingMessage - ->expects($this->once()) - ->method('setOptions') - ->with($this->isInstanceOf(Message\MessageOptions::class)); - $existingMessage->expects($this->once())->method('setTemplate')->with(null); - - $result = $this->builder->buildFromRequest($request, $context); - - $this->assertSame($existingMessage, $result); - } -} diff --git a/tests/Unit/Service/Builder/MessageContentBuilderTest.php b/tests/Unit/Service/Builder/MessageContentBuilderTest.php deleted file mode 100644 index 7a45d4c..0000000 --- a/tests/Unit/Service/Builder/MessageContentBuilderTest.php +++ /dev/null @@ -1,44 +0,0 @@ -builder = new MessageContentBuilder(); - } - - public function testBuildsMessageContentSuccessfully(): void - { - $dto = new MessageContentRequest(); - $dto->subject = 'Test Subject'; - $dto->text = 'Full text content'; - $dto->textMessage = 'Short text version'; - $dto->footer = 'Footer text'; - - $messageContent = $this->builder->buildFromDto($dto); - - $this->assertSame('Test Subject', $messageContent->getSubject()); - $this->assertSame('Full text content', $messageContent->getText()); - $this->assertSame('Short text version', $messageContent->getTextMessage()); - $this->assertSame('Footer text', $messageContent->getFooter()); - } - - public function testThrowsExceptionOnInvalidDto(): void - { - $this->expectException(InvalidArgumentException::class); - - $invalidDto = new \stdClass(); - $this->builder->buildFromDto($invalidDto); - } -} diff --git a/tests/Unit/Service/Builder/MessageFormatBuilderTest.php b/tests/Unit/Service/Builder/MessageFormatBuilderTest.php deleted file mode 100644 index a561248..0000000 --- a/tests/Unit/Service/Builder/MessageFormatBuilderTest.php +++ /dev/null @@ -1,42 +0,0 @@ -builder = new MessageFormatBuilder(); - } - - public function testBuildsMessageFormatSuccessfully(): void - { - $dto = new MessageFormatRequest(); - $dto->htmlFormated = true; - $dto->sendFormat = 'html'; - $dto->formatOptions = ['html', 'text']; - - $messageFormat = $this->builder->buildFromDto($dto); - - $this->assertSame(true, $messageFormat->isHtmlFormatted()); - $this->assertSame('html', $messageFormat->getSendFormat()); - $this->assertEqualsCanonicalizing(['html', 'text'], $messageFormat->getFormatOptions()); - } - - public function testThrowsExceptionOnInvalidDto(): void - { - $this->expectException(InvalidArgumentException::class); - - $invalidDto = new \stdClass(); - $this->builder->buildFromDto($invalidDto); - } -} diff --git a/tests/Unit/Service/Builder/MessageOptionsBuilderTest.php b/tests/Unit/Service/Builder/MessageOptionsBuilderTest.php deleted file mode 100644 index 30fe00c..0000000 --- a/tests/Unit/Service/Builder/MessageOptionsBuilderTest.php +++ /dev/null @@ -1,44 +0,0 @@ -builder = new MessageOptionsBuilder(); - } - - public function testBuildsMessageOptionsSuccessfully(): void - { - $dto = new MessageOptionsRequest(); - $dto->fromField = 'info@example.com'; - $dto->toField = 'user@example.com'; - $dto->replyTo = 'reply@example.com'; - $dto->userSelection = 'all-users'; - - $messageOptions = $this->builder->buildFromDto($dto); - - $this->assertSame('info@example.com', $messageOptions->getFromField()); - $this->assertSame('user@example.com', $messageOptions->getToField()); - $this->assertSame('reply@example.com', $messageOptions->getReplyTo()); - $this->assertSame('all-users', $messageOptions->getUserSelection()); - } - - public function testThrowsExceptionOnInvalidDto(): void - { - $this->expectException(InvalidArgumentException::class); - - $invalidDto = new \stdClass(); - $this->builder->buildFromDto($invalidDto); - } -} diff --git a/tests/Unit/Service/Builder/MessageScheduleBuilderTest.php b/tests/Unit/Service/Builder/MessageScheduleBuilderTest.php deleted file mode 100644 index 85078c4..0000000 --- a/tests/Unit/Service/Builder/MessageScheduleBuilderTest.php +++ /dev/null @@ -1,47 +0,0 @@ -builder = new MessageScheduleBuilder(); - } - - public function testBuildsMessageScheduleSuccessfully(): void - { - $dto = new MessageScheduleRequest(); - $dto->repeatInterval = 1440; - $dto->repeatUntil = '2025-04-30T00:00:00+00:00'; - $dto->requeueInterval = 720; - $dto->requeueUntil = '2025-04-20T00:00:00+00:00'; - $dto->embargo = '2025-04-17T09:00:00+00:00'; - - $messageSchedule = $this->builder->buildFromDto($dto); - - $this->assertSame(1440, $messageSchedule->getRepeatInterval()); - $this->assertEquals(new DateTime('2025-04-30T00:00:00+00:00'), $messageSchedule->getRepeatUntil()); - $this->assertSame(720, $messageSchedule->getRequeueInterval()); - $this->assertEquals(new DateTime('2025-04-20T00:00:00+00:00'), $messageSchedule->getRequeueUntil()); - $this->assertEquals(new DateTime('2025-04-17T09:00:00+00:00'), $messageSchedule->getEmbargo()); - } - - public function testThrowsExceptionOnInvalidDto(): void - { - $this->expectException(InvalidArgumentException::class); - - $invalidDto = new \stdClass(); - $this->builder->buildFromDto($invalidDto); - } -} diff --git a/tests/Unit/Service/Manager/AdministratorManagerTest.php b/tests/Unit/Service/Manager/AdministratorManagerTest.php deleted file mode 100644 index 39f441b..0000000 --- a/tests/Unit/Service/Manager/AdministratorManagerTest.php +++ /dev/null @@ -1,94 +0,0 @@ -createMock(EntityManagerInterface::class); - $hashGenerator = $this->createMock(HashGenerator::class); - - $dto = new CreateAdministratorRequest(); - $dto->loginName = 'admin'; - $dto->email = 'admin@example.com'; - $dto->superUser = true; - $dto->password = 'securepass'; - - $hashGenerator->expects($this->once()) - ->method('createPasswordHash') - ->with('securepass') - ->willReturn('hashed_pass'); - - $entityManager->expects($this->once())->method('persist'); - $entityManager->expects($this->once())->method('flush'); - - $manager = new AdministratorManager($entityManager, $hashGenerator); - $admin = $manager->createAdministrator($dto); - - $this->assertInstanceOf(Administrator::class, $admin); - $this->assertEquals('admin', $admin->getLoginName()); - $this->assertEquals('admin@example.com', $admin->getEmail()); - $this->assertEquals(true, $admin->isSuperUser()); - $this->assertEquals('hashed_pass', $admin->getPasswordHash()); - } - - public function testUpdateAdministrator(): void - { - $entityManager = $this->createMock(EntityManagerInterface::class); - $hashGenerator = $this->createMock(HashGenerator::class); - - $admin = new Administrator(); - $admin->setLoginName('old'); - $admin->setEmail('old@example.com'); - $admin->setSuperUser(false); - $admin->setPasswordHash('old_hash'); - - $dto = new UpdateAdministratorRequest(); - $dto->loginName = 'new'; - $dto->email = 'new@example.com'; - $dto->superAdmin = true; - $dto->password = 'newpass'; - - $hashGenerator->expects($this->once()) - ->method('createPasswordHash') - ->with('newpass') - ->willReturn('new_hash'); - - $entityManager->expects($this->once())->method('flush'); - - $manager = new AdministratorManager($entityManager, $hashGenerator); - $manager->updateAdministrator($admin, $dto); - - $this->assertEquals('new', $admin->getLoginName()); - $this->assertEquals('new@example.com', $admin->getEmail()); - $this->assertTrue($admin->isSuperUser()); - $this->assertEquals('new_hash', $admin->getPasswordHash()); - } - - public function testDeleteAdministrator(): void - { - $entityManager = $this->createMock(EntityManagerInterface::class); - $hashGenerator = $this->createMock(HashGenerator::class); - - $admin = $this->createMock(Administrator::class); - - $entityManager->expects($this->once())->method('remove')->with($admin); - $entityManager->expects($this->once())->method('flush'); - - $manager = new AdministratorManager($entityManager, $hashGenerator); - $manager->deleteAdministrator($admin); - - $this->assertTrue(true); - } -} diff --git a/tests/Unit/Service/Manager/MessageManagerTest.php b/tests/Unit/Service/Manager/MessageManagerTest.php deleted file mode 100644 index 6b08731..0000000 --- a/tests/Unit/Service/Manager/MessageManagerTest.php +++ /dev/null @@ -1,152 +0,0 @@ -createMock(MessageRepository::class); - $messageBuilder = $this->createMock(MessageBuilder::class); - - $manager = new MessageManager($messageRepository, $messageBuilder); - - $format = new MessageFormatRequest(); - $format->htmlFormated = true; - $format->sendFormat = 'html'; - $format->formatOptions = ['html']; - - $schedule = new MessageScheduleRequest(); - $schedule->repeatInterval = 60 * 24; - $schedule->repeatUntil = '2025-04-30T00:00:00+00:00'; - $schedule->requeueInterval = 60 * 12; - $schedule->requeueUntil = '2025-04-20T00:00:00+00:00'; - $schedule->embargo = '2025-04-17T09:00:00+00:00'; - - $metadata = new MessageMetadataRequest(); - $metadata->status = 'draft'; - - $content = new MessageContentRequest(); - $content->subject = 'Subject'; - $content->text = 'Full text'; - $content->textMessage = 'Short text'; - $content->footer = 'Footer'; - - $options = new MessageOptionsRequest(); - $options->fromField = 'from@example.com'; - $options->toField = 'to@example.com'; - $options->replyTo = 'reply@example.com'; - $options->userSelection = 'all-users'; - - $request = new CreateMessageRequest(); - $request->format = $format; - $request->schedule = $schedule; - $request->metadata = $metadata; - $request->content = $content; - $request->options = $options; - $request->templateId = 0; - - $authUser = $this->createMock(Administrator::class); - - $expectedMessage = $this->createMock(Message::class); - $expectedContent = $this->createMock(Message\MessageContent::class); - $expectedMetadata = $this->createMock(Message\MessageMetadata::class); - - $expectedContent->method('getSubject')->willReturn('Subject'); - $expectedMetadata->method('getStatus')->willReturn('draft'); - - $expectedMessage->method('getContent')->willReturn($expectedContent); - $expectedMessage->method('getMetadata')->willReturn($expectedMetadata); - - $messageBuilder->expects($this->once()) - ->method('buildFromRequest') - ->with($request, $this->anything()) - ->willReturn($expectedMessage); - - $messageRepository->expects($this->once()) - ->method('save') - ->with($expectedMessage); - - $message = $manager->createMessage($request, $authUser); - - $this->assertSame('Subject', $message->getContent()->getSubject()); - $this->assertSame('draft', $message->getMetadata()->getStatus()); - } - - public function testUpdateMessageReturnsUpdatedMessage(): void - { - $messageRepository = $this->createMock(MessageRepository::class); - $messageBuilder = $this->createMock(MessageBuilder::class); - - $manager = new MessageManager($messageRepository, $messageBuilder); - - $updateRequest = new \PhpList\RestBundle\Entity\Request\UpdateMessageRequest(); - $updateRequest->messageId = 1; - $updateRequest->format = new MessageFormatRequest(); - $updateRequest->format->htmlFormated = false; - $updateRequest->format->sendFormat = 'text'; - $updateRequest->format->formatOptions = ['text']; - - $updateRequest->schedule = new MessageScheduleRequest(); - $updateRequest->schedule->repeatInterval = 0; - $updateRequest->schedule->repeatUntil = '2025-04-30T00:00:00+00:00'; - $updateRequest->schedule->requeueInterval = 0; - $updateRequest->schedule->requeueUntil = '2025-04-20T00:00:00+00:00'; - $updateRequest->schedule->embargo = '2025-04-17T09:00:00+00:00'; - - $updateRequest->content = new MessageContentRequest(); - $updateRequest->content->subject = 'Updated Subject'; - $updateRequest->content->text = 'Updated Full text'; - $updateRequest->content->textMessage = 'Updated Short text'; - $updateRequest->content->footer = 'Updated Footer'; - - $updateRequest->options = new MessageOptionsRequest(); - $updateRequest->options->fromField = 'newfrom@example.com'; - $updateRequest->options->toField = 'newto@example.com'; - $updateRequest->options->replyTo = 'newreply@example.com'; - $updateRequest->options->userSelection = 'active-users'; - - $updateRequest->templateId = 2; - - $authUser = $this->createMock(Administrator::class); - - $existingMessage = $this->createMock(Message::class); - $expectedContent = $this->createMock(Message\MessageContent::class); - $expectedMetadata = $this->createMock(Message\MessageMetadata::class); - - $expectedContent->method('getSubject')->willReturn('Updated Subject'); - $expectedMetadata->method('getStatus')->willReturn('draft'); - - $existingMessage->method('getContent')->willReturn($expectedContent); - $existingMessage->method('getMetadata')->willReturn($expectedMetadata); - - $messageBuilder->expects($this->once()) - ->method('buildFromRequest') - ->with($updateRequest, $this->anything()) - ->willReturn($existingMessage); - - $messageRepository->expects($this->once()) - ->method('save') - ->with($existingMessage); - - $message = $manager->updateMessage($updateRequest, $existingMessage, $authUser); - - $this->assertSame('Updated Subject', $message->getContent()->getSubject()); - $this->assertSame('draft', $message->getMetadata()->getStatus()); - } -} diff --git a/tests/Unit/Service/Manager/SessionManagerTest.php b/tests/Unit/Service/Manager/SessionManagerTest.php deleted file mode 100644 index 9489c24..0000000 --- a/tests/Unit/Service/Manager/SessionManagerTest.php +++ /dev/null @@ -1,54 +0,0 @@ -loginName = 'admin'; - $request->password = 'wrong'; - - $adminRepo = $this->createMock(AdministratorRepository::class); - $adminRepo->expects(self::once()) - ->method('findOneByLoginCredentials') - ->with('admin', 'wrong') - ->willReturn(null); - - $tokenRepo = $this->createMock(AdministratorTokenRepository::class); - $tokenRepo->expects(self::never())->method('save'); - - $manager = new SessionManager($tokenRepo, $adminRepo); - - $this->expectException(UnauthorizedHttpException::class); - $this->expectExceptionMessage('Not authorized'); - - $manager->createSession($request); - } - - public function testDeleteSessionCallsRemove(): void - { - $token = $this->createMock(AdministratorToken::class); - - $tokenRepo = $this->createMock(AdministratorTokenRepository::class); - $tokenRepo->expects(self::once()) - ->method('remove') - ->with($token); - - $adminRepo = $this->createMock(AdministratorRepository::class); - - $manager = new SessionManager($tokenRepo, $adminRepo); - $manager->deleteSession($token); - } -} diff --git a/tests/Unit/Service/Manager/SubscriberListManagerTest.php b/tests/Unit/Service/Manager/SubscriberListManagerTest.php deleted file mode 100644 index 20e2d77..0000000 --- a/tests/Unit/Service/Manager/SubscriberListManagerTest.php +++ /dev/null @@ -1,77 +0,0 @@ -subscriberListRepository = $this->createMock(SubscriberListRepository::class); - $this->manager = new SubscriberListManager($this->subscriberListRepository); - } - - public function testCreateSubscriberList(): void - { - $request = new CreateSubscriberListRequest(); - $request->name = 'New List'; - $request->description = 'Description'; - $request->listPosition = 3; - $request->public = true; - - $admin = new Administrator(); - - $this->subscriberListRepository - ->expects($this->once()) - ->method('save') - ->with($this->isInstanceOf(SubscriberList::class)); - - $result = $this->manager->createSubscriberList($request, $admin); - - $this->assertSame('New List', $result->getName()); - $this->assertSame('Description', $result->getDescription()); - $this->assertSame(3, $result->getListPosition()); - $this->assertTrue($result->isPublic()); - $this->assertSame($admin, $result->getOwner()); - } - - public function testGetPaginated(): void - { - $list = new SubscriberList(); - $this->subscriberListRepository - ->expects($this->once()) - ->method('getAfterId') - ->willReturn([$list]); - - $result = $this->manager->getPaginated(new PaginationCursorRequest(0)); - - $this->assertIsArray($result); - $this->assertCount(1, $result); - $this->assertSame($list, $result[0]); - } - - public function testDeleteSubscriberList(): void - { - $subscriberList = new SubscriberList(); - - $this->subscriberListRepository - ->expects($this->once()) - ->method('remove') - ->with($subscriberList); - - $this->manager->delete($subscriberList); - } -} diff --git a/tests/Unit/Service/Manager/SubscriberManagerTest.php b/tests/Unit/Service/Manager/SubscriberManagerTest.php deleted file mode 100644 index a159d91..0000000 --- a/tests/Unit/Service/Manager/SubscriberManagerTest.php +++ /dev/null @@ -1,47 +0,0 @@ -createMock(SubscriberRepository::class); - $emMock = $this->createMock(EntityManagerInterface::class); - $repoMock - ->expects($this->once()) - ->method('save') - ->with($this->callback(function (Subscriber $sub): bool { - return $sub->getEmail() === 'foo@bar.com' - && $sub->isConfirmed() === false - && $sub->isBlacklisted() === false - && $sub->hasHtmlEmail() === true - && $sub->isDisabled() === false; - })); - - $manager = new SubscriberManager($repoMock, $emMock); - - $dto = new CreateSubscriberRequest(); - $dto->email = 'foo@bar.com'; - $dto->requestConfirmation = true; - $dto->htmlEmail = true; - - $result = $manager->createSubscriber($dto); - - $this->assertInstanceOf(Subscriber::class, $result); - $this->assertSame('foo@bar.com', $result->getEmail()); - $this->assertFalse($result->isConfirmed()); - $this->assertFalse($result->isBlacklisted()); - $this->assertTrue($result->hasHtmlEmail()); - $this->assertFalse($result->isDisabled()); - } -} diff --git a/tests/Unit/Service/Manager/SubscriptionManagerTest.php b/tests/Unit/Service/Manager/SubscriptionManagerTest.php deleted file mode 100644 index 729deaa..0000000 --- a/tests/Unit/Service/Manager/SubscriptionManagerTest.php +++ /dev/null @@ -1,107 +0,0 @@ -subscriptionRepository = $this->createMock(SubscriptionRepository::class); - $this->subscriberRepository = $this->createMock(SubscriberRepository::class); - $this->manager = new SubscriptionManager($this->subscriptionRepository, $this->subscriberRepository); - } - - public function testCreateSubscriptionWhenSubscriberExists(): void - { - $email = 'test@example.com'; - $subscriber = new Subscriber(); - $list = new SubscriberList(); - - $this->subscriberRepository->method('findOneBy')->with(['email' => $email])->willReturn($subscriber); - $this->subscriptionRepository->method('findOneBySubscriberListAndSubscriber')->willReturn(null); - $this->subscriptionRepository->expects($this->once())->method('save'); - - $subscriptions = $this->manager->createSubscriptions($list, [$email]); - - $this->assertCount(1, $subscriptions); - $this->assertInstanceOf(Subscription::class, $subscriptions[0]); - } - - public function testCreateSubscriptionThrowsWhenSubscriberMissing(): void - { - $this->expectException(SubscriptionCreationException::class); - $this->expectExceptionMessage('Subscriber does not exists.'); - - $list = new SubscriberList(); - - $this->subscriberRepository->method('findOneBy')->willReturn(null); - - $this->manager->createSubscriptions($list, ['missing@example.com']); - } - - public function testDeleteSubscriptionSuccessfully(): void - { - $email = 'user@example.com'; - $subscriberList = $this->createMock(SubscriberList::class); - $subscriberList->method('getId')->willReturn(1); - $subscription = new Subscription(); - - $this->subscriptionRepository - ->method('findOneBySubscriberEmailAndListId') - ->with($subscriberList->getId(), $email) - ->willReturn($subscription); - - $this->subscriptionRepository->expects($this->once())->method('remove')->with($subscription); - - $this->manager->deleteSubscriptions($subscriberList, [$email]); - } - - public function testDeleteSubscriptionSkipsNotFound(): void - { - $email = 'missing@example.com'; - $subscriberList = $this->createMock(SubscriberList::class); - $subscriberList->method('getId')->willReturn(1); - - $this->subscriptionRepository - ->method('findOneBySubscriberEmailAndListId') - ->willReturn(null); - - $this->manager->deleteSubscriptions($subscriberList, [$email]); - - $this->addToAssertionCount(1); - } - - public function testGetSubscriberListMembersReturnsList(): void - { - $subscriberList = $this->createMock(SubscriberList::class); - $subscriberList->method('getId')->willReturn(1); - $subscriber = new Subscriber(); - - $this->subscriberRepository - ->method('getSubscribersBySubscribedListId') - ->with($subscriberList->getId()) - ->willReturn([$subscriber]); - - $result = $this->manager->getSubscriberListMembers($subscriberList); - - $this->assertIsArray($result); - $this->assertCount(1, $result); - $this->assertInstanceOf(Subscriber::class, $result[0]); - } -} diff --git a/tests/Unit/Service/Manager/TemplateImageManagerTest.php b/tests/Unit/Service/Manager/TemplateImageManagerTest.php deleted file mode 100644 index 32d26af..0000000 --- a/tests/Unit/Service/Manager/TemplateImageManagerTest.php +++ /dev/null @@ -1,88 +0,0 @@ -templateImageRepository = $this->createMock(TemplateImageRepository::class); - $this->entityManager = $this->createMock(EntityManagerInterface::class); - - $this->manager = new TemplateImageManager( - $this->templateImageRepository, - $this->entityManager - ); - } - - public function testCreateImagesFromImagePaths(): void - { - $template = $this->createMock(Template::class); - - $this->entityManager->expects($this->exactly(2)) - ->method('persist') - ->with($this->isInstanceOf(TemplateImage::class)); - - $this->entityManager->expects($this->once()) - ->method('flush'); - - $images = $this->manager->createImagesFromImagePaths(['image1.jpg', 'image2.png'], $template); - - $this->assertCount(2, $images); - foreach ($images as $image) { - $this->assertInstanceOf(TemplateImage::class, $image); - } - } - - public function testGuessMimeType(): void - { - $reflection = new \ReflectionClass($this->manager); - $method = $reflection->getMethod('guessMimeType'); - - $this->assertSame('image/jpeg', $method->invoke($this->manager, 'photo.jpg')); - $this->assertSame('image/png', $method->invoke($this->manager, 'picture.png')); - $this->assertSame('application/octet-stream', $method->invoke($this->manager, 'file.unknownext')); - } - - public function testExtractAllImages(): void - { - $html = '' . - '' . - '' . - '' . - 'Download' . - '' . - ''; - - $result = $this->manager->extractAllImages($html); - - $this->assertIsArray($result); - $this->assertContains('image1.jpg', $result); - $this->assertContains('https://example.com/image2.png', $result); - } - - public function testDeleteTemplateImage(): void - { - $templateImage = $this->createMock(TemplateImage::class); - - $this->templateImageRepository->expects($this->once()) - ->method('remove') - ->with($templateImage); - - $this->manager->delete($templateImage); - } -} diff --git a/tests/Unit/Service/Manager/TemplateManagerTest.php b/tests/Unit/Service/Manager/TemplateManagerTest.php deleted file mode 100644 index d021218..0000000 --- a/tests/Unit/Service/Manager/TemplateManagerTest.php +++ /dev/null @@ -1,90 +0,0 @@ -templateRepository = $this->createMock(TemplateRepository::class); - $entityManager = $this->createMock(EntityManagerInterface::class); - $this->templateImageManager = $this->createMock(TemplateImageManager::class); - $this->templateLinkValidator = $this->createMock(TemplateLinkValidator::class); - $this->templateImageValidator = $this->createMock(TemplateImageValidator::class); - - $this->manager = new TemplateManager( - $this->templateRepository, - $entityManager, - $this->templateImageManager, - $this->templateLinkValidator, - $this->templateImageValidator - ); - } - - public function testCreateTemplateSuccessfully(): void - { - $request = new CreateTemplateRequest(); - $request->title = 'Test Template'; - $request->content = 'Content'; - $request->text = 'Plain text'; - $request->checkLinks = true; - $request->checkImages = false; - $request->checkExternalImages = false; - $request->file = null; - - $this->templateLinkValidator->expects($this->once()) - ->method('validate') - ->with($request->content, $this->anything()); - - $this->templateImageManager->expects($this->once()) - ->method('extractAllImages') - ->with($request->content) - ->willReturn([]); - - $this->templateImageValidator->expects($this->once()) - ->method('validate') - ->with([], $this->anything()); - - $this->templateRepository->expects($this->once()) - ->method('save') - ->with($this->isInstanceOf(Template::class)); - - $this->templateImageManager->expects($this->once()) - ->method('createImagesFromImagePaths') - ->with([], $this->isInstanceOf(Template::class)); - - $template = $this->manager->create($request); - - $this->assertSame('Test Template', $template->getTitle()); - } - - public function testDeleteTemplate(): void - { - $template = $this->createMock(Template::class); - - $this->templateRepository->expects($this->once()) - ->method('remove') - ->with($template); - - $this->manager->delete($template); - } -} diff --git a/tests/Unit/Subscription/Request/CreateAttributeDefinitionRequestTest.php b/tests/Unit/Subscription/Request/CreateAttributeDefinitionRequestTest.php new file mode 100644 index 0000000..d55376b --- /dev/null +++ b/tests/Unit/Subscription/Request/CreateAttributeDefinitionRequestTest.php @@ -0,0 +1,49 @@ +name = 'Test Attribute'; + $request->type = 'text'; + $request->order = 5; + $request->defaultValue = 'default'; + $request->required = true; + $request->tableName = 'test_table'; + + $dto = $request->getDto(); + + $this->assertInstanceOf(AttributeDefinitionDto::class, $dto); + $this->assertEquals('Test Attribute', $dto->name); + $this->assertEquals('text', $dto->type); + $this->assertEquals(5, $dto->listOrder); + $this->assertEquals('default', $dto->defaultValue); + $this->assertTrue($dto->required); + $this->assertEquals('test_table', $dto->tableName); + } + + public function testGetDtoWithDefaultValues(): void + { + $request = new CreateAttributeDefinitionRequest(); + $request->name = 'Test Attribute'; + + $dto = $request->getDto(); + + $this->assertInstanceOf(AttributeDefinitionDto::class, $dto); + $this->assertEquals('Test Attribute', $dto->name); + $this->assertNull($dto->type); + $this->assertNull($dto->listOrder); + $this->assertNull($dto->defaultValue); + $this->assertFalse($dto->required); + $this->assertNull($dto->tableName); + } +} diff --git a/tests/Unit/Subscription/Request/CreateSubscriberListRequestTest.php b/tests/Unit/Subscription/Request/CreateSubscriberListRequestTest.php new file mode 100644 index 0000000..5f6f529 --- /dev/null +++ b/tests/Unit/Subscription/Request/CreateSubscriberListRequestTest.php @@ -0,0 +1,43 @@ +name = 'Test List'; + $request->public = true; + $request->listPosition = 5; + $request->description = 'Test description'; + + $dto = $request->getDto(); + + $this->assertInstanceOf(CreateSubscriberListDto::class, $dto); + $this->assertEquals('Test List', $dto->name); + $this->assertTrue($dto->isPublic); + $this->assertEquals(5, $dto->listPosition); + $this->assertEquals('Test description', $dto->description); + } + + public function testGetDtoWithDefaultValues(): void + { + $request = new CreateSubscriberListRequest(); + $request->name = 'Test List'; + + $dto = $request->getDto(); + + $this->assertInstanceOf(CreateSubscriberListDto::class, $dto); + $this->assertEquals('Test List', $dto->name); + $this->assertFalse($dto->isPublic); + $this->assertNull($dto->listPosition); + $this->assertNull($dto->description); + } +} diff --git a/tests/Unit/Subscription/Request/CreateSubscriberRequestTest.php b/tests/Unit/Subscription/Request/CreateSubscriberRequestTest.php new file mode 100644 index 0000000..3e81e47 --- /dev/null +++ b/tests/Unit/Subscription/Request/CreateSubscriberRequestTest.php @@ -0,0 +1,40 @@ +email = 'subscriber@example.com'; + $request->requestConfirmation = true; + $request->htmlEmail = false; + + $dto = $request->getDto(); + + $this->assertInstanceOf(CreateSubscriberDto::class, $dto); + $this->assertEquals('subscriber@example.com', $dto->email); + $this->assertTrue($dto->requestConfirmation); + $this->assertFalse($dto->htmlEmail); + } + + public function testGetDtoWithNullValues(): void + { + $request = new CreateSubscriberRequest(); + $request->email = 'subscriber@example.com'; + + $dto = $request->getDto(); + + $this->assertInstanceOf(CreateSubscriberDto::class, $dto); + $this->assertEquals('subscriber@example.com', $dto->email); + $this->assertNull($dto->requestConfirmation); + $this->assertNull($dto->htmlEmail); + } +} diff --git a/tests/Unit/Subscription/Request/SubscriptionRequestTest.php b/tests/Unit/Subscription/Request/SubscriptionRequestTest.php new file mode 100644 index 0000000..eb0171c --- /dev/null +++ b/tests/Unit/Subscription/Request/SubscriptionRequestTest.php @@ -0,0 +1,32 @@ +emails = ['test1@example.com', 'test2@example.com']; + + $dto = $request->getDto(); + + $this->assertSame($request, $dto); + $this->assertEquals(['test1@example.com', 'test2@example.com'], $dto->emails); + } + + public function testGetDtoWithEmptyEmails(): void + { + $request = new SubscriptionRequest(); + + $dto = $request->getDto(); + + $this->assertSame($request, $dto); + $this->assertEquals([], $dto->emails); + } +} diff --git a/tests/Unit/Subscription/Request/UpdateAttributeDefinitionRequestTest.php b/tests/Unit/Subscription/Request/UpdateAttributeDefinitionRequestTest.php new file mode 100644 index 0000000..512b534 --- /dev/null +++ b/tests/Unit/Subscription/Request/UpdateAttributeDefinitionRequestTest.php @@ -0,0 +1,49 @@ +name = 'Test Attribute'; + $request->type = 'text'; + $request->order = 5; + $request->defaultValue = 'default'; + $request->required = true; + $request->tableName = 'test_table'; + + $dto = $request->getDto(); + + $this->assertInstanceOf(AttributeDefinitionDto::class, $dto); + $this->assertEquals('Test Attribute', $dto->name); + $this->assertEquals('text', $dto->type); + $this->assertEquals(5, $dto->listOrder); + $this->assertEquals('default', $dto->defaultValue); + $this->assertTrue($dto->required); + $this->assertEquals('test_table', $dto->tableName); + } + + public function testGetDtoWithDefaultValues(): void + { + $request = new UpdateAttributeDefinitionRequest(); + $request->name = 'Test Attribute'; + + $dto = $request->getDto(); + + $this->assertInstanceOf(AttributeDefinitionDto::class, $dto); + $this->assertEquals('Test Attribute', $dto->name); + $this->assertNull($dto->type); + $this->assertNull($dto->listOrder); + $this->assertNull($dto->defaultValue); + $this->assertFalse($dto->required); + $this->assertNull($dto->tableName); + } +} diff --git a/tests/Unit/Subscription/Request/UpdateSubscriberRequestTest.php b/tests/Unit/Subscription/Request/UpdateSubscriberRequestTest.php new file mode 100644 index 0000000..16e5257 --- /dev/null +++ b/tests/Unit/Subscription/Request/UpdateSubscriberRequestTest.php @@ -0,0 +1,35 @@ +subscriberId = 123; + $request->email = 'subscriber@example.com'; + $request->confirmed = true; + $request->blacklisted = false; + $request->htmlEmail = true; + $request->disabled = false; + $request->additionalData = 'Some additional data'; + + $dto = $request->getDto(); + + $this->assertInstanceOf(UpdateSubscriberDto::class, $dto); + $this->assertEquals(123, $dto->subscriberId); + $this->assertEquals('subscriber@example.com', $dto->email); + $this->assertTrue($dto->confirmed); + $this->assertFalse($dto->blacklisted); + $this->assertTrue($dto->htmlEmail); + $this->assertFalse($dto->disabled); + $this->assertEquals('Some additional data', $dto->additionalData); + } +} diff --git a/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php b/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php new file mode 100644 index 0000000..c2b8dc0 --- /dev/null +++ b/tests/Unit/Subscription/Serializer/AttributeDefinitionNormalizerTest.php @@ -0,0 +1,57 @@ +createMock(SubscriberAttributeDefinition::class); + self::assertTrue($normalizer->supportsNormalization($definition)); + + $nonSupported = new \stdClass(); + self::assertFalse($normalizer->supportsNormalization($nonSupported)); + } + + public function testNormalize(): void + { + $definition = $this->createMock(SubscriberAttributeDefinition::class); + $definition->method('getId')->willReturn(1); + $definition->method('getName')->willReturn('Country'); + $definition->method('getType')->willReturn('text'); + $definition->method('getListOrder')->willReturn(12); + $definition->method('getDefaultValue')->willReturn('US'); + $definition->method('isRequired')->willReturn(true); + $definition->method('getTableName')->willReturn('user_attribute'); + + $normalizer = new AttributeDefinitionNormalizer(); + $result = $normalizer->normalize($definition); + + self::assertIsArray($result); + self::assertSame([ + 'id' => 1, + 'name' => 'Country', + 'type' => 'text', + 'list_order' => 12, + 'default_value' => 'US', + 'required' => true, + 'table_name' => 'user_attribute', + ], $result); + } + + public function testNormalizeWithInvalidObjectReturnsEmptyArray(): void + { + $normalizer = new AttributeDefinitionNormalizer(); + $result = $normalizer->normalize(new \stdClass()); + + self::assertSame([], $result); + } +} diff --git a/tests/Unit/Serializer/SubscriberListNormalizerTest.php b/tests/Unit/Subscription/Serializer/SubscriberListNormalizerTest.php similarity index 90% rename from tests/Unit/Serializer/SubscriberListNormalizerTest.php rename to tests/Unit/Subscription/Serializer/SubscriberListNormalizerTest.php index aafde29..1eebf3f 100644 --- a/tests/Unit/Serializer/SubscriberListNormalizerTest.php +++ b/tests/Unit/Subscription/Serializer/SubscriberListNormalizerTest.php @@ -2,11 +2,11 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Unit\Serializer; +namespace PhpList\RestBundle\Tests\Unit\Subscription\Serializer; use DateTime; -use PhpList\Core\Domain\Model\Subscription\SubscriberList; -use PhpList\RestBundle\Serializer\SubscriberListNormalizer; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; +use PhpList\RestBundle\Subscription\Serializer\SubscriberListNormalizer; use PHPUnit\Framework\TestCase; class SubscriberListNormalizerTest extends TestCase diff --git a/tests/Unit/Serializer/SubscriberNormalizerTest.php b/tests/Unit/Subscription/Serializer/SubscriberNormalizerTest.php similarity index 79% rename from tests/Unit/Serializer/SubscriberNormalizerTest.php rename to tests/Unit/Subscription/Serializer/SubscriberNormalizerTest.php index b0d6168..5e8da09 100644 --- a/tests/Unit/Serializer/SubscriberNormalizerTest.php +++ b/tests/Unit/Subscription/Serializer/SubscriberNormalizerTest.php @@ -2,22 +2,23 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Unit\Serializer; +namespace PhpList\RestBundle\Tests\Unit\Subscription\Serializer; use DateTime; -use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Model\Subscription\SubscriberList; -use PhpList\Core\Domain\Model\Subscription\Subscription; -use PhpList\RestBundle\Serializer\SubscriberNormalizer; -use PHPUnit\Framework\TestCase; use Doctrine\Common\Collections\ArrayCollection; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; +use PhpList\Core\Domain\Subscription\Model\Subscription; +use PhpList\RestBundle\Subscription\Serializer\SubscriberListNormalizer; +use PhpList\RestBundle\Subscription\Serializer\SubscriberNormalizer; +use PHPUnit\Framework\TestCase; use stdClass; class SubscriberNormalizerTest extends TestCase { public function testSupportsNormalization(): void { - $normalizer = new SubscriberNormalizer(); + $normalizer = new SubscriberNormalizer(new SubscriberListNormalizer()); $subscriber = $this->createMock(Subscriber::class); $this->assertTrue($normalizer->supportsNormalization($subscriber)); @@ -49,7 +50,7 @@ public function testNormalize(): void $subscriber->method('isDisabled')->willReturn(false); $subscriber->method('getSubscriptions')->willReturn(new ArrayCollection([$subscription])); - $normalizer = new SubscriberNormalizer(); + $normalizer = new SubscriberNormalizer(new SubscriberListNormalizer()); $expected = [ 'id' => 101, @@ -65,10 +66,12 @@ public function testNormalize(): void [ 'id' => 1, 'name' => 'News', - 'description' => 'Latest news', 'created_at' => '2025-01-01T00:00:00+00:00', + 'description' => 'Latest news', + 'list_position' => null, + 'subject_prefix' => null, 'public' => true, - 'subscription_date' => '2025-01-10T00:00:00+00:00' + 'category' => '', ] ] ]; @@ -78,7 +81,7 @@ public function testNormalize(): void public function testNormalizeWithInvalidObject(): void { - $normalizer = new SubscriberNormalizer(); + $normalizer = new SubscriberNormalizer(new SubscriberListNormalizer()); $this->assertSame([], $normalizer->normalize(new stdClass())); } } diff --git a/tests/Unit/Subscription/Serializer/SubscribersExportRequestNormalizerTest.php b/tests/Unit/Subscription/Serializer/SubscribersExportRequestNormalizerTest.php new file mode 100644 index 0000000..b7198c9 --- /dev/null +++ b/tests/Unit/Subscription/Serializer/SubscribersExportRequestNormalizerTest.php @@ -0,0 +1,79 @@ +createMock(SubscribersExportRequest::class); + + $this->assertTrue($normalizer->supportsNormalization($request)); + $this->assertFalse($normalizer->supportsNormalization(new stdClass())); + } + + public function testNormalize(): void + { + $request = new SubscribersExportRequest(); + $request->dateType = 'signup'; + $request->listId = 123; + $request->dateFrom = '2023-01-01'; + $request->dateTo = '2023-12-31'; + $request->columns = ['id', 'email', 'confirmed']; + + $normalizer = new SubscribersExportRequestNormalizer(); + + $expected = [ + 'date_type' => 'signup', + 'list_id' => 123, + 'date_from' => '2023-01-01', + 'date_to' => '2023-12-31', + 'columns' => ['id', 'email', 'confirmed'], + ]; + + $this->assertSame($expected, $normalizer->normalize($request)); + } + + public function testNormalizeWithDefaultValues(): void + { + $request = new SubscribersExportRequest(); + + $normalizer = new SubscribersExportRequestNormalizer(); + + $expected = [ + 'date_type' => 'any', + 'list_id' => null, + 'date_from' => null, + 'date_to' => null, + 'columns' => [ + 'id', + 'email', + 'confirmed', + 'blacklisted', + 'bounceCount', + 'createdAt', + 'updatedAt', + 'uniqueId', + 'htmlEmail', + 'disabled', + 'extraData' + ], + ]; + + $this->assertSame($expected, $normalizer->normalize($request)); + } + + public function testNormalizeWithInvalidObject(): void + { + $normalizer = new SubscribersExportRequestNormalizer(); + $this->assertSame([], $normalizer->normalize(new stdClass())); + } +} diff --git a/tests/Unit/Serializer/SubscriptionNormalizerTest.php b/tests/Unit/Subscription/Serializer/SubscriptionNormalizerTest.php similarity index 75% rename from tests/Unit/Serializer/SubscriptionNormalizerTest.php rename to tests/Unit/Subscription/Serializer/SubscriptionNormalizerTest.php index b7de91f..4e99e5e 100644 --- a/tests/Unit/Serializer/SubscriptionNormalizerTest.php +++ b/tests/Unit/Subscription/Serializer/SubscriptionNormalizerTest.php @@ -2,15 +2,15 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Unit\Serializer; +namespace PhpList\RestBundle\Tests\Unit\Subscription\Serializer; use DateTime; -use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Model\Subscription\SubscriberList; -use PhpList\Core\Domain\Model\Subscription\Subscription; -use PhpList\RestBundle\Serializer\SubscriberListNormalizer; -use PhpList\RestBundle\Serializer\SubscriberNormalizer; -use PhpList\RestBundle\Serializer\SubscriptionNormalizer; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Model\SubscriberList; +use PhpList\Core\Domain\Subscription\Model\Subscription; +use PhpList\RestBundle\Subscription\Serializer\SubscriberListNormalizer; +use PhpList\RestBundle\Subscription\Serializer\SubscriberOnlyNormalizer; +use PhpList\RestBundle\Subscription\Serializer\SubscriptionNormalizer; use PHPUnit\Framework\TestCase; class SubscriptionNormalizerTest extends TestCase @@ -18,7 +18,7 @@ class SubscriptionNormalizerTest extends TestCase public function testSupportsNormalization(): void { $normalizer = new SubscriptionNormalizer( - $this->createMock(SubscriberNormalizer::class), + $this->createMock(SubscriberOnlyNormalizer::class), $this->createMock(SubscriberListNormalizer::class) ); @@ -38,7 +38,7 @@ public function testNormalize(): void $subscription->method('getSubscriberList')->willReturn($subscriberList); $subscription->method('getCreatedAt')->willReturn($subscriptionDate); - $subscriberNormalizer = $this->createMock(SubscriberNormalizer::class); + $subscriberNormalizer = $this->createMock(SubscriberOnlyNormalizer::class); $subscriberListNormalizer = $this->createMock(SubscriberListNormalizer::class); $subscriberNormalizer->method('normalize')->with($subscriber)->willReturn(['subscriber_data']); @@ -58,7 +58,7 @@ public function testNormalize(): void public function testNormalizeWithInvalidObjectReturnsEmptyArray(): void { $normalizer = new SubscriptionNormalizer( - $this->createMock(SubscriberNormalizer::class), + $this->createMock(SubscriberOnlyNormalizer::class), $this->createMock(SubscriberListNormalizer::class) ); diff --git a/tests/Unit/Validator/Constraint/EmailExistsValidatorTest.php b/tests/Unit/Subscription/Validator/Constraint/EmailExistsValidatorTest.php similarity index 89% rename from tests/Unit/Validator/Constraint/EmailExistsValidatorTest.php rename to tests/Unit/Subscription/Validator/Constraint/EmailExistsValidatorTest.php index c989b79..9dccd7b 100644 --- a/tests/Unit/Validator/Constraint/EmailExistsValidatorTest.php +++ b/tests/Unit/Subscription/Validator/Constraint/EmailExistsValidatorTest.php @@ -2,12 +2,12 @@ declare(strict_types=1); -namespace PhpList\RestBundle\Tests\Unit\Validator\Constraint; +namespace PhpList\RestBundle\Tests\Unit\Subscription\Validator\Constraint; -use PhpList\Core\Domain\Model\Subscription\Subscriber; -use PhpList\Core\Domain\Repository\Subscription\SubscriberRepository; -use PhpList\RestBundle\Validator\Constraint\EmailExists; -use PhpList\RestBundle\Validator\Constraint\EmailExistsValidator; +use PhpList\Core\Domain\Subscription\Model\Subscriber; +use PhpList\Core\Domain\Subscription\Repository\SubscriberRepository; +use PhpList\RestBundle\Subscription\Validator\Constraint\EmailExists; +use PhpList\RestBundle\Subscription\Validator\Constraint\EmailExistsValidator; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; diff --git a/tests/Unit/Subscription/Validator/Constraint/ListExistsValidatorTest.php b/tests/Unit/Subscription/Validator/Constraint/ListExistsValidatorTest.php new file mode 100644 index 0000000..fe8c512 --- /dev/null +++ b/tests/Unit/Subscription/Validator/Constraint/ListExistsValidatorTest.php @@ -0,0 +1,86 @@ +subscriberListRepository = $this->createMock(SubscriberListRepository::class); + $context = $this->createMock(ExecutionContextInterface::class); + + $this->validator = new ListExistsValidator($this->subscriberListRepository); + $this->validator->initialize($context); + } + + public function testValidateSkipsNull(): void + { + $this->subscriberListRepository->expects($this->never())->method('find'); + $this->validator->validate(null, new ListExists()); + $this->assertTrue(true); + } + + public function testValidateSkipsEmptyString(): void + { + $this->subscriberListRepository->expects($this->never())->method('find'); + $this->validator->validate('', new ListExists()); + $this->assertTrue(true); + } + + public function testValidateThrowsUnexpectedTypeException(): void + { + $this->expectException(UnexpectedTypeException::class); + $this->validator->validate('123', $this->createMock(Constraint::class)); + } + + public function testValidateThrowsUnexpectedValueException(): void + { + $this->expectException(UnexpectedValueException::class); + $this->validator->validate(123, new ListExists()); + } + + public function testValidateThrowsNotFoundExceptionIfListDoesNotExist(): void + { + $this->subscriberListRepository + ->expects($this->once()) + ->method('find') + ->with('123') + ->willReturn(null); + + $this->expectException(NotFoundHttpException::class); + $this->expectExceptionMessage('Subscriber list does not exists.'); + + $this->validator->validate('123', new ListExists()); + } + + public function testValidatePassesIfListExists(): void + { + $subscriberList = $this->createMock(SubscriberList::class); + + $this->subscriberListRepository + ->expects($this->once()) + ->method('find') + ->with('123') + ->willReturn($subscriberList); + + $this->validator->validate('123', new ListExists()); + $this->assertTrue(true); + } +} diff --git a/tests/Unit/Subscription/Validator/Constraint/UniqueEmailValidatorTest.php b/tests/Unit/Subscription/Validator/Constraint/UniqueEmailValidatorTest.php new file mode 100644 index 0000000..e32852d --- /dev/null +++ b/tests/Unit/Subscription/Validator/Constraint/UniqueEmailValidatorTest.php @@ -0,0 +1,149 @@ +entityManager = $this->createMock(EntityManagerInterface::class); + $this->validator = new UniqueEmailValidator($this->entityManager); + $this->context = $this->createMock(ExecutionContextInterface::class); + $this->validator->initialize($this->context); + } + + public function testThrowsUnexpectedTypeExceptionWhenConstraintIsWrong(): void + { + $this->expectException(UnexpectedTypeException::class); + $this->validator->validate('anything', $this->createMock(Constraint::class)); + } + + public function testSkipsValidationForNullOrEmpty(): void + { + $this->entityManager->expects(self::never())->method('getRepository'); + + $this->validator->validate(null, new UniqueEmail(Subscriber::class)); + $this->validator->validate('', new UniqueEmail(Subscriber::class)); + + $this->addToAssertionCount(1); + } + + public function testThrowsUnexpectedValueExceptionForNonString(): void + { + $this->expectException(UnexpectedValueException::class); + $this->validator->validate(123, new UniqueEmail(Subscriber::class)); + } + + public function testThrowsConflictHttpExceptionWhenEmailAlreadyExistsWithDifferentId(): void + { + $email = 'foo@bar.com'; + + $existingUser = $this->createConfiguredMock(Subscriber::class, [ + 'getId' => 99 + ]); + + $repo = $this->createMock(SubscriberRepository::class); + $repo->expects(self::once()) + ->method('findOneBy') + ->with(['email' => $email]) + ->willReturn($existingUser); + + $this->entityManager + ->expects(self::once()) + ->method('getRepository') + ->with(Subscriber::class) + ->willReturn($repo); + + $dto = new class { + public int $subscriberId = 100; + }; + + $this->context + ->method('getObject') + ->willReturn($dto); + + $this->expectException(ConflictHttpException::class); + $this->expectExceptionMessage('Email already exists.'); + + $this->validator->validate($email, new UniqueEmail(Subscriber::class)); + } + + public function testAllowsSameEmailForSameSubscriberId(): void + { + $email = 'foo@bar.com'; + + $existingUser = $this->createConfiguredMock(Subscriber::class, [ + 'getId' => 100 + ]); + + $repo = $this->createMock(SubscriberRepository::class); + $repo->expects(self::once()) + ->method('findOneBy') + ->with(['email' => $email]) + ->willReturn($existingUser); + + $this->entityManager + ->expects(self::once()) + ->method('getRepository') + ->with(Subscriber::class) + ->willReturn($repo); + + $dto = new class { + public int $subscriberId = 100; + }; + + $this->context + ->method('getObject') + ->willReturn($dto); + + $this->validator->validate($email, new UniqueEmail(Subscriber::class)); + + $this->addToAssertionCount(1); + } + + public function testAllowsUniqueEmailWhenNoExistingSubscriber(): void + { + $repo = $this->createMock(SubscriberRepository::class); + $repo->expects(self::once()) + ->method('findOneBy') + ->with(['email' => 'new@example.com']) + ->willReturn(null); + + $this->entityManager + ->expects(self::once()) + ->method('getRepository') + ->with(Subscriber::class) + ->willReturn($repo); + + $dto = new class { + public int $subscriberId = 200; + }; + + $this->context + ->method('getObject') + ->willReturn($dto); + + $this->validator->validate('new@example.com', new UniqueEmail(Subscriber::class)); + + $this->addToAssertionCount(1); + } +} diff --git a/tests/Unit/Validator/TemplateImageValidatorTest.php b/tests/Unit/Validator/TemplateImageValidatorTest.php deleted file mode 100644 index 35bac9d..0000000 --- a/tests/Unit/Validator/TemplateImageValidatorTest.php +++ /dev/null @@ -1,86 +0,0 @@ -httpClient = $this->createMock(ClientInterface::class); - $this->validator = new TemplateImageValidator($this->httpClient); - } - - public function testThrowsExceptionIfValueIsNotArray(): void - { - $this->expectException(InvalidArgumentException::class); - - $this->validator->validate('not-an-array'); - } - - public function testValidatesFullUrls(): void - { - $context = (new ValidationContext())->set('checkImages', true); - - $this->expectException(ValidatorException::class); - $this->expectExceptionMessageMatches('/not-a-url/'); - - $this->validator->validate(['not-a-url', 'https://valid.url/image.jpg'], $context); - } - - public function testValidatesExistenceWithHttp200(): void - { - $context = (new ValidationContext())->set('checkExternalImages', true); - - $this->httpClient->expects($this->once()) - ->method('request') - ->with('HEAD', 'https://example.com/image.jpg') - ->willReturn(new Response(200)); - - $this->validator->validate(['https://example.com/image.jpg'], $context); - - $this->assertTrue(true); - } - - public function testValidatesExistenceWithHttp404(): void - { - $context = (new ValidationContext())->set('checkExternalImages', true); - - $this->httpClient->expects($this->once()) - ->method('request') - ->with('HEAD', 'https://example.com/missing.jpg') - ->willReturn(new Response(404)); - - $this->expectException(ValidatorException::class); - $this->expectExceptionMessageMatches('/does not exist/'); - - $this->validator->validate(['https://example.com/missing.jpg'], $context); - } - - public function testValidatesExistenceThrowsHttpException(): void - { - $context = (new ValidationContext())->set('checkExternalImages', true); - - $this->httpClient->expects($this->once()) - ->method('request') - ->willThrowException(new \Exception('Connection failed')); - - $this->expectException(ValidatorException::class); - $this->expectExceptionMessageMatches('/could not be validated/'); - - $this->validator->validate(['https://example.com/broken.jpg'], $context); - } -} diff --git a/tests/Unit/Validator/TemplateLinkValidatorTest.php b/tests/Unit/Validator/TemplateLinkValidatorTest.php deleted file mode 100644 index 78f8bd4..0000000 --- a/tests/Unit/Validator/TemplateLinkValidatorTest.php +++ /dev/null @@ -1,66 +0,0 @@ -validator = new TemplateLinkValidator(); - } - - public function testSkipsValidationIfNotString(): void - { - $context = (new ValidationContext())->set('checkLinks', true); - - $this->validator->validate(['not', 'a', 'string'], $context); - - $this->assertTrue(true); - } - - public function testSkipsValidationIfCheckLinksIsFalse(): void - { - $context = (new ValidationContext())->set('checkLinks', false); - - $this->validator->validate('Broken link', $context); - - $this->assertTrue(true); - } - - public function testValidatesInvalidLinks(): void - { - $context = (new ValidationContext())->set('checkLinks', true); - - $html = 'Broken'; - - $this->expectException(ValidatorException::class); - $this->expectExceptionMessageMatches('/invalid-link/'); - - $this->validator->validate($html, $context); - } - - public function testAllowsValidLinksAndPlaceholders(): void - { - $context = (new ValidationContext())->set('checkLinks', true); - - $html = '' . - 'Valid Link' . - 'Valid Link' . - 'Email Link' . - 'Placeholder' . - ''; - - $this->validator->validate($html, $context); - - $this->assertTrue(true); - } -}