createMock(Connection::class); $calendar = SchoolCalendar::initialiser( tenantId: TenantId::fromString(self::TENANT_ID), academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID), ); $calendar->ajouterEntree(new CalendarEntry( id: CalendarEntryId::fromString('550e8400-e29b-41d4-a716-446655440020'), type: CalendarEntryType::HOLIDAY, startDate: new DateTimeImmutable('2025-11-01'), endDate: new DateTimeImmutable('2025-11-01'), label: 'Toussaint', )); $connection->expects(self::once()) ->method('transactional') ->willReturnCallback(static function (callable $callback) { $callback(); }); // Expect: 1 DELETE + 1 INSERT $connection->expects(self::exactly(2)) ->method('executeStatement') ->with( self::logicalOr( self::stringContains('DELETE FROM school_calendar_entries'), self::stringContains('INSERT INTO school_calendar_entries'), ), self::isType('array'), ); $repository = new DoctrineSchoolCalendarRepository($connection); $repository->save($calendar); } #[Test] public function saveWithMultipleEntriesExecutesMultipleInserts(): void { $connection = $this->createMock(Connection::class); $calendar = SchoolCalendar::initialiser( tenantId: TenantId::fromString(self::TENANT_ID), academicYearId: AcademicYearId::fromString(self::ACADEMIC_YEAR_ID), ); $calendar->ajouterEntree(new CalendarEntry( id: CalendarEntryId::fromString('550e8400-e29b-41d4-a716-446655440020'), type: CalendarEntryType::HOLIDAY, startDate: new DateTimeImmutable('2025-11-01'), endDate: new DateTimeImmutable('2025-11-01'), label: 'Toussaint', )); $calendar->ajouterEntree(new CalendarEntry( id: CalendarEntryId::fromString('550e8400-e29b-41d4-a716-446655440021'), type: CalendarEntryType::VACATION, startDate: new DateTimeImmutable('2025-12-21'), endDate: new DateTimeImmutable('2026-01-05'), label: 'Noël', )); $connection->expects(self::once()) ->method('transactional') ->willReturnCallback(static function (callable $callback) { $callback(); }); // Expect: 1 DELETE + 2 INSERTs = 3 calls $connection->expects(self::exactly(3)) ->method('executeStatement'); $repository = new DoctrineSchoolCalendarRepository($connection); $repository->save($calendar); } #[Test] public function findByTenantAndYearReturnsNullWhenNoEntries(): void { $connection = $this->createMock(Connection::class); $connection->method('fetchAllAssociative')->willReturn([]); $repository = new DoctrineSchoolCalendarRepository($connection); $calendar = $repository->findByTenantAndYear( TenantId::fromString(self::TENANT_ID), AcademicYearId::fromString(self::ACADEMIC_YEAR_ID), ); self::assertNull($calendar); } #[Test] public function findByTenantAndYearReturnsCalendarWithEntries(): void { $connection = $this->createMock(Connection::class); $connection->method('fetchAllAssociative') ->willReturn([ $this->makeRow('550e8400-e29b-41d4-a716-446655440020', 'holiday', '2025-11-01', '2025-11-01', 'Toussaint', null, 'A'), $this->makeRow('550e8400-e29b-41d4-a716-446655440021', 'vacation', '2025-12-21', '2026-01-05', 'Noël', 'Vacances de Noël', 'A'), ]); $repository = new DoctrineSchoolCalendarRepository($connection); $calendar = $repository->findByTenantAndYear( TenantId::fromString(self::TENANT_ID), AcademicYearId::fromString(self::ACADEMIC_YEAR_ID), ); self::assertNotNull($calendar); self::assertCount(2, $calendar->entries()); self::assertSame(SchoolZone::A, $calendar->zone); self::assertSame('Toussaint', $calendar->entries()[0]->label); self::assertSame('Noël', $calendar->entries()[1]->label); } #[Test] public function findByTenantAndYearHandlesNullZone(): void { $connection = $this->createMock(Connection::class); $connection->method('fetchAllAssociative') ->willReturn([ $this->makeRow('550e8400-e29b-41d4-a716-446655440020', 'pedagogical', '2025-03-14', '2025-03-14', 'Formation', null, null), ]); $repository = new DoctrineSchoolCalendarRepository($connection); $calendar = $repository->findByTenantAndYear( TenantId::fromString(self::TENANT_ID), AcademicYearId::fromString(self::ACADEMIC_YEAR_ID), ); self::assertNotNull($calendar); self::assertNull($calendar->zone); } #[Test] public function getByTenantAndYearThrowsWhenNotFound(): void { $connection = $this->createMock(Connection::class); $connection->method('fetchAllAssociative')->willReturn([]); $repository = new DoctrineSchoolCalendarRepository($connection); $this->expectException(CalendrierNonTrouveException::class); $repository->getByTenantAndYear( TenantId::fromString(self::TENANT_ID), AcademicYearId::fromString(self::ACADEMIC_YEAR_ID), ); } #[Test] public function getByTenantAndYearReturnsCalendarWhenFound(): void { $connection = $this->createMock(Connection::class); $connection->method('fetchAllAssociative') ->willReturn([ $this->makeRow('550e8400-e29b-41d4-a716-446655440020', 'holiday', '2025-05-01', '2025-05-01', 'Fête du travail', null, 'B'), ]); $repository = new DoctrineSchoolCalendarRepository($connection); $calendar = $repository->getByTenantAndYear( TenantId::fromString(self::TENANT_ID), AcademicYearId::fromString(self::ACADEMIC_YEAR_ID), ); self::assertCount(1, $calendar->entries()); self::assertSame(SchoolZone::B, $calendar->zone); } /** * @return array */ private function makeRow( string $id, string $entryType, string $startDate, string $endDate, string $label, ?string $description, ?string $zone, ): array { return [ 'id' => $id, 'tenant_id' => self::TENANT_ID, 'academic_year_id' => self::ACADEMIC_YEAR_ID, 'entry_type' => $entryType, 'start_date' => $startDate, 'end_date' => $endDate, 'label' => $label, 'description' => $description, 'zone' => $zone, 'created_at' => '2026-02-17T10:00:00+00:00', ]; } }