204 lines
7.9 KiB
PHP
204 lines
7.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Repository;
|
|
|
|
use App\Entity\Booking;
|
|
use App\Entity\Resource;
|
|
use App\Entity\User;
|
|
use App\Enum\ResourceType;
|
|
use App\Model\TimeRange;
|
|
use App\Repository\BookingRepository;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
|
|
|
/**
|
|
* Hits the real database, because these are queries.
|
|
*
|
|
* Note what is NOT here any more: the double-booking guard. It used to be a unique index on
|
|
* (resource, date) and was tested by asserting the insert blew up. A booking is now a time range,
|
|
* which no unique index can guard, so the guard lives in BookingManager and is tested there --
|
|
* what this class covers is the overlap query that guard relies on.
|
|
*
|
|
* Each test runs inside a transaction that is rolled back afterwards.
|
|
*/
|
|
class BookingRepositoryTest extends KernelTestCase
|
|
{
|
|
private EntityManagerInterface $em;
|
|
private BookingRepository $bookings;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
self::bootKernel();
|
|
$this->em = self::getContainer()->get(EntityManagerInterface::class);
|
|
$this->bookings = self::getContainer()->get(BookingRepository::class);
|
|
$this->em->getConnection()->beginTransaction();
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
$this->em->getConnection()->rollBack();
|
|
parent::tearDown();
|
|
}
|
|
|
|
public function testFindOverlappingSeesASpanThatCollides(): void
|
|
{
|
|
$desk = $this->resource('D1');
|
|
$date = new \DateTimeImmutable('2026-10-01');
|
|
|
|
$this->persist(new Booking($this->user('[email protected]'), $desk, $date, TimeRange::fromMinutes(9 * 60, 13 * 60)));
|
|
$this->em->flush();
|
|
|
|
self::assertCount(1, $this->bookings->findOverlapping($desk, $date, TimeRange::fromMinutes(11 * 60, 15 * 60)));
|
|
}
|
|
|
|
public function testFindOverlappingTreatsTouchingSpansAsFree(): void
|
|
{
|
|
$desk = $this->resource('D1');
|
|
$date = new \DateTimeImmutable('2026-10-01');
|
|
|
|
$this->persist(new Booking($this->user('[email protected]'), $desk, $date, TimeRange::fromMinutes(9 * 60, 13 * 60)));
|
|
$this->em->flush();
|
|
|
|
// Half-open: 09:00-13:00 and 13:00-17:00 do not collide.
|
|
self::assertSame([], $this->bookings->findOverlapping($desk, $date, TimeRange::fromMinutes(13 * 60, 17 * 60)));
|
|
}
|
|
|
|
public function testAnAllDayBookingBlocksEverySpan(): void
|
|
{
|
|
$desk = $this->resource('D1');
|
|
$date = new \DateTimeImmutable('2026-10-01');
|
|
|
|
$this->persist(new Booking($this->user('[email protected]'), $desk, $date));
|
|
$this->em->flush();
|
|
|
|
self::assertCount(1, $this->bookings->findOverlapping($desk, $date, TimeRange::fromMinutes(8 * 60, 8 * 60 + 15)));
|
|
}
|
|
|
|
public function testTheSameResourceOnAnotherDayIsFine(): void
|
|
{
|
|
$desk = $this->resource('D1');
|
|
$user = $this->user('[email protected]');
|
|
|
|
$this->persist(new Booking($user, $desk, new \DateTimeImmutable('2026-10-01')));
|
|
$this->persist(new Booking($user, $desk, new \DateTimeImmutable('2026-10-02')));
|
|
$this->em->flush();
|
|
|
|
self::assertCount(1, $this->bookings->findForDate(new \DateTimeImmutable('2026-10-01')));
|
|
}
|
|
|
|
public function testATimeComponentOnTheDateDoesNotShiftWhichDayABookingIsOn(): void
|
|
{
|
|
$desk = $this->resource('D1');
|
|
|
|
$this->persist(new Booking($this->user('[email protected]'), $desk, new \DateTimeImmutable('2026-10-01 17:30')));
|
|
$this->em->flush();
|
|
$this->em->clear();
|
|
|
|
self::assertCount(1, $this->bookings->findForDate(new \DateTimeImmutable('2026-10-01')));
|
|
}
|
|
|
|
public function testFindForDateOnlyReturnsWhatOverlapsTheSpan(): void
|
|
{
|
|
$date = new \DateTimeImmutable('2026-10-01');
|
|
$user = $this->user('[email protected]');
|
|
|
|
$this->persist(new Booking($user, $this->resource('D1'), $date, TimeRange::fromMinutes(9 * 60, 12 * 60)));
|
|
$this->persist(new Booking($user, $this->resource('D2'), $date, TimeRange::fromMinutes(14 * 60, 17 * 60)));
|
|
$this->em->flush();
|
|
|
|
$morning = $this->bookings->findForDate($date, TimeRange::fromMinutes(8 * 60, 13 * 60));
|
|
|
|
self::assertCount(1, $morning);
|
|
self::assertSame('D1', $morning[0]->getResource()->getLabel());
|
|
self::assertCount(2, $this->bookings->findForDate($date));
|
|
}
|
|
|
|
public function testFindForDateCarriesTheBookingHolderForTheTeamView(): void
|
|
{
|
|
$this->persist(new Booking($this->user('[email protected]', 'Anna'), $this->resource('D1'), new \DateTimeImmutable('2026-10-01')));
|
|
$this->em->flush();
|
|
$this->em->clear();
|
|
|
|
$bookings = $this->bookings->findForDate(new \DateTimeImmutable('2026-10-01'));
|
|
|
|
self::assertCount(1, $bookings);
|
|
self::assertSame('Anna', $bookings[0]->getUser()->getName());
|
|
self::assertSame('D1', $bookings[0]->getResource()->getLabel());
|
|
}
|
|
|
|
public function testCountPerDateOnlyCountsWhatOverlapsTheSpan(): void
|
|
{
|
|
$user = $this->user('[email protected]');
|
|
$this->persist(new Booking($user, $this->resource('D1'), new \DateTimeImmutable('2026-10-01'), TimeRange::fromMinutes(9 * 60, 12 * 60)));
|
|
$this->persist(new Booking($this->user('[email protected]'), $this->resource('D2'), new \DateTimeImmutable('2026-10-01'), TimeRange::fromMinutes(14 * 60, 17 * 60)));
|
|
$this->em->flush();
|
|
|
|
$counts = $this->bookings->countPerDateInRange(
|
|
new \DateTimeImmutable('2026-10-01'),
|
|
new \DateTimeImmutable('2026-10-31'),
|
|
TimeRange::fromMinutes(8 * 60, 13 * 60),
|
|
);
|
|
|
|
// The calendar number has to mean the same thing as the colours on the plan, so it is
|
|
// filtered by the same span.
|
|
self::assertSame(['2026-10-01' => 1], $counts);
|
|
}
|
|
|
|
public function testCountPerDateInRangeIsKeyedByDay(): void
|
|
{
|
|
$user = $this->user('[email protected]');
|
|
$this->persist(new Booking($user, $this->resource('D1'), new \DateTimeImmutable('2026-10-01')));
|
|
$this->persist(new Booking($this->user('[email protected]'), $this->resource('D2'), new \DateTimeImmutable('2026-10-01')));
|
|
$this->persist(new Booking($user, $this->resource('P1', ResourceType::PARKING), new \DateTimeImmutable('2026-10-03')));
|
|
$this->em->flush();
|
|
|
|
$counts = $this->bookings->countPerDateInRange(new \DateTimeImmutable('2026-10-01'), new \DateTimeImmutable('2026-10-31'));
|
|
|
|
self::assertSame(['2026-10-01' => 2, '2026-10-03' => 1], $counts);
|
|
}
|
|
|
|
public function testCountPerDateIgnoresDaysOutsideTheWindow(): void
|
|
{
|
|
$this->persist(new Booking($this->user('[email protected]'), $this->resource('D1'), new \DateTimeImmutable('2026-11-05')));
|
|
$this->em->flush();
|
|
|
|
self::assertSame([], $this->bookings->countPerDateInRange(new \DateTimeImmutable('2026-10-01'), new \DateTimeImmutable('2026-10-31')));
|
|
}
|
|
|
|
public function testFindForUserInRangeOnlyReturnsThatPersonsBookings(): void
|
|
{
|
|
$anna = $this->user('[email protected]');
|
|
$this->persist(new Booking($anna, $this->resource('D1'), new \DateTimeImmutable('2026-10-01')));
|
|
$this->persist(new Booking($this->user('[email protected]'), $this->resource('D2'), new \DateTimeImmutable('2026-10-01')));
|
|
$this->em->flush();
|
|
|
|
$mine = $this->bookings->findForUserInRange($anna, new \DateTimeImmutable('2026-10-01'), new \DateTimeImmutable('2026-10-31'));
|
|
|
|
self::assertCount(1, $mine);
|
|
self::assertSame('D1', $mine[0]->getResource()->getLabel());
|
|
}
|
|
|
|
private function persist(object $entity): void
|
|
{
|
|
$this->em->persist($entity);
|
|
}
|
|
|
|
private function user(string $email, string $name = 'Test'): User
|
|
{
|
|
$user = new User($email, $name);
|
|
$this->em->persist($user);
|
|
|
|
return $user;
|
|
}
|
|
|
|
private function resource(string $label, ResourceType $type = ResourceType::DESK): Resource
|
|
{
|
|
$resource = new Resource($type, $label);
|
|
$this->em->persist($resource);
|
|
|
|
return $resource;
|
|
}
|
|
}
|