repository = new InMemoryTeacherReplacementRepository(); $this->tenantContext = new TenantContext(); $this->clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2026-03-15 10:00:00'); } }; } #[Test] public function itProvidesCollectionOfActiveReplacements(): void { $provider = $this->createProvider(granted: true); $this->setTenant(); $replacement = $this->createReplacement(); $this->repository->save($replacement); $result = $provider->provide(new GetCollection()); self::assertCount(1, $result); self::assertContainsOnlyInstancesOf(TeacherReplacementResource::class, $result); self::assertSame((string) $replacement->id, $result[0]->id); self::assertSame(self::REPLACED_TEACHER_ID, $result[0]->replacedTeacherId); self::assertSame(self::REPLACEMENT_TEACHER_ID, $result[0]->replacementTeacherId); self::assertSame('active', $result[0]->status); } #[Test] public function itReturnsEmptyWhenNoActiveReplacements(): void { $provider = $this->createProvider(granted: true); $this->setTenant(); $result = $provider->provide(new GetCollection()); self::assertSame([], $result); } #[Test] public function itRejectsUnauthorizedAccess(): void { $provider = $this->createProvider(granted: false); $this->setTenant(); $this->expectException(AccessDeniedHttpException::class); $provider->provide(new GetCollection()); } #[Test] public function itRejectsRequestWithoutTenant(): void { $provider = $this->createProvider(granted: true); $this->expectException(UnauthorizedHttpException::class); $provider->provide(new GetCollection()); } private function setTenant(): void { $this->tenantContext->setCurrentTenant(new TenantConfig( tenantId: InfraTenantId::fromString(self::TENANT_UUID), subdomain: 'test', databaseUrl: 'sqlite:///:memory:', )); } private function createProvider(bool $granted): TeacherReplacementsCollectionProvider { $authChecker = new class($granted) implements AuthorizationCheckerInterface { public function __construct(private readonly bool $granted) { } public function isGranted(mixed $attribute, mixed $subject = null, ?AccessDecision $accessDecision = null): bool { return $this->granted; } }; $handler = new GetActiveReplacementsHandler($this->repository, $this->clock); return new TeacherReplacementsCollectionProvider($handler, $this->tenantContext, $authChecker); } private function createReplacement(): TeacherReplacement { return TeacherReplacement::designer( tenantId: TenantId::fromString(self::TENANT_UUID), replacedTeacherId: UserId::fromString(self::REPLACED_TEACHER_ID), replacementTeacherId: UserId::fromString(self::REPLACEMENT_TEACHER_ID), startDate: new DateTimeImmutable('2026-03-01'), endDate: new DateTimeImmutable('2026-03-31'), classes: [ new ClassSubjectPair( ClassId::fromString(self::CLASS_ID), SubjectId::fromString(self::SUBJECT_ID), ), ], reason: 'Congé maladie', createdBy: UserId::fromString(self::CREATED_BY_ID), now: new DateTimeImmutable('2026-02-15 10:00:00'), ); } }