teamerRepository = $this->createMock(TeamerRepository::class); $this->mailer = $this->createMock(Mailer::class); $this->service = new TeamerMailingService( $this->teamerRepository, $this->mailer, $this->createMock(LoggerInterface::class), ); } public function testRenderReplacesAllNamePlaceholders(): void { $recipient = $this->createRecipient('Anna', 'Berg', 'anna@example.org'); $rendered = $this->service->render( 'Hallo {{vorname}} {{nachname}}, alias {{name}}!', $this->service->placeholderValuesFor($recipient) ); $this->assertSame('Hallo Anna Berg, alias Anna Berg!', $rendered); } public function testRenderLeavesUnknownPlaceholdersUntouched(): void { $recipient = $this->createRecipient('Anna', 'Berg', 'anna@example.org'); $rendered = $this->service->render( 'Hallo {{vorname}}, {{unbekannt}} bleibt stehen', $this->service->placeholderValuesFor($recipient) ); $this->assertSame('Hallo Anna, {{unbekannt}} bleibt stehen', $rendered); } public function testRecipientIsBuiltFromARepositoryRow(): void { $deletedAt = new \DateTimeImmutable('2026-01-02 03:04:05'); $recipient = TeamerMailingRecipient::fromRow([ 'id' => 42, 'firstName' => 'Anna', 'lastName' => 'Berg', 'email' => 'anna@example.org', 'deletedAt' => $deletedAt, 'disabledAt' => null, ]); $this->assertSame(42, $recipient->getId()); $this->assertSame('Anna Berg', $recipient->getFullName()); $this->assertSame('anna@example.org', $recipient->getEmail()); $this->assertTrue($recipient->isDeleted()); $this->assertFalse($recipient->isDisabled()); } public function testResolveRecipientsPartitionsBySkipReason(): void { $eligible = $this->createRecipient('Anna', 'Berg', 'anna@example.org'); $deleted = $this->createRecipient('Bea', 'Ohm', 'bea@example.org', deleted: true); $disabled = $this->createRecipient('Cem', 'Dal', 'cem@example.org', disabled: true); $noEmail = $this->createRecipient('Dana', 'Elf', null); $blankEmail = $this->createRecipient('Emil', 'Fux', ' '); $this->teamerRepository ->method('getMailingRecipients') ->willReturn([$eligible, $deleted, $disabled, $noEmail, $blankEmail]) ; $recipients = $this->service->resolveRecipients(new TeamerFilterDto()); $this->assertSame([$eligible], $recipients->getEligible()); $this->assertSame(1, $recipients->getSkippedDeleted()); $this->assertSame(1, $recipients->getSkippedDisabled()); $this->assertSame(2, $recipients->getSkippedNoEmail()); $this->assertSame(5, $recipients->getTotal()); } public function testSendPersonalisesSubjectAndMessagePerRecipient(): void { $this->teamerRepository ->method('getMailingRecipients') ->willReturn([ $this->createRecipient('Anna', 'Berg', 'anna@example.org'), $this->createRecipient('Bea', 'Ohm', 'bea@example.org'), ]) ; $sent = []; $this->mailer ->expects($this->exactly(2)) ->method('createAndSendEmail') ->willReturnCallback(function (array $context, array $options) use (&$sent): void { $sent[] = [$options['to'], $options['subject'], $context['message']]; }) ; $mailingDto = (new TeamerMailingDto()) ->setSubject('Hallo {{vorname}}') ->setMessage('Servus {{name}}') ; $recipients = $this->service->resolveRecipients(new TeamerFilterDto()); $count = $this->service->send($mailingDto, $recipients); $this->assertSame(2, $count); $this->assertSame([ ['anna@example.org', 'Hallo Anna', 'Servus Anna Berg'], ['bea@example.org', 'Hallo Bea', 'Servus Bea Ohm'], ], $sent); } public function testSendPreviewUsesAdminOwnNameAndMarksTheSubject(): void { $admin = (new User()) ->setEmail('admin@example.org') ->setFirstName('Rita') ->setLastName('Kern') ; $this->mailer ->expects($this->once()) ->method('createAndSendEmail') ->with( ['message' => 'Servus Rita Kern'], $this->callback(function (array $options): bool { $this->assertSame('admin@example.org', $options['to']); $this->assertSame('[Vorschau] Hallo Rita', $options['subject']); return true; }) ) ; $mailingDto = (new TeamerMailingDto()) ->setSubject('Hallo {{vorname}}') ->setMessage('Servus {{name}}') ; $this->service->sendPreview($mailingDto, $admin); } public function testSendPreviewPrefersTheAdminsOwnTeamerRecord(): void { $teamer = (new Teamer())->setFirstName('Rita')->setLastName('Kern-Teamer'); $admin = (new User()) ->setEmail('admin@example.org') ->setFirstName('Rita') ->setLastName('Kern-User') ->setTeamer($teamer) ; $this->assertSame( 'Kern-Teamer', $this->service->placeholderValuesForUser($admin)[TeamerMailingService::PLACEHOLDER_LAST_NAME] ); } public function testPreviewFallsBackToTheAddressWhenTheAdminHasNoName(): void { $admin = (new User())->setEmail('rita.kern@example.org'); $values = $this->service->placeholderValuesForUser($admin); $this->assertSame('rita.kern', $values[TeamerMailingService::PLACEHOLDER_FIRST_NAME]); $this->assertSame('rita.kern', $values[TeamerMailingService::PLACEHOLDER_FULL_NAME]); } private function createRecipient( string $firstName, string $lastName, ?string $email, bool $deleted = false, bool $disabled = false, ): TeamerMailingRecipient { $now = new \DateTimeImmutable(); return new TeamerMailingRecipient( self::$nextId++, $firstName, $lastName, $email, $deleted ? $now : null, $disabled ? $now : null ); } }