Skip to content

Commit f00eebb

Browse files
committed
Add more tests + fix handler
1 parent eda1465 commit f00eebb

File tree

6 files changed

+542
-1
lines changed

6 files changed

+542
-1
lines changed

src/Domain/Messaging/MessageHandler/SubscriptionConfirmationMessageHandler.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ public function __invoke(SubscriptionConfirmationMessage $message): void
4949
$textContent = $this->configProvider->getValue(ConfigOption::SubscribeMessage);
5050
$personalizedTextContent = $this->userPersonalizer->personalize($textContent, $message->getUniqueId());
5151
$listOfLists = $this->getListNames($message->getListIds());
52-
$replacedTextContent = str_replace('[LISTS]', $personalizedTextContent, $listOfLists);
52+
$replacedTextContent = str_replace('[LISTS]', $listOfLists, $personalizedTextContent);
5353

5454
$email = (new Email())
5555
->to($message->getEmail())
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Tests\Unit\Domain\Messaging\MessageHandler;
6+
7+
use PhpList\Core\Domain\Messaging\Message\SubscriptionConfirmationMessage;
8+
use PhpList\Core\Domain\Subscription\Model\SubscriberList;
9+
use PHPUnit\Framework\TestCase;
10+
use PhpList\Core\Domain\Messaging\MessageHandler\SubscriptionConfirmationMessageHandler;
11+
use PhpList\Core\Domain\Messaging\Service\EmailService;
12+
use PhpList\Core\Domain\Configuration\Service\Provider\ConfigProvider;
13+
use PhpList\Core\Domain\Configuration\Service\UserPersonalizer;
14+
use PhpList\Core\Domain\Configuration\Model\ConfigOption;
15+
use PhpList\Core\Domain\Subscription\Repository\SubscriberListRepository;
16+
use Psr\Log\LoggerInterface;
17+
use Symfony\Component\Mime\Email;
18+
19+
/**
20+
* @covers \PhpList\Core\Domain\Messaging\MessageHandler\SubscriptionConfirmationMessageHandler
21+
*/
22+
class SubscriptionConfirmationMessageHandlerTest extends TestCase
23+
{
24+
public function testSendsEmailWithPersonalizedContentAndListNames(): void
25+
{
26+
$emailService = $this->createMock(EmailService::class);
27+
$configProvider = $this->createMock(ConfigProvider::class);
28+
$logger = $this->createMock(LoggerInterface::class);
29+
$personalizer = $this->createMock(UserPersonalizer::class);
30+
$listRepo = $this->createMock(SubscriberListRepository::class);
31+
32+
$handler = new SubscriptionConfirmationMessageHandler(
33+
emailService: $emailService,
34+
configProvider: $configProvider,
35+
logger: $logger,
36+
userPersonalizer: $personalizer,
37+
subscriberListRepository: $listRepo
38+
);
39+
$configProvider
40+
->expects($this->exactly(2))
41+
->method('getValue')
42+
->willReturnMap([
43+
[ConfigOption::SubscribeEmailSubject, 'Please confirm your subscription'],
44+
[ConfigOption::SubscribeMessage, 'Hi {{name}}, you subscribed to: [LISTS]'],
45+
]);
46+
47+
$message = new SubscriptionConfirmationMessage('[email protected]', 'user-123', [10, 11]);
48+
49+
$personalizer->expects($this->once())
50+
->method('personalize')
51+
->with('Hi {{name}}, you subscribed to: [LISTS]', 'user-123')
52+
->willReturn('Hi Alice, you subscribed to: [LISTS]');
53+
54+
$listA = $this->createMock(SubscriberList::class);
55+
$listA->method('getName')->willReturn('Releases');
56+
$listB = $this->createMock(SubscriberList::class);
57+
$listB->method('getName')->willReturn('Security Advisories');
58+
59+
$listRepo->method('find')
60+
->willReturnCallback(function (int $id) use ($listA, $listB) {
61+
return match ($id) {
62+
10 => $listA,
63+
11 => $listB,
64+
default => null
65+
};
66+
});
67+
68+
// Capture the Email object passed to EmailService
69+
$emailService->expects($this->once())
70+
->method('sendEmail')
71+
->with($this->callback(function (Email $email): bool {
72+
$addresses = $email->getTo();
73+
if (count($addresses) !== 1 || $addresses[0]->getAddress() !== '[email protected]') {
74+
return false;
75+
}
76+
if ($email->getSubject() !== 'Please confirm your subscription') {
77+
return false;
78+
}
79+
$body = $email->getTextBody();
80+
return $body === 'Hi Alice, you subscribed to: Releases, Security Advisories';
81+
}));
82+
83+
$logger->expects($this->once())
84+
->method('info')
85+
->with(
86+
'Subscription confirmation email sent to {email}',
87+
['email' => '[email protected]']
88+
);
89+
90+
$handler($message);
91+
}
92+
93+
public function testHandlesMissingListsGracefullyAndEmptyJoin(): void
94+
{
95+
$emailService = $this->createMock(EmailService::class);
96+
$configProvider = $this->createMock(ConfigProvider::class);
97+
$logger = $this->createMock(LoggerInterface::class);
98+
$personalizer = $this->createMock(UserPersonalizer::class);
99+
$listRepo = $this->createMock(SubscriberListRepository::class);
100+
101+
$handler = new SubscriptionConfirmationMessageHandler(
102+
emailService: $emailService,
103+
configProvider: $configProvider,
104+
logger: $logger,
105+
userPersonalizer: $personalizer,
106+
subscriberListRepository: $listRepo
107+
);
108+
109+
$configProvider->method('getValue')
110+
->willReturnMap([
111+
[ConfigOption::SubscribeEmailSubject, 'Please confirm your subscription'],
112+
[ConfigOption::SubscribeMessage, 'Lists: [LISTS]'],
113+
]);
114+
115+
$message = $this->createMock(SubscriptionConfirmationMessage::class);
116+
$message->method('getEmail')->willReturn('[email protected]');
117+
$message->method('getUniqueId')->willReturn('user-456');
118+
$message->method('getListIds')->willReturn([42]);
119+
120+
$personalizer->method('personalize')
121+
->with('Lists: [LISTS]', 'user-456')
122+
->willReturn('Lists: [LISTS]');
123+
124+
$listRepo->method('find')->with(42)->willReturn(null);
125+
126+
$emailService->expects($this->once())
127+
->method('sendEmail')
128+
->with($this->callback(function (Email $email): bool {
129+
// Intended empty replacement when no lists found -> empty string
130+
return $email->getTextBody() === 'Lists: ';
131+
}));
132+
133+
$logger->expects($this->once())
134+
->method('info')
135+
->with('Subscription confirmation email sent to {email}', ['email' => '[email protected]']);
136+
137+
$handler($message);
138+
}
139+
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Tests\Unit\Domain\Subscription\Repository;
6+
7+
use Doctrine\DBAL\ArrayParameterType;
8+
use Doctrine\DBAL\Connection;
9+
use Doctrine\DBAL\Query\QueryBuilder;
10+
use Doctrine\DBAL\Result;
11+
use InvalidArgumentException;
12+
use PhpList\Core\Domain\Subscription\Repository\DynamicListAttrRepository;
13+
use PHPUnit\Framework\TestCase;
14+
15+
class DynamicListAttrRepositoryTest extends TestCase
16+
{
17+
public function testFetchOptionNamesReturnsEmptyForEmptyIds(): void
18+
{
19+
$conn = $this->createMock(Connection::class);
20+
$repo = new DynamicListAttrRepository($conn, 'phplist_');
21+
22+
$this->assertSame([], $repo->fetchOptionNames('valid_table', []));
23+
$this->assertSame([], $repo->fetchOptionNames('valid_table', []));
24+
}
25+
26+
public function testFetchOptionNamesThrowsOnInvalidTable(): void
27+
{
28+
$conn = $this->createMock(Connection::class);
29+
$repo = new DynamicListAttrRepository($conn, 'phplist_');
30+
31+
$this->expectException(InvalidArgumentException::class);
32+
$this->expectExceptionMessage('Invalid list table');
33+
34+
$repo->fetchOptionNames('invalid-table;', [1, 2]);
35+
}
36+
37+
public function testFetchOptionNamesReturnsNames(): void
38+
{
39+
$conn = $this->createMock(Connection::class);
40+
41+
$qb = $this->getMockBuilder(QueryBuilder::class)
42+
->disableOriginalConstructor()
43+
->onlyMethods(['select', 'from', 'where', 'setParameter', 'executeQuery'])
44+
->getMock();
45+
46+
$qb->expects($this->once())
47+
->method('select')
48+
->with('name')
49+
->willReturnSelf();
50+
51+
$qb->expects($this->once())
52+
->method('from')
53+
->with('phplist_listattr_users')
54+
->willReturnSelf();
55+
56+
$qb->expects($this->once())
57+
->method('where')
58+
->with('id IN (:ids)')
59+
->willReturnSelf();
60+
61+
// Expect integer coercion of IDs and correct array parameter type
62+
$qb->expects($this->once())
63+
->method('setParameter')
64+
->with(
65+
'ids',
66+
[1, 2, 3],
67+
ArrayParameterType::INTEGER
68+
)
69+
->willReturnSelf();
70+
71+
// Mock Result
72+
$result = $this->createMock(Result::class);
73+
$result->expects($this->once())
74+
->method('fetchFirstColumn')
75+
->willReturn(['alpha', 'beta', 'gamma']);
76+
77+
$qb->expects($this->once())
78+
->method('executeQuery')
79+
->willReturn($result);
80+
81+
$conn->method('createQueryBuilder')->willReturn($qb);
82+
83+
$repo = new DynamicListAttrRepository($conn, 'phplist_');
84+
$names = $repo->fetchOptionNames('users', [1, '2', 3]);
85+
86+
$this->assertSame(['alpha', 'beta', 'gamma'], $names);
87+
}
88+
89+
public function testFetchSingleOptionNameThrowsOnInvalidTable(): void
90+
{
91+
$conn = $this->createMock(Connection::class);
92+
$repo = new DynamicListAttrRepository($conn, 'phplist_');
93+
94+
$this->expectException(InvalidArgumentException::class);
95+
$this->expectExceptionMessage('Invalid list table');
96+
97+
$repo->fetchSingleOptionName('bad name!', 10);
98+
}
99+
100+
public function testFetchSingleOptionNameReturnsString(): void
101+
{
102+
$conn = $this->createMock(Connection::class);
103+
104+
$qb = $this->getMockBuilder(QueryBuilder::class)
105+
->disableOriginalConstructor()
106+
->onlyMethods(['select', 'from', 'where', 'setParameter', 'executeQuery'])
107+
->getMock();
108+
109+
$qb->expects($this->once())->method('select')->with('name')->willReturnSelf();
110+
$qb->expects($this->once())->method('from')->with('phplist_listattr_ukcountries')->willReturnSelf();
111+
$qb->expects($this->once())->method('where')->with('id = :id')->willReturnSelf();
112+
$qb->expects($this->once())->method('setParameter')->with('id', 42)->willReturnSelf();
113+
114+
$result = $this->createMock(Result::class);
115+
$result->expects($this->once())->method('fetchOne')->willReturn('Bradford');
116+
117+
$qb->expects($this->once())->method('executeQuery')->willReturn($result);
118+
$conn->method('createQueryBuilder')->willReturn($qb);
119+
120+
$repo = new DynamicListAttrRepository($conn, 'phplist_');
121+
$this->assertSame('Bradford', $repo->fetchSingleOptionName('ukcountries', 42));
122+
}
123+
124+
public function testFetchSingleOptionNameReturnsNullWhenNotFound(): void
125+
{
126+
$conn = $this->createMock(Connection::class);
127+
128+
$qb = $this->getMockBuilder(QueryBuilder::class)
129+
->disableOriginalConstructor()
130+
->onlyMethods(['select', 'from', 'where', 'setParameter', 'executeQuery'])
131+
->getMock();
132+
133+
$qb->method('select')->with('name')->willReturnSelf();
134+
$qb->method('from')->with('phplist_listattr_termsofservices')->willReturnSelf();
135+
$qb->method('where')->with('id = :id')->willReturnSelf();
136+
$qb->method('setParameter')->with('id', 999)->willReturnSelf();
137+
138+
$result = $this->createMock(Result::class);
139+
$result->expects($this->once())->method('fetchOne')->willReturn(false);
140+
141+
$qb->method('executeQuery')->willReturn($result);
142+
$conn->method('createQueryBuilder')->willReturn($qb);
143+
144+
$repo = new DynamicListAttrRepository($conn, 'phplist_');
145+
$this->assertNull($repo->fetchSingleOptionName('termsofservices', 999));
146+
}
147+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Tests\Unit\Domain\Subscription\Service;
6+
7+
use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeDefinition;
8+
use PhpList\Core\Domain\Subscription\Model\SubscriberAttributeValue;
9+
use PhpList\Core\Domain\Subscription\Service\Provider\AttributeValueProvider;
10+
use PhpList\Core\Domain\Subscription\Service\Resolver\AttributeValueResolver;
11+
use PHPUnit\Framework\TestCase;
12+
13+
final class AttributeValueResolverTest extends TestCase
14+
{
15+
public function testResolveReturnsEmptyWhenNoProviderSupports(): void
16+
{
17+
$def = $this->createMock(SubscriberAttributeDefinition::class);
18+
$userAttr = $this->createMock(SubscriberAttributeValue::class);
19+
$userAttr->method('getAttributeDefinition')->willReturn($def);
20+
21+
$p1 = $this->createMock(AttributeValueProvider::class);
22+
$p1->expects($this->once())->method('supports')->with($def)->willReturn(false);
23+
$p1->expects($this->never())->method('getValue');
24+
25+
$p2 = $this->createMock(AttributeValueProvider::class);
26+
$p2->expects($this->once())->method('supports')->with($def)->willReturn(false);
27+
$p2->expects($this->never())->method('getValue');
28+
29+
$resolver = new AttributeValueResolver([$p1, $p2]);
30+
31+
self::assertSame('', $resolver->resolve($userAttr));
32+
}
33+
34+
public function testResolveReturnsValueFromFirstSupportingProvider(): void
35+
{
36+
$def = $this->createMock(SubscriberAttributeDefinition::class);
37+
$userAttr = $this->createMock(SubscriberAttributeValue::class);
38+
$userAttr->method('getAttributeDefinition')->willReturn($def);
39+
40+
$nonSupporting = $this->createMock(AttributeValueProvider::class);
41+
$nonSupporting->expects($this->once())->method('supports')->with($def)->willReturn(false);
42+
$nonSupporting->expects($this->never())->method('getValue');
43+
44+
$supporting = $this->createMock(AttributeValueProvider::class);
45+
$supporting->expects($this->once())->method('supports')->with($def)->willReturn(true);
46+
$supporting->expects($this->once())
47+
->method('getValue')
48+
->with($def, $userAttr)
49+
->willReturn('Resolved Value');
50+
51+
// This provider should never be interrogated because resolver exits early.
52+
$afterFirstMatch = $this->createMock(AttributeValueProvider::class);
53+
$afterFirstMatch->expects($this->never())->method('supports');
54+
$afterFirstMatch->expects($this->never())->method('getValue');
55+
56+
$resolver = new AttributeValueResolver([$nonSupporting, $supporting, $afterFirstMatch]);
57+
58+
self::assertSame('Resolved Value', $resolver->resolve($userAttr));
59+
}
60+
61+
public function testResolveHonorsProviderOrderFirstMatchWins(): void
62+
{
63+
$def = $this->createMock(SubscriberAttributeDefinition::class);
64+
$userAttr = $this->createMock(SubscriberAttributeValue::class);
65+
$userAttr->method('getAttributeDefinition')->willReturn($def);
66+
67+
$firstSupporting = $this->createMock(AttributeValueProvider::class);
68+
$firstSupporting->expects($this->once())->method('supports')->with($def)->willReturn(true);
69+
$firstSupporting->expects($this->once())
70+
->method('getValue')
71+
->with($def, $userAttr)
72+
->willReturn('first');
73+
74+
$secondSupporting = $this->createMock(AttributeValueProvider::class);
75+
// Must not be called because the first already matched
76+
$secondSupporting->expects($this->never())->method('supports');
77+
$secondSupporting->expects($this->never())->method('getValue');
78+
79+
$resolver = new AttributeValueResolver([$firstSupporting, $secondSupporting]);
80+
81+
self::assertSame('first', $resolver->resolve($userAttr));
82+
}
83+
}

0 commit comments

Comments
 (0)