From 73082a43ef107ce335a31181effc97c255732054 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Thu, 3 Sep 2026 11:55:38 +0400 Subject: [PATCH 1/4] Refactor normalizers to use SubscriberHistoryRecordInterface and add test for Elasticsearch-backed history --- composer.json | 2 +- .../SubscriberHistoryNormalizer.php | 13 +++--- .../Serializer/SubscriberNormalizer.php | 4 +- .../Serializer/SubscriberNormalizerTest.php | 44 +++++++++++++++++++ 4 files changed, 54 insertions(+), 9 deletions(-) diff --git a/composer.json b/composer.json index 11174c5..ddad518 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ }, "require": { "php": "^8.1", - "phplist/core": "dev-dev", + "phplist/core": "dev-elasticsearch", "friendsofsymfony/rest-bundle": "*", "symfony/test-pack": "^1.0", "symfony/process": "^6.4", diff --git a/src/Subscription/Serializer/SubscriberHistoryNormalizer.php b/src/Subscription/Serializer/SubscriberHistoryNormalizer.php index 760207c..cd5b732 100644 --- a/src/Subscription/Serializer/SubscriberHistoryNormalizer.php +++ b/src/Subscription/Serializer/SubscriberHistoryNormalizer.php @@ -4,8 +4,9 @@ namespace PhpList\RestBundle\Subscription\Serializer; +use DateTimeInterface; use OpenApi\Attributes as OA; -use PhpList\Core\Domain\Subscription\Model\SubscriberHistory; +use PhpList\Core\Domain\Subscription\Model\Interfaces\SubscriberHistoryRecordInterface; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; #[OA\Schema( @@ -32,14 +33,14 @@ class SubscriberHistoryNormalizer implements NormalizerInterface */ public function normalize($object, string $format = null, array $context = []): array { - if (!$object instanceof SubscriberHistory) { + if (!$object instanceof SubscriberHistoryRecordInterface) { return []; } return [ 'id' => $object->getId(), 'ip' => $object->getIp(), - 'created_at' => $object->getCreatedAt()->format(\DateTimeInterface::ATOM), + 'created_at' => $object->getCreatedAt()->format(DateTimeInterface::ATOM), 'summary' => $object->getSummary(), 'detail' => $object->getDetail(), 'system_info' => $object->getSystemInfo(), @@ -51,7 +52,7 @@ public function normalize($object, string $format = null, array $context = []): */ public function supportsNormalization($data, string $format = null): bool { - return $data instanceof SubscriberHistory; + return $data instanceof SubscriberHistoryRecordInterface; } /** @@ -60,7 +61,7 @@ public function supportsNormalization($data, string $format = null): bool public function getSupportedTypes(?string $format): array { return [ - SubscriberHistory::class => true, + SubscriberHistoryRecordInterface::class => true, ]; } -} +} \ No newline at end of file diff --git a/src/Subscription/Serializer/SubscriberNormalizer.php b/src/Subscription/Serializer/SubscriberNormalizer.php index 0909b64..4ab4830 100644 --- a/src/Subscription/Serializer/SubscriberNormalizer.php +++ b/src/Subscription/Serializer/SubscriberNormalizer.php @@ -6,8 +6,8 @@ use DateTimeInterface; use OpenApi\Attributes as OA; +use PhpList\Core\Domain\Subscription\Model\Interfaces\SubscriberHistoryRecordInterface; use PhpList\Core\Domain\Subscription\Model\Subscriber; -use PhpList\Core\Domain\Subscription\Model\SubscriberHistory; use PhpList\Core\Domain\Subscription\Model\Subscription; use Symfony\Component\Serializer\Normalizer\NormalizerInterface; @@ -80,7 +80,7 @@ public function normalize($object, string $format = null, array $context = []): 'subscribed_lists' => array_map(function (Subscription $subscription) { return $this->subscriberListNormalizer->normalize($subscription->getSubscriberList()); }, $object->getSubscriptions()->toArray()), - 'history' => array_map(function (SubscriberHistory $history) { + 'history' => array_map(function (SubscriberHistoryRecordInterface $history) { return $this->subscriberHistoryNormalizer->normalize($history); }, $object->getHistory()), ]; diff --git a/tests/Unit/Subscription/Serializer/SubscriberNormalizerTest.php b/tests/Unit/Subscription/Serializer/SubscriberNormalizerTest.php index ebb1bbc..53e521e 100644 --- a/tests/Unit/Subscription/Serializer/SubscriberNormalizerTest.php +++ b/tests/Unit/Subscription/Serializer/SubscriberNormalizerTest.php @@ -6,6 +6,7 @@ use DateTime; use Doctrine\Common\Collections\ArrayCollection; +use PhpList\Core\Domain\Subscription\Model\ReadModel\SubscriberHistoryReadModel; use PhpList\Core\Domain\Subscription\Model\Subscriber; use PhpList\Core\Domain\Subscription\Model\SubscriberList; use PhpList\Core\Domain\Subscription\Model\Subscription; @@ -91,4 +92,47 @@ public function testNormalizeWithInvalidObject(): void $normalizer = new SubscriberNormalizer(new SubscriberListNormalizer(), new SubscriberHistoryNormalizer()); $this->assertSame([], $normalizer->normalize(new stdClass())); } + + public function testNormalizeAcceptsElasticsearchBackedHistoryReadModels(): void + { + $history = new SubscriberHistoryReadModel( + id: 7, + subscriberId: 101, + ip: '127.0.0.1', + createdAt: new DateTime('2025-02-01T00:00:00+00:00'), + summary: 'Updated', + detail: 'Detail', + systemInfo: 'Info', + ); + + $subscriber = $this->createMock(Subscriber::class); + $subscriber->method('getId')->willReturn(101); + $subscriber->method('getEmail')->willReturn('test@example.com'); + $subscriber->method('getCreatedAt')->willReturn(new DateTime('2024-12-31T12:00:00+00:00')); + $subscriber->method('getUpdatedAt')->willReturn(new DateTime('2024-12-31T12:00:00+00:00')); + $subscriber->method('isConfirmed')->willReturn(true); + $subscriber->method('isBlacklisted')->willReturn(false); + $subscriber->method('getBounceCount')->willReturn(0); + $subscriber->method('getUniqueId')->willReturn('abc123'); + $subscriber->method('getUuid')->willReturn('abc-123-abc-123'); + $subscriber->method('hasHtmlEmail')->willReturn(true); + $subscriber->method('isDisabled')->willReturn(false); + $subscriber->method('getSubscriptions')->willReturn(new ArrayCollection([])); + $subscriber->method('getHistory')->willReturn([$history]); + + $normalizer = new SubscriberNormalizer(new SubscriberListNormalizer(), new SubscriberHistoryNormalizer()); + + $result = $normalizer->normalize($subscriber); + + $this->assertSame([ + [ + 'id' => 7, + 'ip' => '127.0.0.1', + 'created_at' => '2025-02-01T00:00:00+00:00', + 'summary' => 'Updated', + 'detail' => 'Detail', + 'system_info' => 'Info', + ], + ], $result['history']); + } } From 3060886f43422cc77e3eef3b7416f23c19696eac Mon Sep 17 00:00:00 2001 From: Tatevik Date: Thu, 3 Sep 2026 13:54:45 +0400 Subject: [PATCH 2/4] Refactor BounceController to use UserMessageBounceReportReaderInterface and add regression tests for bounce counts --- composer.json | 1 + src/Messaging/Controller/BounceController.php | 8 +++--- .../SubscriberHistoryNormalizer.php | 2 +- .../Controller/BounceControllerTest.php | 28 +++++++++++++++++++ 4 files changed, 34 insertions(+), 5 deletions(-) diff --git a/composer.json b/composer.json index ddad518..611f7e9 100644 --- a/composer.json +++ b/composer.json @@ -43,6 +43,7 @@ "require": { "php": "^8.1", "phplist/core": "dev-elasticsearch", + "elasticsearch/elasticsearch": "^8.9", "friendsofsymfony/rest-bundle": "*", "symfony/test-pack": "^1.0", "symfony/process": "^6.4", diff --git a/src/Messaging/Controller/BounceController.php b/src/Messaging/Controller/BounceController.php index 216e237..0deff11 100644 --- a/src/Messaging/Controller/BounceController.php +++ b/src/Messaging/Controller/BounceController.php @@ -8,7 +8,7 @@ use OpenApi\Attributes as OA; use PhpList\Core\Domain\Messaging\Model\Bounce; use PhpList\Core\Domain\Messaging\Repository\BounceRepository; -use PhpList\Core\Domain\Messaging\Repository\UserMessageBounceRepository; +use PhpList\Core\Domain\Messaging\Repository\Interfaces\UserMessageBounceReportReaderInterface; use PhpList\Core\Domain\Identity\Service\Authentication; use PhpList\RestBundle\Common\Controller\BaseController; use PhpList\RestBundle\Common\Service\Provider\PaginatedDataProvider; @@ -34,7 +34,7 @@ public function __construct( private readonly EntityManagerInterface $entityManager, private readonly BounceNormalizer $normalizer, private readonly PaginatedDataProvider $paginatedProvider, - private readonly UserMessageBounceRepository $userMessageBounceRepository + private readonly UserMessageBounceReportReaderInterface $userMessageBounceReportReader ) { parent::__construct($authentication, $validator); } @@ -212,7 +212,7 @@ public function getBounceCountsByCampaign(Request $request): JsonResponse $authUser = $this->requireAuthentication($request); return $this->json( - data: $this->userMessageBounceRepository->getCampaignBounceTotals($authUser->getId()), + data: $this->userMessageBounceReportReader->getCampaignBounceTotals($authUser->getId()), status: Response::HTTP_OK ); } @@ -263,7 +263,7 @@ public function getBounceCountsBySubscriber(Request $request): JsonResponse $authUser = $this->requireAuthentication($request); return $this->json( - data: $this->userMessageBounceRepository->getListBounceTotals($authUser->getId()), + data: $this->userMessageBounceReportReader->getListBounceTotals($authUser->getId()), status: Response::HTTP_OK ); } diff --git a/src/Subscription/Serializer/SubscriberHistoryNormalizer.php b/src/Subscription/Serializer/SubscriberHistoryNormalizer.php index cd5b732..fd1cd1c 100644 --- a/src/Subscription/Serializer/SubscriberHistoryNormalizer.php +++ b/src/Subscription/Serializer/SubscriberHistoryNormalizer.php @@ -64,4 +64,4 @@ public function getSupportedTypes(?string $format): array SubscriberHistoryRecordInterface::class => true, ]; } -} \ No newline at end of file +} diff --git a/tests/Integration/Messaging/Controller/BounceControllerTest.php b/tests/Integration/Messaging/Controller/BounceControllerTest.php index 680c6ad..0787188 100644 --- a/tests/Integration/Messaging/Controller/BounceControllerTest.php +++ b/tests/Integration/Messaging/Controller/BounceControllerTest.php @@ -110,4 +110,32 @@ public function testDeleteWithValidIdReturnsNoContentAndRemovesBounce(): void $this->entityManager->clear(); self::assertNull($this->entityManager->getRepository(Bounce::class)->find($bounceId)); } + + public function testGetBounceCountsByCampaignWithoutSessionKeyReturnsUnauthorized(): void + { + self::getClient()->request('GET', '/api/v2/bounces/by/campaign'); + $this->assertHttpUnauthorized(); + } + + public function testGetBounceCountsByCampaignWithValidSessionKeyReturnsArray(): void + { + // Regression check for wiring BounceController to UserMessageBounceReportReaderInterface + // (Elasticsearch-backed by default) - exercises the real service graph end to end. + $this->authenticatedJsonRequest('GET', '/api/v2/bounces/by/campaign'); + $this->assertHttpOkay(); + } + + public function testGetBounceCountsBySubscriberWithoutSessionKeyReturnsUnauthorized(): void + { + self::getClient()->request('GET', '/api/v2/bounces/by/subscriber'); + $this->assertHttpUnauthorized(); + } + + public function testGetBounceCountsBySubscriberWithValidSessionKeyReturnsArray(): void + { + // Regression check for wiring BounceController to UserMessageBounceReportReaderInterface + // (Elasticsearch-backed by default) - exercises the real service graph end to end. + $this->authenticatedJsonRequest('GET', '/api/v2/bounces/by/subscriber'); + $this->assertHttpOkay(); + } } From ac9ac63489c1832b3fb80cd7d971d92325e787aa Mon Sep 17 00:00:00 2001 From: Tatevik Date: Thu, 3 Sep 2026 16:27:05 +0400 Subject: [PATCH 3/4] Add conditional checks for OpenAPI generation in CI configuration --- .github/workflows/client-docs.yml | 1 + .github/workflows/front-docs.yml | 1 + .../Vallidator/Constraint/UniqueLoginNameValidatorTest.php | 2 +- 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/client-docs.yml b/.github/workflows/client-docs.yml index 33eee07..cbf6eea 100644 --- a/.github/workflows/client-docs.yml +++ b/.github/workflows/client-docs.yml @@ -11,6 +11,7 @@ on: jobs: generate-openapi: + if: (github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'dev')) || (github.event_name == 'pull_request' && github.base_ref == 'main') runs-on: ubuntu-22.04 outputs: source_branch: ${{ steps.branch.outputs.source_branch }} diff --git a/.github/workflows/front-docs.yml b/.github/workflows/front-docs.yml index 0aac103..0b4784c 100644 --- a/.github/workflows/front-docs.yml +++ b/.github/workflows/front-docs.yml @@ -14,6 +14,7 @@ on: - main jobs: generate-openapi: + if: (github.event_name == 'push' && (github.ref_name == 'main' || github.ref_name == 'dev')) || (github.event_name == 'pull_request' && github.base_ref == 'main') runs-on: ubuntu-22.04 outputs: source_branch: ${{ steps.branch.outputs.source_branch }} diff --git a/tests/Unit/Identity/Vallidator/Constraint/UniqueLoginNameValidatorTest.php b/tests/Unit/Identity/Vallidator/Constraint/UniqueLoginNameValidatorTest.php index 3fda8c7..d96559b 100644 --- a/tests/Unit/Identity/Vallidator/Constraint/UniqueLoginNameValidatorTest.php +++ b/tests/Unit/Identity/Vallidator/Constraint/UniqueLoginNameValidatorTest.php @@ -63,7 +63,7 @@ public function testValidateSkipsConflictIfSameAdministrator(): void $context = $this->createMock(ExecutionContextInterface::class); $dto = new class { - public $updatingId = 1; + public int $updatingId = 1; }; $context->method('getObject')->willReturn($dto); From b2054971fbc0b5eaf74d984199332c3e99207dd6 Mon Sep 17 00:00:00 2001 From: Tatevik Date: Mon, 7 Sep 2026 12:17:58 +0400 Subject: [PATCH 4/4] Update phplist/core dependency to dev-dev version --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 611f7e9..ba3e52f 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,7 @@ }, "require": { "php": "^8.1", - "phplist/core": "dev-elasticsearch", + "phplist/core": "dev-dev", "elasticsearch/elasticsearch": "^8.9", "friendsofsymfony/rest-bundle": "*", "symfony/test-pack": "^1.0",