feat: all-season assignments
This commit is contained in:
1 parent
675cec1b69
commit
cf876ed224
119 files changed
+6194
-294
No files matched your search
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Repository;
|
||||
|
||||
use App\Entity\SeasonAssignment;
|
||||
use App\Entity\Teamer;
|
||||
use App\Repository\SeasonAssignmentRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* The rule that a teamer gets one season assignment per season is only as good as the window the
|
||||
* query asks for, so the conditions are compiled and pinned here - the suite has no fixtures.
|
||||
*/
|
||||
class SeasonAssignmentRepositoryTest extends KernelTestCase
|
||||
{
|
||||
public function testTheSeasonWindowQueryBucketsByTheAssignmentsFirstDay(): void
|
||||
{
|
||||
$dql = $this->createInSeasonWindowQueryBuilder()->getQuery()->getDQL();
|
||||
|
||||
// both bounds on dateFrom: a placement running past the last day of the season still
|
||||
// belongs to that season, so dateTo must not be part of the test
|
||||
$this->assertStringContainsString('season_assignment.dateFrom >= :windowStart', $dql);
|
||||
$this->assertStringContainsString('season_assignment.dateFrom <= :windowEnd', $dql);
|
||||
$this->assertStringNotContainsString('dateTo', $dql);
|
||||
}
|
||||
|
||||
/**
|
||||
* The period is the assignment's own, so there is no second row left to join - and none that
|
||||
* could be soft-deleted underneath a live placement.
|
||||
*/
|
||||
public function testTheSeasonWindowQueryNeedsNoJoin(): void
|
||||
{
|
||||
$dql = $this->createInSeasonWindowQueryBuilder()->getQuery()->getDQL();
|
||||
|
||||
$this->assertStringContainsString('season_assignment.deletedAt IS NULL', $dql);
|
||||
$this->assertStringNotContainsString('destination', $dql);
|
||||
}
|
||||
|
||||
public function testTheSeasonWindowQueryOnlyExcludesARowWhenEditingOne(): void
|
||||
{
|
||||
$this->assertStringNotContainsString(
|
||||
'season_assignment.id <> :excludeId',
|
||||
$this->createInSeasonWindowQueryBuilder()->getQuery()->getDQL()
|
||||
);
|
||||
|
||||
$this->assertStringContainsString(
|
||||
'season_assignment.id <> :excludeId',
|
||||
$this->createInSeasonWindowQueryBuilder(42)->getQuery()->getDQL()
|
||||
);
|
||||
}
|
||||
|
||||
private function createInSeasonWindowQueryBuilder(?int $excludeId = null): \Doctrine\ORM\QueryBuilder
|
||||
{
|
||||
/** @var EntityManagerInterface $entityManager */
|
||||
$entityManager = static::getContainer()->get(EntityManagerInterface::class);
|
||||
|
||||
/** @var SeasonAssignmentRepository $repository */
|
||||
$repository = $entityManager->getRepository(SeasonAssignment::class);
|
||||
|
||||
return $repository->createInSeasonWindowQueryBuilder(
|
||||
new Teamer(),
|
||||
new \DateTimeImmutable('2026-11-01'),
|
||||
new \DateTimeImmutable('2027-04-01'),
|
||||
$excludeId
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Repository;
|
||||
|
||||
use App\Entity\SeasonAssignmentTask;
|
||||
use App\Repository\SeasonAssignmentTaskRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* The reminders are only as good as the set of rows they start from, so the conditions are
|
||||
* compiled and pinned here - the suite has no fixtures.
|
||||
*
|
||||
* Note what is deliberately absent: the deadlines. They are a relative offset off the
|
||||
* assignment's first day, and month arithmetic is not invertible day by day, so they are
|
||||
* resolved in PHP rather than reproduced in DQL.
|
||||
*/
|
||||
class SeasonAssignmentTaskRepositoryTest extends KernelTestCase
|
||||
{
|
||||
public function testOpenTasksExcludeDoneOnesAndDeletedRecords(): void
|
||||
{
|
||||
$dql = $this->createOpenQueryBuilder()->getQuery()->getDQL();
|
||||
|
||||
$this->assertStringContainsString('task.done = :done', $dql);
|
||||
$this->assertStringContainsString('season_assignment.deletedAt IS NULL', $dql);
|
||||
$this->assertStringContainsString('teamer.deletedAt IS NULL', $dql);
|
||||
// the period is the assignment's own, so there is no destination left to join
|
||||
$this->assertStringNotContainsString('destination', $dql);
|
||||
}
|
||||
|
||||
/**
|
||||
* An inner join, not a left one: an assignment the office has laid out but not staffed yet
|
||||
* has nobody to remind and nobody who could be late.
|
||||
*/
|
||||
public function testAnUnstaffedAssignmentIsLeftOut(): void
|
||||
{
|
||||
$dql = $this->createOpenQueryBuilder()->getQuery()->getDQL();
|
||||
|
||||
$this->assertStringContainsString('INNER JOIN season_assignment.teamer teamer', $dql);
|
||||
$this->assertStringNotContainsString('LEFT JOIN', $dql);
|
||||
}
|
||||
|
||||
public function testTheSeasonWindowIsOptional(): void
|
||||
{
|
||||
$unscoped = $this->createOpenQueryBuilder()->getQuery()->getDQL();
|
||||
$scoped = $this
|
||||
->createOpenQueryBuilder(new \DateTimeImmutable('2026-11-01'), new \DateTimeImmutable('2027-04-01'))
|
||||
->getQuery()
|
||||
->getDQL()
|
||||
;
|
||||
|
||||
$this->assertStringNotContainsString('windowStart', $unscoped);
|
||||
$this->assertStringContainsString('season_assignment.dateFrom >= :windowStart', $scoped);
|
||||
$this->assertStringContainsString('season_assignment.dateFrom <= :windowEnd', $scoped);
|
||||
}
|
||||
|
||||
private function createOpenQueryBuilder(
|
||||
?\DateTimeImmutable $windowStart = null,
|
||||
?\DateTimeImmutable $windowEnd = null,
|
||||
): QueryBuilder {
|
||||
/** @var EntityManagerInterface $entityManager */
|
||||
$entityManager = static::getContainer()->get(EntityManagerInterface::class);
|
||||
|
||||
/** @var SeasonAssignmentTaskRepository $repository */
|
||||
$repository = $entityManager->getRepository(SeasonAssignmentTask::class);
|
||||
|
||||
return $repository->createOpenQueryBuilder($windowStart, $windowEnd);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Repository;
|
||||
|
||||
use App\Model\TeamerFilterDto;
|
||||
use App\Repository\TeamerRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* The all-season list is the ordinary teamer list under a scope, so what separates the two is a
|
||||
* single condition in the shared query builder. Compiled here so the scope cannot quietly stop
|
||||
* being applied - an unscoped "Saisonteamer" list would silently show the whole team.
|
||||
*/
|
||||
class TeamerRepositoryTest extends KernelTestCase
|
||||
{
|
||||
public function testTheScopedListRestrictsToAllSeasonTeamers(): void
|
||||
{
|
||||
$query = $this->getRepository()->getListQuery((new TeamerFilterDto())->setAllSeason(true));
|
||||
|
||||
$this->assertStringContainsString('teamer.allSeason = :allSeason', $query->getDQL());
|
||||
$this->assertTrue($query->getParameter('allSeason')->getValue());
|
||||
}
|
||||
|
||||
public function testTheGeneralListIsNotRestricted(): void
|
||||
{
|
||||
$query = $this->getRepository()->getListQuery(new TeamerFilterDto());
|
||||
|
||||
$this->assertStringNotContainsString('teamer.allSeason', $query->getDQL());
|
||||
$this->assertNull($query->getParameter('allSeason'));
|
||||
}
|
||||
|
||||
/**
|
||||
* The mailing resolves its recipients through the same builder, so a mailing started from
|
||||
* the all-season list must not be able to reach the rest of the team.
|
||||
*/
|
||||
public function testTheMailingRecipientsCarryTheSameScope(): void
|
||||
{
|
||||
$repository = $this->getRepository();
|
||||
|
||||
$reflection = new \ReflectionMethod($repository, 'createListQueryBuilder');
|
||||
$queryBuilder = $reflection->invoke($repository, (new TeamerFilterDto())->setAllSeason(true));
|
||||
|
||||
$this->assertStringContainsString('teamer.allSeason = :allSeason', $queryBuilder->getDQL());
|
||||
}
|
||||
|
||||
/**
|
||||
* The season assignment form picks its teamer through this endpoint, so an unrestricted
|
||||
* suggestion list would let an admin place a short term teamer on a season assignment.
|
||||
*/
|
||||
public function testTheAutocompleteCanBeRestrictedToAllSeasonTeamers(): void
|
||||
{
|
||||
$dql = $this->createAutocompletionQueryBuilder(true)->getDQL();
|
||||
|
||||
$this->assertStringContainsString('teamer.allSeason = :allSeason', $dql);
|
||||
}
|
||||
|
||||
public function testTheAutocompleteIsUnrestrictedByDefault(): void
|
||||
{
|
||||
$dql = $this->createAutocompletionQueryBuilder(false)->getDQL();
|
||||
|
||||
$this->assertStringNotContainsString('teamer.allSeason', $dql);
|
||||
$this->assertStringContainsString('teamer.deletedAt IS NULL', $dql);
|
||||
}
|
||||
|
||||
private function createAutocompletionQueryBuilder(bool $allSeasonOnly): QueryBuilder
|
||||
{
|
||||
$repository = $this->getRepository();
|
||||
$reflection = new \ReflectionMethod($repository, 'createAutocompletionQueryBuilder');
|
||||
|
||||
return $reflection->invoke($repository, '', $allSeasonOnly);
|
||||
}
|
||||
|
||||
private function getRepository(): TeamerRepository
|
||||
{
|
||||
self::bootKernel();
|
||||
|
||||
return self::getContainer()->get(TeamerRepository::class);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Repository;
|
||||
|
||||
use App\Entity\Upload;
|
||||
use App\Repository\UploadRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
|
||||
/**
|
||||
* The document picker decides which files an administrative setting may point at, so the type
|
||||
* restriction is pinned here: unrestricted it offers every upload in the system, which would let
|
||||
* a setting reference a teamer's Honorarvertrag or Beleg.
|
||||
*/
|
||||
class UploadRepositoryTest extends KernelTestCase
|
||||
{
|
||||
public function testTheAutocompleteOnlyOffersTheGlobalLibrary(): void
|
||||
{
|
||||
$qb = $this->createAutocompletionQueryBuilder();
|
||||
|
||||
$this->assertStringContainsString('upload.type = :type', $qb->getDQL());
|
||||
$this->assertSame(
|
||||
Upload::TYPE_DOCUMENT,
|
||||
$qb->getQuery()->getParameter('type')->getValue()
|
||||
);
|
||||
}
|
||||
|
||||
public function testTheAutocompleteStillSearchesBothNames(): void
|
||||
{
|
||||
$dql = $this->createAutocompletionQueryBuilder()->getDQL();
|
||||
|
||||
$this->assertStringContainsString('upload.originalFilename LIKE :filename', $dql);
|
||||
$this->assertStringContainsString('upload.displayName LIKE :filename', $dql);
|
||||
}
|
||||
|
||||
private function createAutocompletionQueryBuilder(): QueryBuilder
|
||||
{
|
||||
/** @var EntityManagerInterface $entityManager */
|
||||
$entityManager = static::getContainer()->get(EntityManagerInterface::class);
|
||||
|
||||
/** @var UploadRepository $repository */
|
||||
$repository = $entityManager->getRepository(Upload::class);
|
||||
|
||||
$reflection = new \ReflectionMethod($repository, 'createAutocompletionQueryBuilder');
|
||||
|
||||
return $reflection->invoke($repository, '');
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user