repository = new InMemoryGradingConfigurationRepository(); $this->tenantContext = new TenantContext(); $this->clock = new class implements Clock { #[Override] 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 itReturnsDefaultModeWhenNoConfigurationExists(): void { $provider = $this->createProvider(granted: true); $this->setTenant(); $result = $provider->provide(new Get(), ['academicYearId' => 'current']); self::assertInstanceOf(GradingModeResource::class, $result); self::assertSame('numeric_20', $result->mode); self::assertSame('Notes /20', $result->label); self::assertFalse($result->hasExistingGrades); } #[Test] public function itReturnsExistingConfigurationWhenPresent(): void { $provider = $this->createProvider(granted: true); $this->setTenant(); $resolver = new CurrentAcademicYearResolver($this->tenantContext, $this->clock); $academicYearId = $resolver->resolve('current'); self::assertNotNull($academicYearId); $schoolIdResolver = new SchoolIdResolver(); $schoolId = $schoolIdResolver->resolveForTenant(self::TENANT_UUID); $config = SchoolGradingConfiguration::configurer( tenantId: TenantId::fromString(self::TENANT_UUID), schoolId: SchoolId::fromString($schoolId), academicYearId: AcademicYearId::fromString($academicYearId), mode: GradingMode::COMPETENCIES, hasExistingGrades: false, configuredAt: new DateTimeImmutable(), ); $this->repository->save($config); $result = $provider->provide(new Get(), ['academicYearId' => 'current']); self::assertInstanceOf(GradingModeResource::class, $result); self::assertSame('competencies', $result->mode); self::assertSame('Compétences', $result->label); } #[Test] public function itSetsAvailableModesOnResult(): void { $provider = $this->createProvider(granted: true); $this->setTenant(); $result = $provider->provide(new Get(), ['academicYearId' => 'current']); self::assertNotNull($result->availableModes); self::assertCount(5, $result->availableModes); } #[Test] public function itSetsHasExistingGradesFlag(): void { $provider = $this->createProvider(granted: true, hasGrades: true); $this->setTenant(); $result = $provider->provide(new Get(), ['academicYearId' => 'current']); self::assertTrue($result->hasExistingGrades); } #[Test] public function itResolvesNextAcademicYear(): void { $provider = $this->createProvider(granted: true); $this->setTenant(); $result = $provider->provide(new Get(), ['academicYearId' => 'next']); self::assertInstanceOf(GradingModeResource::class, $result); self::assertSame('numeric_20', $result->mode); } #[Test] public function itResolvesPreviousAcademicYear(): void { $provider = $this->createProvider(granted: true); $this->setTenant(); $result = $provider->provide(new Get(), ['academicYearId' => 'previous']); self::assertInstanceOf(GradingModeResource::class, $result); self::assertSame('numeric_20', $result->mode); } 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, bool $hasGrades = false): GradingModeProvider { $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; } }; $gradeChecker = new class($hasGrades) implements GradeExistenceChecker { public function __construct(private readonly bool $hasGrades) { } #[Override] public function hasGradesInPeriod(TenantId $tenantId, AcademicYearId $academicYearId, int $periodSequence): bool { return $this->hasGrades; } #[Override] public function hasGradesForYear(TenantId $tenantId, SchoolId $schoolId, AcademicYearId $academicYearId): bool { return $this->hasGrades; } }; $resolver = new CurrentAcademicYearResolver($this->tenantContext, $this->clock); $schoolIdResolver = new SchoolIdResolver(); return new GradingModeProvider( $this->repository, $gradeChecker, $this->tenantContext, $authChecker, $resolver, $schoolIdResolver, ); } }