132 lines
6.0 KiB
PHP
132 lines
6.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Repository;
|
|
|
|
use App\Entity\Application;
|
|
use App\Entity\Assignment;
|
|
use App\Entity\Teamer;
|
|
use App\Repository\ApplicationRepository;
|
|
use Carbon\CarbonPeriodImmutable;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Doctrine\ORM\Query;
|
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
|
|
|
/**
|
|
* The overlap query behind the automatic purge of applications is expressed in DQL, so its semantics
|
|
* cannot be covered by mocking the query builder - doing so is what let a wrong comparison ship
|
|
* unnoticed. These tests compile the query instead and pin the parts that carry the semantics.
|
|
*/
|
|
class ApplicationRepositoryTest extends KernelTestCase
|
|
{
|
|
public function testOverlappingQueryUsesInclusiveBoundariesOnTheDestinationDates(): void
|
|
{
|
|
$dql = $this->createOverlappingQuery()->getDQL();
|
|
|
|
// the assignment period is the date range of its destination
|
|
$this->assertStringContainsString('destination.dateFrom <= :dateTo', $dql);
|
|
$this->assertStringContainsString('destination.dateTo >= :dateFrom', $dql);
|
|
|
|
// strict comparisons would let assignments that touch on a boundary pass and could never
|
|
// match a single day assignment at all
|
|
$this->assertDoesNotMatchRegularExpression('/date(From|To) [<>] :date(From|To)/', $dql);
|
|
}
|
|
|
|
public function testOverlappingQueryExcludesRejectedApplicationsAndDeadAssignments(): void
|
|
{
|
|
$dql = $this->createOverlappingQuery()->getDQL();
|
|
|
|
$this->assertStringContainsString('application.teamer = :teamer', $dql);
|
|
$this->assertStringContainsString('application.status <> :application_rejected', $dql);
|
|
$this->assertStringContainsString('assignment.status <> :assignment_called_off', $dql);
|
|
$this->assertStringContainsString('assignment.deletedAt IS NULL', $dql);
|
|
$this->assertStringContainsString('destination.deletedAt IS NULL', $dql);
|
|
}
|
|
|
|
public function testOverlappingQueryBindsThePeriodBoundaries(): void
|
|
{
|
|
$teamer = new Teamer();
|
|
$period = new CarbonPeriodImmutable('2025-01-10', '2025-01-20');
|
|
|
|
$query = $this->createOverlappingQuery($teamer, $period);
|
|
|
|
$this->assertSame($teamer, $query->getParameter('teamer')->getValue());
|
|
$this->assertSame(Application::STATUS_REJECTED, $query->getParameter('application_rejected')->getValue());
|
|
$this->assertSame(Assignment::STATUS_CALLED_OFF, $query->getParameter('assignment_called_off')->getValue());
|
|
$this->assertSame('2025-01-10', $query->getParameter('dateFrom')->getValue()->format('Y-m-d'));
|
|
$this->assertSame('2025-01-20', $query->getParameter('dateTo')->getValue()->format('Y-m-d'));
|
|
}
|
|
|
|
private function createOverlappingQuery(?Teamer $teamer = null, ?CarbonPeriodImmutable $period = null): Query
|
|
{
|
|
/** @var EntityManagerInterface $entityManager */
|
|
$entityManager = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
/** @var ApplicationRepository $repository */
|
|
$repository = $entityManager->getRepository(Application::class);
|
|
|
|
return $repository->getOverlappingForTeamerQuery(
|
|
$teamer ?? new Teamer(),
|
|
$period ?? new CarbonPeriodImmutable('2025-01-10', '2025-01-20')
|
|
);
|
|
}
|
|
|
|
public function testApplicationsByDestinationGroupsByTheNormalizedHotelCode(): void
|
|
{
|
|
$dql = $this->createApplicationsByDestinationQuery()->getDQL();
|
|
|
|
// the SER prefix is stripped so a house counts once, however its code is spelled
|
|
$this->assertStringContainsString('SUBSTRING(destination.hotelCode, 4, 3)', $dql);
|
|
$this->assertStringContainsString('SUBSTRING(destination.hotelCode, 1, 3)', $dql);
|
|
$this->assertStringContainsString('GROUP BY hotelCode', $dql);
|
|
|
|
// a destination lives on the assignment, not the application
|
|
$this->assertStringContainsString('INNER JOIN application.assignment assignment', $dql);
|
|
$this->assertStringContainsString('INNER JOIN assignment.destination destination', $dql);
|
|
}
|
|
|
|
public function testApplicationsByDestinationCountsEveryApplicationRegardlessOfStatus(): void
|
|
{
|
|
$query = $this->createApplicationsByDestinationQuery();
|
|
$dql = $query->getDQL();
|
|
|
|
// status moves over an application's lifetime, so it is not broken out - a plain count
|
|
$this->assertStringContainsString('COUNT(application.id)', $dql);
|
|
$this->assertStringNotContainsStringIgnoringCase('application.status', $dql);
|
|
$this->assertNull($query->getParameter('statusNew'));
|
|
}
|
|
|
|
public function testApplicationsByDestinationAppliesTheSeasonBoundsOnlyWhenGiven(): void
|
|
{
|
|
$unfiltered = $this->createApplicationsByDestinationQuery();
|
|
$this->assertNull($unfiltered->getParameter('dateFrom'));
|
|
$this->assertNull($unfiltered->getParameter('dateTo'));
|
|
|
|
$filtered = $this->createApplicationsByDestinationQuery(
|
|
new \DateTimeImmutable('2025-05-01'),
|
|
new \DateTimeImmutable('2025-09-30'),
|
|
);
|
|
$dql = $filtered->getDQL();
|
|
|
|
// the season is the destination's date range, same as every other statistics screen
|
|
$this->assertStringContainsString('destination.dateFrom >= :dateFrom', $dql);
|
|
$this->assertStringContainsString('destination.dateTo <= :dateTo', $dql);
|
|
$this->assertSame('2025-05-01', $filtered->getParameter('dateFrom')->getValue()->format('Y-m-d'));
|
|
$this->assertSame('2025-09-30', $filtered->getParameter('dateTo')->getValue()->format('Y-m-d'));
|
|
}
|
|
|
|
private function createApplicationsByDestinationQuery(
|
|
?\DateTimeImmutable $dateFrom = null,
|
|
?\DateTimeImmutable $dateTo = null,
|
|
): Query {
|
|
/** @var EntityManagerInterface $entityManager */
|
|
$entityManager = static::getContainer()->get(EntityManagerInterface::class);
|
|
|
|
/** @var ApplicationRepository $repository */
|
|
$repository = $entityManager->getRepository(Application::class);
|
|
|
|
return $repository->getApplicationsByDestinationQuery($dateFrom, $dateTo);
|
|
}
|
|
}
|