test(services): add 88 unit tests for 7 critical service classes
RateLimiterServiceTest (17): hit/isBlocked/reset/registerFailure, fail-open, sliding window, scope normalization. PermissionServiceTest (24): userHas, getUserPermissions, cache hit/miss/refresh, clearUserCache, CRUD, system permission protection. RoleServiceTest (9): createFromAdmin, updateFromAdmin, deleteByUuid, Admin role protection, duplicate code rejection. TenantServiceTest (8): CRUD, department-dependent deletion blocking. DepartmentServiceTest (14): listPaged scope filtering, groupActiveByTenantIds, createFromAdmin, deleteByUuid, syncTenants. MailServiceTest (8): send logging, MailerException handling, template metadata. UserLifecycleServiceTest (8): advisory locking, deactivation, deletion, snapshot failure skip, privileged user exclusion, manual trigger type. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
165
tests/Service/Mail/MailServiceTest.php
Normal file
165
tests/Service/Mail/MailServiceTest.php
Normal file
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
namespace MintyPHP\Tests\Service\Mail;
|
||||
|
||||
use MintyPHP\Repository\Mail\MailLogRepositoryInterface;
|
||||
use MintyPHP\Service\Mail\MailService;
|
||||
use MintyPHP\Service\Settings\SettingsSmtpGateway;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
class MailServiceTest extends TestCase
|
||||
{
|
||||
private function createSmtpGatewayMock(): SettingsSmtpGateway
|
||||
{
|
||||
$gateway = $this->createMock(SettingsSmtpGateway::class);
|
||||
$gateway->method('getSmtpHost')->willReturn('localhost');
|
||||
$gateway->method('getSmtpPort')->willReturn(587);
|
||||
$gateway->method('getSmtpUser')->willReturn('');
|
||||
$gateway->method('getSmtpPassword')->willReturn('');
|
||||
$gateway->method('getSmtpSecure')->willReturn('tls');
|
||||
$gateway->method('getSmtpFrom')->willReturn('test@example.com');
|
||||
$gateway->method('getSmtpFromName')->willReturn('Test');
|
||||
|
||||
return $gateway;
|
||||
}
|
||||
|
||||
private function newService(
|
||||
?MailLogRepositoryInterface $mailLogRepository = null,
|
||||
?SettingsSmtpGateway $settingsSmtpGateway = null,
|
||||
): MailService {
|
||||
return new MailService(
|
||||
$mailLogRepository ?? $this->createMock(MailLogRepositoryInterface::class),
|
||||
$settingsSmtpGateway ?? $this->createSmtpGatewayMock(),
|
||||
);
|
||||
}
|
||||
|
||||
public function testSendLogsBeforeSending(): void
|
||||
{
|
||||
$mailLogRepository = $this->createMock(MailLogRepositoryInterface::class);
|
||||
$mailLogRepository->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(function (array $data): bool {
|
||||
return $data['to_email'] === 'user@example.com'
|
||||
&& $data['subject'] === 'Hello'
|
||||
&& $data['status'] === 'queued';
|
||||
}))
|
||||
->willReturn(1);
|
||||
|
||||
$service = $this->newService(mailLogRepository: $mailLogRepository);
|
||||
|
||||
$service->send('user@example.com', 'Hello', '<p>Hi</p>', 'Hi');
|
||||
}
|
||||
|
||||
public function testSendHandlesMailerException(): void
|
||||
{
|
||||
$mailLogRepository = $this->createMock(MailLogRepositoryInterface::class);
|
||||
$mailLogRepository->method('create')->willReturn(1);
|
||||
$mailLogRepository->expects($this->once())->method('markFailed');
|
||||
$mailLogRepository->expects($this->never())->method('markSent');
|
||||
|
||||
$service = $this->newService(mailLogRepository: $mailLogRepository);
|
||||
|
||||
$result = $service->send('user@example.com', 'Hello', '<p>Hi</p>', 'Hi');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('send_failed', $result['error']);
|
||||
}
|
||||
|
||||
public function testSendHandlesNullLogId(): void
|
||||
{
|
||||
$mailLogRepository = $this->createMock(MailLogRepositoryInterface::class);
|
||||
$mailLogRepository->method('create')->willReturn(null);
|
||||
$mailLogRepository->expects($this->never())->method('markFailed');
|
||||
$mailLogRepository->expects($this->never())->method('markSent');
|
||||
|
||||
$service = $this->newService(mailLogRepository: $mailLogRepository);
|
||||
|
||||
$result = $service->send('user@example.com', 'Hello', '<p>Hi</p>', 'Hi');
|
||||
|
||||
$this->assertFalse($result['ok']);
|
||||
$this->assertSame('send_failed', $result['error']);
|
||||
}
|
||||
|
||||
public function testSendWithEmptyLogIdStillReturnsError(): void
|
||||
{
|
||||
// create() returns null (e.g. DB insert failed) — error path still returns a result.
|
||||
$mailLogRepository = $this->createMock(MailLogRepositoryInterface::class);
|
||||
$mailLogRepository->method('create')->willReturn(null);
|
||||
|
||||
$service = $this->newService(mailLogRepository: $mailLogRepository);
|
||||
|
||||
$result = $service->send('user@example.com', 'Subject', '<p>Body</p>', 'Body');
|
||||
|
||||
$this->assertArrayHasKey('ok', $result);
|
||||
$this->assertArrayHasKey('error', $result);
|
||||
$this->assertFalse($result['ok']);
|
||||
}
|
||||
|
||||
public function testSendTemplateCallsSend(): void
|
||||
{
|
||||
// Templates won't exist in test context, so html/text will be empty strings.
|
||||
// But the send path is still exercised, creating a log entry.
|
||||
$mailLogRepository = $this->createMock(MailLogRepositoryInterface::class);
|
||||
$mailLogRepository->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(function (array $data): bool {
|
||||
return $data['to_email'] === 'user@example.com'
|
||||
&& $data['template'] === 'welcome';
|
||||
}))
|
||||
->willReturn(2);
|
||||
|
||||
$service = $this->newService(mailLogRepository: $mailLogRepository);
|
||||
|
||||
$result = $service->sendTemplate('welcome', ['name' => 'Alice'], 'user@example.com', 'Welcome');
|
||||
|
||||
$this->assertArrayHasKey('ok', $result);
|
||||
}
|
||||
|
||||
public function testSendCallsMarkFailedWithErrorMessage(): void
|
||||
{
|
||||
$mailLogRepository = $this->createMock(MailLogRepositoryInterface::class);
|
||||
$mailLogRepository->method('create')->willReturn(5);
|
||||
$mailLogRepository->expects($this->once())
|
||||
->method('markFailed')
|
||||
->with(
|
||||
$this->identicalTo(5),
|
||||
$this->isType('string'),
|
||||
);
|
||||
|
||||
$service = $this->newService(mailLogRepository: $mailLogRepository);
|
||||
|
||||
$service->send('user@example.com', 'Test', '<p>Body</p>', 'Body');
|
||||
}
|
||||
|
||||
public function testSendPassesMetaTemplateToLogEntry(): void
|
||||
{
|
||||
$mailLogRepository = $this->createMock(MailLogRepositoryInterface::class);
|
||||
$mailLogRepository->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(function (array $data): bool {
|
||||
return $data['template'] === 'invoice';
|
||||
}))
|
||||
->willReturn(3);
|
||||
|
||||
$service = $this->newService(mailLogRepository: $mailLogRepository);
|
||||
|
||||
$service->send('user@example.com', 'Invoice', '<p>Invoice</p>', 'Invoice', [
|
||||
'template' => 'invoice',
|
||||
]);
|
||||
}
|
||||
|
||||
public function testSendWithoutMetaTemplateLogsNullTemplate(): void
|
||||
{
|
||||
$mailLogRepository = $this->createMock(MailLogRepositoryInterface::class);
|
||||
$mailLogRepository->expects($this->once())
|
||||
->method('create')
|
||||
->with($this->callback(function (array $data): bool {
|
||||
return $data['template'] === null;
|
||||
}))
|
||||
->willReturn(4);
|
||||
|
||||
$service = $this->newService(mailLogRepository: $mailLogRepository);
|
||||
|
||||
$service->send('user@example.com', 'Ad hoc', '<p>Ad hoc</p>', 'Ad hoc');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user