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', '

Hi

', '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', '

Hi

', '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', '

Hi

', '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', '

Body

', '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', '

Body

', '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', '

Invoice

', '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', '

Ad hoc

', 'Ad hoc'); } }