Skip to content

Commit 65253d5

Browse files
committed
More tests
1 parent 2d9f46b commit 65253d5

12 files changed

+1129
-5
lines changed
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service;
6+
7+
use PhpList\Core\Domain\Messaging\Model\SendProcess;
8+
use PhpList\Core\Domain\Messaging\Repository\SendProcessRepository;
9+
use PhpList\Core\Domain\Messaging\Service\LockService;
10+
use PhpList\Core\Domain\Messaging\Service\Manager\SendProcessManager;
11+
use PHPUnit\Framework\MockObject\MockObject;
12+
use PHPUnit\Framework\TestCase;
13+
use Psr\Log\LoggerInterface;
14+
15+
class LockServiceTest extends TestCase
16+
{
17+
private SendProcessRepository&MockObject $repo;
18+
private SendProcessManager&MockObject $manager;
19+
private LoggerInterface&MockObject $logger;
20+
21+
protected function setUp(): void
22+
{
23+
$this->repo = $this->createMock(SendProcessRepository::class);
24+
$this->manager = $this->createMock(SendProcessManager::class);
25+
$this->logger = $this->createMock(LoggerInterface::class);
26+
}
27+
28+
public function testAcquirePageLockCreatesProcessWhenBelowMax(): void
29+
{
30+
$service = new LockService($this->repo, $this->manager, $this->logger, 600, 0, 0);
31+
32+
$this->repo->method('countAliveByPage')->willReturn(0);
33+
$this->manager->method('findNewestAliveWithAge')->willReturn(null);
34+
35+
$sendProcess = $this->createConfiguredMock(SendProcess::class, ['getId' => 42]);
36+
$this->manager->expects($this->once())
37+
->method('create')
38+
->with('mypage', $this->callback(fn(string $id) => $id !== ''))
39+
->willReturn($sendProcess);
40+
41+
$id = $service->acquirePageLock('my page');
42+
$this->assertSame(42, $id);
43+
}
44+
45+
public function testAcquirePageLockReturnsNullWhenAtMaxInCli(): void
46+
{
47+
$service = new LockService($this->repo, $this->manager, $this->logger, 600, 0, 0);
48+
49+
$this->repo->method('countAliveByPage')->willReturn(1);
50+
$this->manager->method('findNewestAliveWithAge')->willReturn(['age' => 1, 'id' => 10]);
51+
52+
$this->logger->expects($this->atLeastOnce())->method('info');
53+
$id = $service->acquirePageLock('page', false, true, false, 1);
54+
$this->assertNull($id);
55+
}
56+
57+
public function testAcquirePageLockStealsStale(): void
58+
{
59+
$service = new LockService($this->repo, $this->manager, $this->logger, 1, 0, 0);
60+
61+
$this->repo->expects($this->exactly(2))->method('countAliveByPage')->willReturnOnConsecutiveCalls(1, 0);
62+
$this->manager
63+
->expects($this->exactly(2))
64+
->method('findNewestAliveWithAge')
65+
->willReturnOnConsecutiveCalls(['age' => 5, 'id' => 10], null);
66+
$this->repo->expects($this->once())->method('markDeadById')->with(10);
67+
68+
$sendProcess = $this->createConfiguredMock(SendProcess::class, ['getId' => 99]);
69+
$this->manager->method('create')->willReturn($sendProcess);
70+
71+
$id = $service->acquirePageLock('page', false, true);
72+
$this->assertSame(99, $id);
73+
}
74+
75+
public function testKeepCheckReleaseDelegatesToRepo(): void
76+
{
77+
$service = new LockService($this->repo, $this->manager, $this->logger);
78+
79+
$this->repo->expects($this->once())->method('incrementAlive')->with(5);
80+
$service->keepLock(5);
81+
82+
$this->repo->expects($this->once())->method('getAliveValue')->with(5)->willReturn(7);
83+
$this->assertSame(7, $service->checkLock(5));
84+
85+
$this->repo->expects($this->once())->method('markDeadById')->with(5);
86+
$service->release(5);
87+
}
88+
}

tests/Unit/Domain/Messaging/Service/Manager/BounceManagerTest.php

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,27 +7,34 @@
77
use DateTimeImmutable;
88
use Doctrine\ORM\EntityManagerInterface;
99
use PhpList\Core\Domain\Messaging\Model\Bounce;
10+
use PhpList\Core\Domain\Messaging\Model\UserMessageBounce;
1011
use PhpList\Core\Domain\Messaging\Repository\BounceRepository;
1112
use PhpList\Core\Domain\Messaging\Repository\UserMessageBounceRepository;
1213
use PhpList\Core\Domain\Messaging\Service\Manager\BounceManager;
14+
use PhpList\Core\Domain\Subscription\Model\Subscriber;
1315
use PHPUnit\Framework\MockObject\MockObject;
1416
use PHPUnit\Framework\TestCase;
1517
use Psr\Log\LoggerInterface;
1618

1719
class BounceManagerTest extends TestCase
1820
{
1921
private BounceRepository&MockObject $repository;
22+
private UserMessageBounceRepository&MockObject $userMessageBounceRepository;
23+
private EntityManagerInterface&MockObject $entityManager;
24+
private LoggerInterface&MockObject $logger;
2025
private BounceManager $manager;
2126

2227
protected function setUp(): void
2328
{
2429
$this->repository = $this->createMock(BounceRepository::class);
25-
$userMessageBounceRepository = $this->createMock(UserMessageBounceRepository::class);
30+
$this->userMessageBounceRepository = $this->createMock(UserMessageBounceRepository::class);
31+
$this->entityManager = $this->createMock(EntityManagerInterface::class);
32+
$this->logger = $this->createMock(LoggerInterface::class);
2633
$this->manager = new BounceManager(
2734
bounceRepository: $this->repository,
28-
userMessageBounceRepo: $userMessageBounceRepository,
29-
entityManager: $this->createMock(EntityManagerInterface::class),
30-
logger: $this->createMock(LoggerInterface::class),
35+
userMessageBounceRepo: $this->userMessageBounceRepository,
36+
entityManager: $this->entityManager,
37+
logger: $this->logger,
3138
);
3239
}
3340

@@ -102,4 +109,103 @@ public function testGetByIdReturnsNullWhenNotFound(): void
102109

103110
$this->assertNull($this->manager->getById(999));
104111
}
112+
113+
public function testUpdateChangesFieldsAndSaves(): void
114+
{
115+
$bounce = new Bounce();
116+
$this->repository->expects($this->once())
117+
->method('save')
118+
->with($bounce);
119+
120+
$updated = $this->manager->update($bounce, 'processed', 'done');
121+
$this->assertSame($bounce, $updated);
122+
$this->assertSame('processed', $bounce->getStatus());
123+
$this->assertSame('done', $bounce->getComment());
124+
}
125+
126+
public function testLinkUserMessageBounceFlushesAndSetsFields(): void
127+
{
128+
$bounce = new Bounce();
129+
$this->setId($bounce, 77);
130+
131+
$this->entityManager->expects($this->once())->method('flush');
132+
133+
$dt = new DateTimeImmutable('2024-05-01 12:34:56');
134+
$umb = $this->manager->linkUserMessageBounce($bounce, $dt, 123, 456);
135+
136+
$this->assertSame(77, $umb->getBounceId());
137+
$this->assertSame(123, $umb->getUserId());
138+
$this->assertSame(456, $umb->getMessageId());
139+
}
140+
141+
public function testExistsUserMessageBounceDelegatesToRepo(): void
142+
{
143+
$this->userMessageBounceRepository->expects($this->once())
144+
->method('existsByMessageIdAndUserId')
145+
->with(456, 123)
146+
->willReturn(true);
147+
148+
$this->assertTrue($this->manager->existsUserMessageBounce(123, 456));
149+
}
150+
151+
public function testFindByStatusDelegatesToRepository(): void
152+
{
153+
$b1 = new Bounce();
154+
$b2 = new Bounce();
155+
$this->repository->expects($this->once())
156+
->method('findByStatus')
157+
->with('new')
158+
->willReturn([$b1, $b2]);
159+
160+
$this->assertSame([$b1, $b2], $this->manager->findByStatus('new'));
161+
}
162+
163+
public function testGetUserMessageBounceCount(): void
164+
{
165+
$this->userMessageBounceRepository->expects($this->once())
166+
->method('count')
167+
->willReturn(5);
168+
$this->assertSame(5, $this->manager->getUserMessageBounceCount());
169+
}
170+
171+
public function testFetchUserMessageBounceBatchDelegates(): void
172+
{
173+
$expected = [['umb' => new UserMessageBounce(1, new \DateTime()), 'bounce' => new Bounce()]];
174+
$this->userMessageBounceRepository->expects($this->once())
175+
->method('getPaginatedWithJoinNoRelation')
176+
->with(10, 50)
177+
->willReturn($expected);
178+
$this->assertSame($expected, $this->manager->fetchUserMessageBounceBatch(10, 50));
179+
}
180+
181+
public function testGetUserMessageHistoryWithBouncesDelegates(): void
182+
{
183+
$subscriber = new Subscriber();
184+
$expected = [];
185+
$this->userMessageBounceRepository->expects($this->once())
186+
->method('getUserMessageHistoryWithBounces')
187+
->with($subscriber)
188+
->willReturn($expected);
189+
$this->assertSame($expected, $this->manager->getUserMessageHistoryWithBounces($subscriber));
190+
}
191+
192+
public function testAnnounceDeletionModeLogsCorrectMessage(): void
193+
{
194+
$this->logger->expects($this->exactly(2))
195+
->method('info')
196+
->withConsecutive([
197+
'Running in test mode, not deleting messages from mailbox'
198+
], [
199+
'Processed messages will be deleted from the mailbox'
200+
]);
201+
202+
$this->manager->announceDeletionMode(true);
203+
$this->manager->announceDeletionMode(false);
204+
}
205+
206+
private function setId(object $entity, int $id): void
207+
{
208+
$ref = new \ReflectionProperty($entity, 'id');
209+
$ref->setValue($entity, $id);
210+
}
105211
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace PhpList\Core\Tests\Unit\Domain\Messaging\Service\Manager;
6+
7+
use PhpList\Core\Domain\Messaging\Model\Bounce;
8+
use PhpList\Core\Domain\Messaging\Model\BounceRegex;
9+
use PhpList\Core\Domain\Messaging\Model\BounceRegexBounce;
10+
use PhpList\Core\Domain\Messaging\Repository\BounceRegexBounceRepository;
11+
use PhpList\Core\Domain\Messaging\Repository\BounceRegexRepository;
12+
use PhpList\Core\Domain\Messaging\Service\Manager\BounceRuleManager;
13+
use PHPUnit\Framework\MockObject\MockObject;
14+
use PHPUnit\Framework\TestCase;
15+
16+
class BounceRuleManagerTest extends TestCase
17+
{
18+
private BounceRegexRepository&MockObject $regexRepository;
19+
private BounceRegexBounceRepository&MockObject $relationRepository;
20+
private BounceRuleManager $manager;
21+
22+
protected function setUp(): void
23+
{
24+
$this->regexRepository = $this->createMock(BounceRegexRepository::class);
25+
$this->relationRepository = $this->createMock(BounceRegexBounceRepository::class);
26+
$this->manager = new BounceRuleManager($this->regexRepository, $this->relationRepository);
27+
}
28+
29+
public function testLoadActiveRulesMapsRowsAndSkipsInvalid(): void
30+
{
31+
$valid = new BounceRegex(regex: 'user unknown', regexHash: md5('user unknown'), action: 'delete');
32+
// invalids: no regex, no action, no id
33+
$noRegex = new BounceRegex(regex: '', regexHash: md5(''), action: 'delete');
34+
$noAction = new BounceRegex(regex: 'pattern', regexHash: md5('pattern'), action: '');
35+
$noId = new BounceRegex(regex: 'has no id', regexHash: md5('has no id'), action: 'keep');
36+
37+
// Simulate id assignment for only some of them
38+
$this->setId($valid, 1);
39+
$this->setId($noRegex, 2);
40+
$this->setId($noAction, 3);
41+
// $noId intentionally left without id
42+
43+
$this->regexRepository->expects($this->once())
44+
->method('fetchActiveOrdered')
45+
->willReturn([$valid, $noRegex, $noAction, $noId]);
46+
47+
$result = $this->manager->loadActiveRules();
48+
49+
$this->assertSame(['user unknown' => $valid], $result);
50+
}
51+
52+
public function testLoadAllRulesDelegatesToRepository(): void
53+
{
54+
$r1 = new BounceRegex(regex: 'a', regexHash: md5('a'), action: 'keep');
55+
$r2 = new BounceRegex(regex: 'b', regexHash: md5('b'), action: 'delete');
56+
$this->setId($r1, 10);
57+
$this->setId($r2, 11);
58+
59+
$this->regexRepository->expects($this->once())
60+
->method('fetchAllOrdered')
61+
->willReturn([$r1, $r2]);
62+
63+
$result = $this->manager->loadAllRules();
64+
$this->assertSame(['a' => $r1, 'b' => $r2], $result);
65+
}
66+
67+
public function testMatchBounceRulesMatchesQuotedAndRawAndHandlesInvalidPatterns(): void
68+
{
69+
$valid = new BounceRegex(regex: 'user unknown', regexHash: md5('user unknown'), action: 'delete');
70+
$this->setId($valid, 1);
71+
// invalid regex pattern that would break preg_match if not handled (unbalanced bracket)
72+
$invalid = new BounceRegex(regex: '([a-z', regexHash: md5('([a-z'), action: 'keep');
73+
$this->setId($invalid, 2);
74+
75+
$rules = ['user unknown' => $valid, '([a-z' => $invalid];
76+
77+
$matched = $this->manager->matchBounceRules('Delivery failed: user unknown at example', $rules);
78+
$this->assertSame($valid, $matched);
79+
80+
// Ensure invalid pattern does not throw and simply not match
81+
$matchedInvalid = $this->manager->matchBounceRules('something else', ['([a-z' => $invalid]);
82+
$this->assertNull($matchedInvalid);
83+
}
84+
85+
public function testIncrementCountPersists(): void
86+
{
87+
$rule = new BounceRegex(regex: 'x', regexHash: md5('x'), action: 'keep', count: 0);
88+
$this->setId($rule, 5);
89+
90+
$this->regexRepository->expects($this->once())
91+
->method('save')
92+
->with($rule);
93+
94+
$this->manager->incrementCount($rule);
95+
$this->assertSame(1, $rule->getCount());
96+
}
97+
98+
public function testLinkRuleToBounceCreatesRelationAndSaves(): void
99+
{
100+
$rule = new BounceRegex(regex: 'y', regexHash: md5('y'), action: 'delete');
101+
$bounce = new Bounce();
102+
$this->setId($rule, 9);
103+
$this->setId($bounce, 20);
104+
105+
$this->relationRepository->expects($this->once())
106+
->method('save')
107+
->with($this->isInstanceOf(BounceRegexBounce::class));
108+
109+
$relation = $this->manager->linkRuleToBounce($rule, $bounce);
110+
111+
$this->assertInstanceOf(BounceRegexBounce::class, $relation);
112+
$this->assertSame(9, $relation->getRegexId());
113+
}
114+
115+
private function setId(object $entity, int $id): void
116+
{
117+
$ref = new \ReflectionProperty($entity, 'id');
118+
$ref->setValue($entity, $id);
119+
}
120+
}

0 commit comments

Comments
 (0)