Files
myep-team/tests/Service/Cron/SeasonTaskReminderServiceTest.php
T

227 lines
7.5 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Service\Cron;
use App\Config\EmailTextKey;
use App\Config\HouseCatalog;
use App\Config\SeasonCalendar;
use App\Config\SeasonTask;
use App\Email\Mailer;
use App\Entity\Embeddable\Communication;
use App\Entity\SeasonAssignment;
use App\Entity\SeasonAssignmentTask;
use App\Entity\Teamer;
use App\Repository\SeasonAssignmentTaskRepository;
use App\Service\Cron\SeasonTaskReminderService;
use App\Service\Season\SeasonAssignmentLabeler;
use App\Service\Season\SeasonTaskDeadlineResolver;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use Psr\Log\NullLogger;
/**
* Both rules pick out exactly one day, which is what lets the cron run twice without mailing
* anyone twice. That property, and what does and does not silence a reminder, is what is pinned
* here - the queries behind them are asserted in the repository test.
*/
class SeasonTaskReminderServiceTest extends TestCase
{
private SeasonAssignmentTaskRepository&MockObject $repository;
private Mailer&MockObject $mailer;
private SeasonTaskReminderService $service;
protected function setUp(): void
{
$this->repository = $this->createMock(SeasonAssignmentTaskRepository::class);
$this->mailer = $this->createMock(Mailer::class);
$this->service = new SeasonTaskReminderService(
$this->repository,
new SeasonTaskDeadlineResolver('-1 month'),
new SeasonAssignmentLabeler(
new SeasonCalendar('11-01', '04-01'),
new HouseCatalog(['AGR' => 'Rotbach'])
),
new SeasonCalendar('11-01', '04-01'),
$this->mailer,
new NullLogger(),
);
}
public function testTheSummaryFiresOnTheDeadlineDay(): void
{
$this->repository->method('findOpen')->willReturn([$this->task('2026-11-01')]);
$this->mailer
->expects($this->once())
->method('createAndSendEmail')
->with(
$this->callback(static function (array $context): bool {
$entries = reset($context['violations']);
return 1 === count($entries) && 1 === $entries[0]['level'];
}),
$this->anything()
)
;
$this->service->sendDeadlineSummary(new \DateTimeImmutable('2026-10-01'));
}
/**
* The summary is one office mail, not a copy per admin: it passes no recipient at all and so
* lands on the Mailer default, APP_DEFAULT_EMAIL_TO.
*/
public function testTheSummaryGoesOutOnceToTheDefaultAddress(): void
{
$this->repository->method('findOpen')->willReturn([$this->task('2026-11-01')]);
$this->mailer
->expects($this->once())
->method('createAndSendEmail')
->with(
$this->anything(),
$this->callback(
static fn (array $options): bool => false === array_key_exists('to', $options)
)
)
;
$this->service->sendDeadlineSummary(new \DateTimeImmutable('2026-10-01'));
}
public function testTheSummaryDoesNotFireTheDayAfter(): void
{
$this->repository->method('findOpen')->willReturn([$this->task('2026-11-01')]);
$this->mailer->expects($this->never())->method('createAndSendEmail');
$this->service->sendDeadlineSummary(new \DateTimeImmutable('2026-10-02'));
}
/**
* Frist 2 defaults to Frist 1, so the two coincide unless the office moves one. The entry is
* then reported once and as Frist 1 - a second, identical line would be pure noise.
*/
public function testCoincidingDeadlinesAreReportedOnceAsTheFirst(): void
{
$this->repository->method('findOpen')->willReturn([$this->task('2026-11-01')]);
$levels = [];
$this->mailer
->method('createAndSendEmail')
->willReturnCallback(static function (array $context) use (&$levels): void {
foreach (reset($context['violations']) as $entry) {
$levels[] = $entry['level'];
}
})
;
$this->service->sendDeadlineSummary(new \DateTimeImmutable('2026-10-01'));
$this->assertSame([1], $levels);
}
public function testAPushedOutSecondDeadlineGetsItsOwnDay(): void
{
$task = $this->task('2026-11-01')->setDeadlineOverride2(new \DateTimeImmutable('2026-10-15'));
$this->repository->method('findOpen')->willReturn([$task]);
$levels = [];
$this->mailer
->method('createAndSendEmail')
->willReturnCallback(static function (array $context) use (&$levels): void {
foreach (reset($context['violations']) as $entry) {
$levels[] = $entry['level'];
}
})
;
$this->service->sendDeadlineSummary(new \DateTimeImmutable('2026-10-15'));
$this->assertSame([2], $levels);
}
public function testTheDigestOnlyGoesOutOnMondays(): void
{
$this->repository->expects($this->never())->method('findOpen');
$this->mailer->expects($this->never())->method('createAndSendText');
// 2026-10-06 is a tuesday
$this->service->sendTeamerDigest(new \DateTimeImmutable('2026-10-06'));
}
public function testTheDigestListsTheOpenReturnEntries(): void
{
$this->repository->method('findOpen')->willReturn([$this->task('2026-11-01')]);
$this->mailer
->expects($this->once())
->method('createAndSendText')
->with(
EmailTextKey::REMINDER_SEASON_TASKS,
$this->callback(static fn (array $placeholders): bool => SeasonTask::CONTRACT->label() === $placeholders['tasks']),
$this->anything()
)
;
$this->service->sendTeamerDigest($this->monday());
}
public function testAnOfficeOnlyEntryNeverReachesTheTeamer(): void
{
$this->repository
->method('findOpen')
->willReturn([$this->task('2026-11-01', SeasonTask::SAAS_REGISTERED)])
;
$this->mailer->expects($this->never())->method('createAndSendText');
$this->service->sendTeamerDigest($this->monday());
}
/**
* The flag is the office's own note and silences nothing: only ticking the entry off does.
*/
public function testDoneTeamerDoesNotSuppressTheDigest(): void
{
$task = $this->task('2026-11-01');
$task->getSeasonAssignment()->setDoneTeamer(true);
$this->repository->method('findOpen')->willReturn([$task]);
$this->mailer->expects($this->once())->method('createAndSendText');
$this->service->sendTeamerDigest($this->monday());
}
private function monday(): \DateTimeImmutable
{
// 2026-10-05 is a monday, and inside the season the digest is scoped to
return new \DateTimeImmutable('2026-10-05');
}
private function task(string $dateFrom, SeasonTask $definition = SeasonTask::CONTRACT): SeasonAssignmentTask
{
$teamer = new Teamer();
$teamer->setCommunication((new Communication())->setEmail('[email protected]'));
$seasonAssignment = (new SeasonAssignment())
->setTeamer($teamer)
->setDateFrom(new \DateTimeImmutable($dateFrom))
->setDateTo(new \DateTimeImmutable($dateFrom.' +5 months'))
->setHotelCode('AGR')
;
$task = (new SeasonAssignmentTask())->setTaskKey($definition->value);
$seasonAssignment->addTask($task);
return $task;
}
}