Skip to content

Commit 34cab49

Browse files
committed
ISSUE-345: top local parts stats
1 parent 30b8e48 commit 34cab49

File tree

7 files changed

+242
-23
lines changed

7 files changed

+242
-23
lines changed

config/services/normalizers.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,7 @@ services:
8181
PhpList\RestBundle\Statistics\Serializer\TopDomainsNormalizer:
8282
tags: [ 'serializer.normalizer' ]
8383
autowire: true
84+
85+
PhpList\RestBundle\Statistics\Serializer\TopLocalPartsNormalizer:
86+
tags: [ 'serializer.normalizer' ]
87+
autowire: true

src/Statistics/Controller/AnalyticsController.php

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
use PhpList\RestBundle\Common\Validator\RequestValidator;
1313
use PhpList\RestBundle\Statistics\Serializer\CampaignStatisticsNormalizer;
1414
use PhpList\RestBundle\Statistics\Serializer\TopDomainsNormalizer;
15+
use PhpList\RestBundle\Statistics\Serializer\TopLocalPartsNormalizer;
1516
use PhpList\RestBundle\Statistics\Serializer\ViewOpensStatisticsNormalizer;
1617
use Symfony\Component\HttpFoundation\JsonResponse;
1718
use Symfony\Component\HttpFoundation\Request;
@@ -29,20 +30,23 @@ class AnalyticsController extends BaseController
2930
private CampaignStatisticsNormalizer $campaignStatsNormalizer;
3031
private ViewOpensStatisticsNormalizer $viewOpensStatsNormalizer;
3132
private TopDomainsNormalizer $topDomainsNormalizer;
33+
private TopLocalPartsNormalizer $topLocalPartsNormalizer;
3234

3335
public function __construct(
3436
Authentication $authentication,
3537
RequestValidator $validator,
3638
AnalyticsService $analyticsService,
3739
CampaignStatisticsNormalizer $campaignStatsNormalizer,
3840
ViewOpensStatisticsNormalizer $viewOpensStatsNormalizer,
39-
TopDomainsNormalizer $topDomainsNormalizer
41+
TopDomainsNormalizer $topDomainsNormalizer,
42+
TopLocalPartsNormalizer $topLocalPartsNormalizer
4043
) {
4144
parent::__construct($authentication, $validator);
4245
$this->analyticsService = $analyticsService;
4346
$this->campaignStatsNormalizer = $campaignStatsNormalizer;
4447
$this->viewOpensStatsNormalizer = $viewOpensStatsNormalizer;
4548
$this->topDomainsNormalizer = $topDomainsNormalizer;
49+
$this->topLocalPartsNormalizer = $topLocalPartsNormalizer;
4650
}
4751

4852
#[Route('/campaigns', name: 'campaign_statistics', methods: ['GET'])]
@@ -337,24 +341,7 @@ public function getDomainConfirmationStatistics(Request $request): JsonResponse
337341
new OA\Response(
338342
response: 200,
339343
description: 'Success',
340-
content: new OA\JsonContent(
341-
properties: [
342-
new OA\Property(
343-
property: 'local_parts',
344-
type: 'array',
345-
items: new OA\Items(
346-
properties: [
347-
new OA\Property(property: 'local_part', type: 'string'),
348-
new OA\Property(property: 'count', type: 'integer'),
349-
new OA\Property(property: 'percentage', type: 'number', format: 'float'),
350-
],
351-
type: 'object'
352-
)
353-
),
354-
new OA\Property(property: 'total', type: 'integer'),
355-
],
356-
type: 'object'
357-
)
344+
content: new OA\JsonContent(ref: '#/components/schemas/LocalPartsStats')
358345
),
359346
new OA\Response(
360347
response: 403,
@@ -373,7 +360,10 @@ public function getTopLocalParts(Request $request): JsonResponse
373360
$limit = (int) $request->query->get('limit', 25);
374361

375362
$data = $this->analyticsService->getTopLocalParts($limit);
363+
$normalizedData = $this->topLocalPartsNormalizer->normalize($data, null, [
364+
'top_local_parts' => true,
365+
]);
376366

377-
return $this->json($data, Response::HTTP_OK);
367+
return $this->json($normalizedData, Response::HTTP_OK);
378368
}
379369
}

src/Statistics/OpenApi/SwaggerSchemasResponse.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,27 @@
103103
type: 'object',
104104
nullable: true
105105
)]
106+
#[OA\Schema(
107+
schema: 'LocalPartsStats',
108+
properties: [
109+
new OA\Property(
110+
property: 'local_parts',
111+
type: 'array',
112+
items: new OA\Items(
113+
properties: [
114+
new OA\Property(property: 'local_part', type: 'string'),
115+
new OA\Property(property: 'count', type: 'integer'),
116+
new OA\Property(property: 'percentage', type: 'number', format: 'float'),
117+
],
118+
type: 'object'
119+
)
120+
),
121+
new OA\Property(property: 'total', type: 'integer'),
122+
],
123+
type: 'object',
124+
nullable: true
125+
)]
126+
106127
class SwaggerSchemasResponse
107128
{
108129
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Statistics\Serializer;
6+
7+
use Symfony\Component\Serializer\Normalizer\NormalizerInterface;
8+
9+
class TopLocalPartsNormalizer implements NormalizerInterface
10+
{
11+
/**
12+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
13+
*/
14+
public function normalize(mixed $object, string $format = null, array $context = []): array
15+
{
16+
if (!is_array($object)) {
17+
return [];
18+
}
19+
20+
$localParts = [];
21+
foreach ($object['localParts'] ?? [] as $localPart) {
22+
$localParts[] = [
23+
'local_part' => $localPart['localPart'] ?? '',
24+
'count' => $localPart['count'] ?? 0,
25+
'percentage' => $localPart['percentage'] ?? 0.0,
26+
];
27+
}
28+
29+
return [
30+
'local_parts' => $localParts,
31+
'total' => $object['total'] ?? 0,
32+
];
33+
}
34+
35+
/**
36+
* @SuppressWarnings(PHPMD.UnusedFormalParameter)
37+
*/
38+
public function supportsNormalization(mixed $data, string $format = null, array $context = []): bool
39+
{
40+
return is_array($data) && isset($context['top_local_parts']);
41+
}
42+
}

tests/Integration/Statistics/Controller/AnalyticsControllerTest.php

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,34 @@ public function testGetTopLocalPartsReturnsLocalPartsData(): void
224224
$response = $this->getDecodedJsonResponseContent();
225225

226226
self::assertIsArray($response);
227-
self::assertArrayHasKey('localParts', $response);
227+
self::assertArrayHasKey('local_parts', $response);
228228
self::assertArrayHasKey('total', $response);
229+
self::assertIsArray($response['local_parts']);
230+
self::assertIsInt($response['total']);
231+
}
232+
233+
public function testGetTopLocalPartsWithLimitParameter(): void
234+
{
235+
$this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class, SubscriberFixture::class]);
236+
237+
$this->authenticatedJsonRequest('GET', '/api/v2/analytics/local-parts/top?limit=5');
238+
$response = $this->getDecodedJsonResponseContent();
239+
240+
self::assertIsArray($response);
241+
self::assertArrayHasKey('local_parts', $response);
242+
self::assertIsArray($response['local_parts']);
243+
self::assertLessThanOrEqual(5, count($response['local_parts']));
244+
}
245+
246+
public function testGetTopLocalPartsWithInvalidLimitParameter(): void
247+
{
248+
$this->loadFixtures([AdministratorFixture::class, AdministratorTokenFixture::class, SubscriberFixture::class]);
249+
250+
$this->authenticatedJsonRequest('GET', '/api/v2/analytics/local-parts/top?limit=invalid');
251+
$response = $this->getDecodedJsonResponseContent();
252+
253+
self::assertIsArray($response);
254+
self::assertArrayHasKey('local_parts', $response);
255+
self::assertIsArray($response['local_parts']);
229256
}
230257
}

tests/Unit/Statistics/Controller/AnalyticsControllerTest.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
use PhpList\RestBundle\Common\Validator\RequestValidator;
1313
use PhpList\RestBundle\Statistics\Controller\AnalyticsController;
1414
use PhpList\RestBundle\Statistics\Serializer\CampaignStatisticsNormalizer;
15+
use PhpList\RestBundle\Statistics\Serializer\TopDomainsNormalizer;
16+
use PhpList\RestBundle\Statistics\Serializer\TopLocalPartsNormalizer;
1517
use PhpList\RestBundle\Statistics\Serializer\ViewOpensStatisticsNormalizer;
1618
use PhpList\RestBundle\Tests\Helpers\DummyAnalyticsController;
1719
use PHPUnit\Framework\MockObject\MockObject;
@@ -36,12 +38,15 @@ protected function setUp(): void
3638
$this->analyticsService = $this->createMock(AnalyticsService::class);
3739
$campaignStatisticsNormalizer = new CampaignStatisticsNormalizer();
3840
$viewOpensStatisticsNormalizer = new ViewOpensStatisticsNormalizer();
41+
$topDomainsNormalizer = new TopDomainsNormalizer();
3942
$this->controller = new DummyAnalyticsController(
4043
$this->authentication,
4144
$validator,
4245
$this->analyticsService,
4346
$campaignStatisticsNormalizer,
4447
$viewOpensStatisticsNormalizer,
48+
$topDomainsNormalizer,
49+
new TopLocalPartsNormalizer()
4550
);
4651

4752
$this->privileges = $this->createMock(Privileges::class);
@@ -425,8 +430,16 @@ public function testGetTopLocalPartsReturnsJsonResponse(): void
425430

426431
$response = $this->controller->getTopLocalParts($request);
427432

428-
self::assertInstanceOf(JsonResponse::class, $response);
429433
self::assertEquals(Response::HTTP_OK, $response->getStatusCode());
430-
self::assertEquals($expectedData, json_decode($response->getContent(), true));
434+
self::assertEquals([
435+
'local_parts' => [
436+
[
437+
'local_part' => 'info',
438+
'count' => 30,
439+
'percentage' => 60.0,
440+
]
441+
],
442+
'total' => 1,
443+
], json_decode($response->getContent(), true));
431444
}
432445
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\RestBundle\Tests\Unit\Statistics\Serializer;
6+
7+
use PhpList\RestBundle\Statistics\Serializer\TopLocalPartsNormalizer;
8+
use PHPUnit\Framework\TestCase;
9+
10+
class TopLocalPartsNormalizerTest extends TestCase
11+
{
12+
public function testNormalizeWithValidData(): void
13+
{
14+
$data = [
15+
'localParts' => [
16+
['localPart' => 'john', 'count' => 100, 'percentage' => 40.0],
17+
['localPart' => 'info', 'count' => 50, 'percentage' => 20.0],
18+
],
19+
'total' => 250,
20+
];
21+
22+
$normalizer = new TopLocalPartsNormalizer();
23+
$result = $normalizer->normalize($data);
24+
25+
$this->assertIsArray($result);
26+
$this->assertArrayHasKey('local_parts', $result);
27+
$this->assertArrayHasKey('total', $result);
28+
$this->assertEquals(250, $result['total']);
29+
$this->assertCount(2, $result['local_parts']);
30+
$this->assertEquals('john', $result['local_parts'][0]['local_part']);
31+
$this->assertEquals(100, $result['local_parts'][0]['count']);
32+
$this->assertEquals(40.0, $result['local_parts'][0]['percentage']);
33+
$this->assertEquals('info', $result['local_parts'][1]['local_part']);
34+
$this->assertEquals(50, $result['local_parts'][1]['count']);
35+
$this->assertEquals(20.0, $result['local_parts'][1]['percentage']);
36+
}
37+
38+
public function testNormalizeWithMissingFields(): void
39+
{
40+
$data = [
41+
'localParts' => [
42+
['localPart' => 'john'],
43+
['count' => 50],
44+
['percentage' => 20.0],
45+
[],
46+
],
47+
];
48+
49+
$normalizer = new TopLocalPartsNormalizer();
50+
$result = $normalizer->normalize($data);
51+
52+
$this->assertIsArray($result);
53+
$this->assertArrayHasKey('local_parts', $result);
54+
$this->assertArrayHasKey('total', $result);
55+
$this->assertEquals(0, $result['total']);
56+
$this->assertCount(4, $result['local_parts']);
57+
$this->assertEquals('john', $result['local_parts'][0]['local_part']);
58+
$this->assertEquals(0, $result['local_parts'][0]['count']);
59+
$this->assertEquals(0.0, $result['local_parts'][0]['percentage']);
60+
$this->assertEquals('', $result['local_parts'][1]['local_part']);
61+
$this->assertEquals(50, $result['local_parts'][1]['count']);
62+
$this->assertEquals(0.0, $result['local_parts'][1]['percentage']);
63+
$this->assertEquals('', $result['local_parts'][2]['local_part']);
64+
$this->assertEquals(0, $result['local_parts'][2]['count']);
65+
$this->assertEquals(20.0, $result['local_parts'][2]['percentage']);
66+
$this->assertEquals('', $result['local_parts'][3]['local_part']);
67+
$this->assertEquals(0, $result['local_parts'][3]['count']);
68+
$this->assertEquals(0.0, $result['local_parts'][3]['percentage']);
69+
}
70+
71+
public function testNormalizeWithEmptyLocalParts(): void
72+
{
73+
$data = [
74+
'localParts' => [],
75+
'total' => 0,
76+
];
77+
78+
$normalizer = new TopLocalPartsNormalizer();
79+
$result = $normalizer->normalize($data);
80+
81+
$this->assertIsArray($result);
82+
$this->assertArrayHasKey('local_parts', $result);
83+
$this->assertArrayHasKey('total', $result);
84+
$this->assertEquals(0, $result['total']);
85+
$this->assertEmpty($result['local_parts']);
86+
}
87+
88+
public function testNormalizeWithNoLocalParts(): void
89+
{
90+
$data = [
91+
'total' => 100,
92+
];
93+
94+
$normalizer = new TopLocalPartsNormalizer();
95+
$result = $normalizer->normalize($data);
96+
97+
$this->assertIsArray($result);
98+
$this->assertArrayHasKey('local_parts', $result);
99+
$this->assertArrayHasKey('total', $result);
100+
$this->assertEquals(100, $result['total']);
101+
$this->assertEmpty($result['local_parts']);
102+
}
103+
104+
public function testNormalizeWithInvalidObject(): void
105+
{
106+
$normalizer = new TopLocalPartsNormalizer();
107+
$result = $normalizer->normalize('not an array');
108+
109+
$this->assertIsArray($result);
110+
$this->assertEmpty($result);
111+
}
112+
113+
public function testSupportsNormalization(): void
114+
{
115+
$normalizer = new TopLocalPartsNormalizer();
116+
117+
$this->assertTrue($normalizer->supportsNormalization([], null, ['top_local_parts' => true]));
118+
$this->assertFalse($normalizer->supportsNormalization([], null, []));
119+
$this->assertFalse($normalizer->supportsNormalization('not an array', null, ['top_local_parts' => true]));
120+
$this->assertFalse($normalizer->supportsNormalization(new \stdClass(), null, ['top_local_parts' => true]));
121+
}
122+
}

0 commit comments

Comments
 (0)