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('first@ep-reisen.de'), $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('first@ep-reisen.de'), $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('first@ep-reisen.de'), $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('first@ep-reisen.de'); $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('first@ep-reisen.de'), $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('anna@ep-reisen.de'); $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('anna@ep-reisen.de', '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('anna@ep-reisen.de'); $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('bea@ep-reisen.de'), $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('anna@ep-reisen.de'); $this->persist(new Booking($user, $this->resource('D1'), new \DateTimeImmutable('2026-10-01'))); $this->persist(new Booking($this->user('bea@ep-reisen.de'), $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('anna@ep-reisen.de'), $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('anna@ep-reisen.de'); $this->persist(new Booking($anna, $this->resource('D1'), new \DateTimeImmutable('2026-10-01'))); $this->persist(new Booking($this->user('bea@ep-reisen.de'), $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; } }