repository = new InMemoryPeriodConfigurationRepository(); $this->tenantContext = new TenantContext(); $this->clock = new class implements Clock { public function now(): DateTimeImmutable { return new DateTimeImmutable('2025-10-15 10:00:00'); } }; } #[Test] public function itRejectsUnauthorizedAccess(): void { $provider = $this->createProvider(granted: false); $this->setTenant(); $this->expectException(AccessDeniedHttpException::class); $provider->provide(new Get(), ['academicYearId' => 'current']); } #[Test] public function itRejectsRequestWithoutTenant(): void { $provider = $this->createProvider(granted: true); $this->expectException(UnauthorizedHttpException::class); $provider->provide(new Get(), ['academicYearId' => 'current']); } #[Test] public function itRejectsInvalidAcademicYearId(): void { $provider = $this->createProvider(granted: true); $this->setTenant(); $this->expectException(NotFoundHttpException::class); $provider->provide(new Get(), ['academicYearId' => 'invalid']); } #[Test] public function itReturnsNullWhenNoPeriodsConfigured(): void { $provider = $this->createProvider(granted: true); $this->setTenant(); $result = $provider->provide(new Get(), ['academicYearId' => 'current']); self::assertNull($result); } #[Test] public function itResolvesCurrentToValidUuidAndReturnsPeriods(): void { $provider = $this->createProvider(granted: true); $this->setTenant(); // Seed periods using the same resolved UUID $resolver = new CurrentAcademicYearResolver($this->tenantContext, $this->clock); $academicYearId = $resolver->resolve('current'); self::assertNotNull($academicYearId); $config = DefaultPeriods::forType(PeriodType::TRIMESTER, 2025); $this->repository->save( TenantId::fromString(self::TENANT_UUID), AcademicYearId::fromString($academicYearId), $config, ); $result = $provider->provide(new Get(), ['academicYearId' => 'current']); self::assertInstanceOf(PeriodResource::class, $result); self::assertSame($academicYearId, $result->academicYearId); self::assertSame('trimester', $result->type); self::assertCount(3, $result->periods); self::assertContainsOnlyInstancesOf(PeriodItem::class, $result->periods); } #[Test] public function itResolvesNextAcademicYear(): void { $provider = $this->createProvider(granted: true); $this->setTenant(); $result = $provider->provide(new Get(), ['academicYearId' => 'next']); // No periods for next year → null self::assertNull($result); } #[Test] public function itResolvesPreviousAcademicYear(): void { $provider = $this->createProvider(granted: true); $this->setTenant(); $result = $provider->provide(new Get(), ['academicYearId' => 'previous']); // No periods for previous year → null self::assertNull($result); } #[Test] public function itReturnsPeriodItemsWithCorrectFields(): void { $provider = $this->createProvider(granted: true); $this->setTenant(); $resolver = new CurrentAcademicYearResolver($this->tenantContext, $this->clock); $academicYearId = $resolver->resolve('current'); self::assertNotNull($academicYearId); $config = DefaultPeriods::forType(PeriodType::TRIMESTER, 2025); $this->repository->save( TenantId::fromString(self::TENANT_UUID), AcademicYearId::fromString($academicYearId), $config, ); $result = $provider->provide(new Get(), ['academicYearId' => 'current']); self::assertNotNull($result); $firstPeriod = $result->periods[0]; self::assertInstanceOf(PeriodItem::class, $firstPeriod); self::assertSame(1, $firstPeriod->sequence); self::assertSame('T1', $firstPeriod->label); self::assertSame('2025-09-01', $firstPeriod->startDate); self::assertSame('2025-11-30', $firstPeriod->endDate); self::assertTrue($firstPeriod->isCurrent); } #[Test] public function itReturnsCurrentPeriodBanner(): void { $provider = $this->createProvider(granted: true); $this->setTenant(); $resolver = new CurrentAcademicYearResolver($this->tenantContext, $this->clock); $academicYearId = $resolver->resolve('current'); self::assertNotNull($academicYearId); $config = DefaultPeriods::forType(PeriodType::TRIMESTER, 2025); $this->repository->save( TenantId::fromString(self::TENANT_UUID), AcademicYearId::fromString($academicYearId), $config, ); $result = $provider->provide(new Get(), ['academicYearId' => 'current']); self::assertNotNull($result); self::assertInstanceOf(PeriodItem::class, $result->currentPeriod); self::assertSame('T1', $result->currentPeriod->label); self::assertTrue($result->currentPeriod->isCurrent); } 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): PeriodsProvider { $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 GetPeriodsHandler($this->repository, $this->clock); $resolver = new CurrentAcademicYearResolver($this->tenantContext, $this->clock); return new PeriodsProvider($handler, $this->tenantContext, $authChecker, $resolver); } }