230 lines
7.8 KiB
PHP
230 lines
7.8 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\Entity\User;
|
|
use App\Repository\SeasonAssignmentTaskRepository;
|
|
use App\Repository\UserRepository;
|
|
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 UserRepository&MockObject $userRepository;
|
|
private Mailer&MockObject $mailer;
|
|
private SeasonTaskReminderService $service;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->repository = $this->createMock(SeasonAssignmentTaskRepository::class);
|
|
$this->userRepository = $this->createMock(UserRepository::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->userRepository,
|
|
$this->mailer,
|
|
new NullLogger(),
|
|
);
|
|
}
|
|
|
|
public function testTheSummaryFiresOnTheDeadlineDay(): void
|
|
{
|
|
$this->repository->method('findOpen')->willReturn([$this->task('2026-11-01')]);
|
|
$this->userRepository->method('getUsersByRoles')->willReturn([$this->admin()]);
|
|
|
|
$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'));
|
|
}
|
|
|
|
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')]);
|
|
$this->userRepository->method('getUsersByRoles')->willReturn([$this->admin()]);
|
|
|
|
$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]);
|
|
$this->userRepository->method('getUsersByRoles')->willReturn([$this->admin()]);
|
|
|
|
$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 testAnAdminMutingNotificationsIsSkipped(): void
|
|
{
|
|
$this->repository->method('findOpen')->willReturn([$this->task('2026-11-01')]);
|
|
$this->userRepository->method('getUsersByRoles')->willReturn([
|
|
$this->admin()->setMuteNotifications(true),
|
|
]);
|
|
|
|
$this->mailer->expects($this->never())->method('createAndSendEmail');
|
|
|
|
$this->service->sendDeadlineSummary(new \DateTimeImmutable('2026-10-01'));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
private function admin(): User
|
|
{
|
|
return (new User())->setEmail('[email protected]');
|
|
}
|
|
}
|