voter = new TeacherReplacementVoter(); } #[Test] public function itAbstainsForUnrelatedAttributes(): void { $token = $this->tokenWithSecurityUser(Role::ADMIN->value); $result = $this->voter->vote($token, null, ['SOME_OTHER_ATTRIBUTE']); self::assertSame(Voter::ACCESS_ABSTAIN, $result); } #[Test] public function itDeniesAccessToUnauthenticatedUsers(): void { $token = $this->createMock(TokenInterface::class); $token->method('getUser')->willReturn(null); $result = $this->voter->vote($token, null, [TeacherReplacementVoter::VIEW]); self::assertSame(Voter::ACCESS_DENIED, $result); } #[Test] public function itDeniesAccessToNonSecurityUserInstances(): void { $user = $this->createMock(UserInterface::class); $user->method('getRoles')->willReturn([Role::ADMIN->value]); $token = $this->createMock(TokenInterface::class); $token->method('getUser')->willReturn($user); $result = $this->voter->vote($token, null, [TeacherReplacementVoter::VIEW]); self::assertSame(Voter::ACCESS_DENIED, $result); } // --- VIEW --- #[Test] #[DataProvider('adminRolesProvider')] public function itGrantsViewToAdminRoles(string $role): void { $token = $this->tokenWithSecurityUser($role); $result = $this->voter->vote($token, null, [TeacherReplacementVoter::VIEW]); self::assertSame(Voter::ACCESS_GRANTED, $result); } #[Test] public function itGrantsViewToReplacedTeacher(): void { $token = $this->tokenWithSecurityUser(Role::PROF->value, self::REPLACED_TEACHER_ID); $replacement = $this->createReplacement(); $result = $this->voter->vote($token, $replacement, [TeacherReplacementVoter::VIEW]); self::assertSame(Voter::ACCESS_GRANTED, $result); } #[Test] public function itGrantsViewToReplacementTeacher(): void { $token = $this->tokenWithSecurityUser(Role::PROF->value, self::REPLACEMENT_TEACHER_ID); $replacement = $this->createReplacement(); $result = $this->voter->vote($token, $replacement, [TeacherReplacementVoter::VIEW]); self::assertSame(Voter::ACCESS_GRANTED, $result); } #[Test] public function itDeniesViewToUninvolvedTeacher(): void { $token = $this->tokenWithSecurityUser(Role::PROF->value, '550e8400-e29b-41d4-a716-446655440099'); $replacement = $this->createReplacement(); $result = $this->voter->vote($token, $replacement, [TeacherReplacementVoter::VIEW]); self::assertSame(Voter::ACCESS_DENIED, $result); } #[Test] public function itDeniesViewToTeacherWithoutSubject(): void { $token = $this->tokenWithSecurityUser(Role::PROF->value, self::REPLACED_TEACHER_ID); $result = $this->voter->vote($token, null, [TeacherReplacementVoter::VIEW]); self::assertSame(Voter::ACCESS_DENIED, $result); } #[Test] #[DataProvider('nonStaffRolesProvider')] public function itDeniesViewToNonStaffRoles(string $role): void { $token = $this->tokenWithSecurityUser($role); $result = $this->voter->vote($token, null, [TeacherReplacementVoter::VIEW]); self::assertSame(Voter::ACCESS_DENIED, $result); } // --- CREATE --- #[Test] #[DataProvider('adminRolesProvider')] public function itGrantsCreateToAdminRoles(string $role): void { $token = $this->tokenWithSecurityUser($role); $result = $this->voter->vote($token, null, [TeacherReplacementVoter::CREATE]); self::assertSame(Voter::ACCESS_GRANTED, $result); } #[Test] #[DataProvider('nonAdminRolesProvider')] public function itDeniesCreateToNonAdminRoles(string $role): void { $token = $this->tokenWithSecurityUser($role); $result = $this->voter->vote($token, null, [TeacherReplacementVoter::CREATE]); self::assertSame(Voter::ACCESS_DENIED, $result); } // --- DELETE --- #[Test] #[DataProvider('adminRolesProvider')] public function itGrantsDeleteToAdminRoles(string $role): void { $token = $this->tokenWithSecurityUser($role); $result = $this->voter->vote($token, null, [TeacherReplacementVoter::DELETE]); self::assertSame(Voter::ACCESS_GRANTED, $result); } #[Test] #[DataProvider('nonAdminRolesProvider')] public function itDeniesDeleteToNonAdminRoles(string $role): void { $token = $this->tokenWithSecurityUser($role); $result = $this->voter->vote($token, null, [TeacherReplacementVoter::DELETE]); self::assertSame(Voter::ACCESS_DENIED, $result); } // --- Data Providers --- /** @return iterable */ public static function adminRolesProvider(): iterable { yield 'SUPER_ADMIN' => [Role::SUPER_ADMIN->value]; yield 'ADMIN' => [Role::ADMIN->value]; } /** @return iterable */ public static function nonAdminRolesProvider(): iterable { yield 'PROF' => [Role::PROF->value]; yield 'VIE_SCOLAIRE' => [Role::VIE_SCOLAIRE->value]; yield 'SECRETARIAT' => [Role::SECRETARIAT->value]; yield 'PARENT' => [Role::PARENT->value]; yield 'ELEVE' => [Role::ELEVE->value]; } /** @return iterable */ public static function nonStaffRolesProvider(): iterable { yield 'PARENT' => [Role::PARENT->value]; yield 'ELEVE' => [Role::ELEVE->value]; } private function createReplacement(): TeacherReplacement { return TeacherReplacement::designer( tenantId: TenantId::fromString(self::TENANT_ID), 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('550e8400-e29b-41d4-a716-446655440020'), SubjectId::fromString('550e8400-e29b-41d4-a716-446655440030'), ), ], reason: null, createdBy: UserId::fromString('550e8400-e29b-41d4-a716-446655440099'), now: new DateTimeImmutable('2026-02-15 10:00:00'), ); } private function tokenWithSecurityUser( string $role, string $userId = '550e8400-e29b-41d4-a716-446655440001', ): TokenInterface { $securityUser = new SecurityUser( UserId::fromString($userId), 'test@example.com', 'hashed_password', TenantId::fromString(self::TENANT_ID), [$role], ); $token = $this->createMock(TokenInterface::class); $token->method('getUser')->willReturn($securityUser); return $token; } }