Files
breadcrumb-the-shire/tests/Service/Mail/MailServiceTest.php
fs d4978d1504 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>
2026-03-13 13:57:54 +01:00

166 lines
6.3 KiB
PHP

<?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');
}
}