feat: Liaison parents-enfants avec gestion des tuteurs
Les parents doivent pouvoir suivre la scolarité de leurs enfants (notes, emploi du temps, devoirs). Cela nécessite un lien formalisé entre le compte parent et le compte élève, géré par les administrateurs. Le lien est établi soit manuellement via l'interface d'administration, soit automatiquement lors de l'activation du compte parent lorsque l'invitation inclut un élève cible. Ce lien conditionne l'accès aux données scolaires de l'enfant (autorisations vérifiées par un voter dédié).
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Infrastructure\Security;
|
||||
|
||||
use App\Administration\Domain\Model\User\Role;
|
||||
use App\Administration\Infrastructure\Api\Resource\ClassResource;
|
||||
use App\Administration\Infrastructure\Security\ClassVoter;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
final class ClassVoterTest extends TestCase
|
||||
{
|
||||
private ClassVoter $voter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->voter = new ClassVoter();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itAbstainsForUnrelatedAttributes(): void
|
||||
{
|
||||
$token = $this->tokenWithRole(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, [ClassVoter::VIEW]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
// --- VIEW ---
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('viewAllowedRolesProvider')]
|
||||
public function itGrantsViewToStaffRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, null, [ClassVoter::VIEW]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function viewAllowedRolesProvider(): iterable
|
||||
{
|
||||
yield 'SUPER_ADMIN' => [Role::SUPER_ADMIN->value];
|
||||
yield 'ADMIN' => [Role::ADMIN->value];
|
||||
yield 'PROF' => [Role::PROF->value];
|
||||
yield 'VIE_SCOLAIRE' => [Role::VIE_SCOLAIRE->value];
|
||||
yield 'SECRETARIAT' => [Role::SECRETARIAT->value];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('viewDeniedRolesProvider')]
|
||||
public function itDeniesViewToNonStaffRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, null, [ClassVoter::VIEW]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function viewDeniedRolesProvider(): iterable
|
||||
{
|
||||
yield 'PARENT' => [Role::PARENT->value];
|
||||
yield 'ELEVE' => [Role::ELEVE->value];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itSupportsViewWithClassResourceSubject(): void
|
||||
{
|
||||
$token = $this->tokenWithRole(Role::ADMIN->value);
|
||||
$subject = new ClassResource();
|
||||
|
||||
$result = $this->voter->vote($token, $subject, [ClassVoter::VIEW]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
// --- CREATE ---
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('adminRolesProvider')]
|
||||
public function itGrantsCreateToAdminRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, null, [ClassVoter::CREATE]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('nonAdminRolesProvider')]
|
||||
public function itDeniesCreateToNonAdminRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, null, [ClassVoter::CREATE]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
// --- EDIT ---
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('adminRolesProvider')]
|
||||
public function itGrantsEditToAdminRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, new ClassResource(), [ClassVoter::EDIT]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('nonAdminRolesProvider')]
|
||||
public function itDeniesEditToNonAdminRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, new ClassResource(), [ClassVoter::EDIT]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
// --- DELETE ---
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('adminRolesProvider')]
|
||||
public function itGrantsDeleteToAdminRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, new ClassResource(), [ClassVoter::DELETE]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('nonAdminRolesProvider')]
|
||||
public function itDeniesDeleteToNonAdminRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, new ClassResource(), [ClassVoter::DELETE]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
// --- Data Providers ---
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function adminRolesProvider(): iterable
|
||||
{
|
||||
yield 'SUPER_ADMIN' => [Role::SUPER_ADMIN->value];
|
||||
yield 'ADMIN' => [Role::ADMIN->value];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
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];
|
||||
}
|
||||
|
||||
private function tokenWithRole(string $role): TokenInterface
|
||||
{
|
||||
$user = $this->createMock(UserInterface::class);
|
||||
$user->method('getRoles')->willReturn([$role]);
|
||||
|
||||
$token = $this->createMock(TokenInterface::class);
|
||||
$token->method('getUser')->willReturn($user);
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Infrastructure\Security;
|
||||
|
||||
use App\Administration\Domain\Model\User\Role;
|
||||
use App\Administration\Infrastructure\Security\PeriodVoter;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
final class PeriodVoterTest extends TestCase
|
||||
{
|
||||
private PeriodVoter $voter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->voter = new PeriodVoter();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itAbstainsForUnrelatedAttributes(): void
|
||||
{
|
||||
$token = $this->tokenWithRole(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, [PeriodVoter::VIEW]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
// --- VIEW ---
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('viewAllowedRolesProvider')]
|
||||
public function itGrantsViewToStaffRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, null, [PeriodVoter::VIEW]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function viewAllowedRolesProvider(): iterable
|
||||
{
|
||||
yield 'SUPER_ADMIN' => [Role::SUPER_ADMIN->value];
|
||||
yield 'ADMIN' => [Role::ADMIN->value];
|
||||
yield 'PROF' => [Role::PROF->value];
|
||||
yield 'VIE_SCOLAIRE' => [Role::VIE_SCOLAIRE->value];
|
||||
yield 'SECRETARIAT' => [Role::SECRETARIAT->value];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('viewDeniedRolesProvider')]
|
||||
public function itDeniesViewToNonStaffRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, null, [PeriodVoter::VIEW]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function viewDeniedRolesProvider(): iterable
|
||||
{
|
||||
yield 'PARENT' => [Role::PARENT->value];
|
||||
yield 'ELEVE' => [Role::ELEVE->value];
|
||||
}
|
||||
|
||||
// --- CONFIGURE ---
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('configureAllowedRolesProvider')]
|
||||
public function itGrantsConfigureToAdminRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, null, [PeriodVoter::CONFIGURE]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function configureAllowedRolesProvider(): iterable
|
||||
{
|
||||
yield 'SUPER_ADMIN' => [Role::SUPER_ADMIN->value];
|
||||
yield 'ADMIN' => [Role::ADMIN->value];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('configureDeniedRolesProvider')]
|
||||
public function itDeniesConfigureToNonAdminRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, null, [PeriodVoter::CONFIGURE]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function configureDeniedRolesProvider(): 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];
|
||||
}
|
||||
|
||||
private function tokenWithRole(string $role): TokenInterface
|
||||
{
|
||||
$user = $this->createMock(UserInterface::class);
|
||||
$user->method('getRoles')->willReturn([$role]);
|
||||
|
||||
$token = $this->createMock(TokenInterface::class);
|
||||
$token->method('getUser')->willReturn($user);
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Infrastructure\Security;
|
||||
|
||||
use App\Administration\Domain\Model\StudentGuardian\RelationshipType;
|
||||
use App\Administration\Domain\Model\StudentGuardian\StudentGuardian;
|
||||
use App\Administration\Domain\Model\User\UserId;
|
||||
use App\Administration\Infrastructure\Persistence\InMemory\InMemoryStudentGuardianRepository;
|
||||
use App\Administration\Infrastructure\Security\SecurityUser;
|
||||
use App\Administration\Infrastructure\Security\StudentGuardianVoter;
|
||||
use App\Shared\Domain\Tenant\TenantId;
|
||||
use App\Shared\Infrastructure\Tenant\TenantConfig;
|
||||
use App\Shared\Infrastructure\Tenant\TenantContext;
|
||||
use App\Shared\Infrastructure\Tenant\TenantId as InfraTenantId;
|
||||
use DateTimeImmutable;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
final class StudentGuardianVoterTest extends TestCase
|
||||
{
|
||||
private InMemoryStudentGuardianRepository $repository;
|
||||
private TenantContext $tenantContext;
|
||||
private StudentGuardianVoter $voter;
|
||||
private TenantId $tenantId;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->repository = new InMemoryStudentGuardianRepository();
|
||||
$this->tenantContext = new TenantContext();
|
||||
$this->tenantId = TenantId::generate();
|
||||
$this->tenantContext->setCurrentTenant(new TenantConfig(
|
||||
tenantId: InfraTenantId::fromString((string) $this->tenantId),
|
||||
subdomain: 'test',
|
||||
databaseUrl: 'sqlite:///:memory:',
|
||||
));
|
||||
$this->voter = new StudentGuardianVoter($this->repository, $this->tenantContext);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itAbstainsForUnrelatedAttributes(): void
|
||||
{
|
||||
$token = $this->tokenWithSecurityUser('ROLE_ADMIN');
|
||||
|
||||
$result = $this->voter->vote($token, 'some-student-id', ['SOME_OTHER_ATTRIBUTE']);
|
||||
|
||||
self::assertSame(Voter::ACCESS_ABSTAIN, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itAbstainsWhenSubjectIsNotAString(): void
|
||||
{
|
||||
$token = $this->tokenWithSecurityUser('ROLE_ADMIN');
|
||||
|
||||
$result = $this->voter->vote($token, null, [StudentGuardianVoter::VIEW_STUDENT]);
|
||||
|
||||
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, 'some-student-id', [StudentGuardianVoter::VIEW_STUDENT]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itDeniesAccessToNonSecurityUser(): void
|
||||
{
|
||||
$user = $this->createMock(UserInterface::class);
|
||||
$user->method('getRoles')->willReturn(['ROLE_ADMIN']);
|
||||
|
||||
$token = $this->createMock(TokenInterface::class);
|
||||
$token->method('getUser')->willReturn($user);
|
||||
|
||||
$result = $this->voter->vote($token, 'some-student-id', [StudentGuardianVoter::VIEW_STUDENT]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itGrantsViewToSuperAdmin(): void
|
||||
{
|
||||
$result = $this->voteWithRole('ROLE_SUPER_ADMIN');
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itGrantsViewToAdmin(): void
|
||||
{
|
||||
$result = $this->voteWithRole('ROLE_ADMIN');
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itGrantsViewToSecretariat(): void
|
||||
{
|
||||
$result = $this->voteWithRole('ROLE_SECRETARIAT');
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itGrantsViewToProf(): void
|
||||
{
|
||||
$result = $this->voteWithRole('ROLE_PROF');
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itGrantsViewToVieScolaire(): void
|
||||
{
|
||||
$result = $this->voteWithRole('ROLE_VIE_SCOLAIRE');
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itGrantsViewToLinkedParent(): void
|
||||
{
|
||||
$parentId = UserId::generate();
|
||||
$studentId = UserId::generate();
|
||||
|
||||
$link = StudentGuardian::lier(
|
||||
studentId: $studentId,
|
||||
guardianId: $parentId,
|
||||
relationshipType: RelationshipType::MOTHER,
|
||||
tenantId: $this->tenantId,
|
||||
createdAt: new DateTimeImmutable(),
|
||||
);
|
||||
$this->repository->save($link);
|
||||
|
||||
$token = $this->tokenWithSecurityUser('ROLE_PARENT', $parentId);
|
||||
|
||||
$result = $this->voter->vote($token, (string) $studentId, [StudentGuardianVoter::VIEW_STUDENT]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itDeniesViewToUnlinkedParent(): void
|
||||
{
|
||||
$parentId = UserId::generate();
|
||||
$otherStudentId = UserId::generate();
|
||||
|
||||
$token = $this->tokenWithSecurityUser('ROLE_PARENT', $parentId);
|
||||
|
||||
$result = $this->voter->vote($token, (string) $otherStudentId, [StudentGuardianVoter::VIEW_STUDENT]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itDeniesViewToEleve(): void
|
||||
{
|
||||
$result = $this->voteWithRole('ROLE_ELEVE');
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itGrantsViewToEachSeparatedParent(): void
|
||||
{
|
||||
$parent1Id = UserId::generate();
|
||||
$parent2Id = UserId::generate();
|
||||
$studentId = UserId::generate();
|
||||
|
||||
$link1 = StudentGuardian::lier(
|
||||
studentId: $studentId,
|
||||
guardianId: $parent1Id,
|
||||
relationshipType: RelationshipType::FATHER,
|
||||
tenantId: $this->tenantId,
|
||||
createdAt: new DateTimeImmutable(),
|
||||
);
|
||||
$link2 = StudentGuardian::lier(
|
||||
studentId: $studentId,
|
||||
guardianId: $parent2Id,
|
||||
relationshipType: RelationshipType::MOTHER,
|
||||
tenantId: $this->tenantId,
|
||||
createdAt: new DateTimeImmutable(),
|
||||
);
|
||||
$this->repository->save($link1);
|
||||
$this->repository->save($link2);
|
||||
|
||||
$token1 = $this->tokenWithSecurityUser('ROLE_PARENT', $parent1Id);
|
||||
$token2 = $this->tokenWithSecurityUser('ROLE_PARENT', $parent2Id);
|
||||
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $this->voter->vote($token1, (string) $studentId, [StudentGuardianVoter::VIEW_STUDENT]));
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $this->voter->vote($token2, (string) $studentId, [StudentGuardianVoter::VIEW_STUDENT]));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itDeniesParentWhenNoTenantSet(): void
|
||||
{
|
||||
$parentId = UserId::generate();
|
||||
$studentId = UserId::generate();
|
||||
|
||||
$tenantContext = new TenantContext();
|
||||
$voter = new StudentGuardianVoter($this->repository, $tenantContext);
|
||||
|
||||
$token = $this->tokenWithSecurityUser('ROLE_PARENT', $parentId);
|
||||
|
||||
$result = $voter->vote($token, (string) $studentId, [StudentGuardianVoter::VIEW_STUDENT]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itGrantsManageToAdmin(): void
|
||||
{
|
||||
$token = $this->tokenWithSecurityUser('ROLE_ADMIN');
|
||||
|
||||
$result = $this->voter->vote($token, null, [StudentGuardianVoter::MANAGE]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itDeniesManageToParent(): void
|
||||
{
|
||||
$token = $this->tokenWithSecurityUser('ROLE_PARENT');
|
||||
|
||||
$result = $this->voter->vote($token, null, [StudentGuardianVoter::MANAGE]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itDeniesManageToEleve(): void
|
||||
{
|
||||
$token = $this->tokenWithSecurityUser('ROLE_ELEVE');
|
||||
|
||||
$result = $this->voter->vote($token, null, [StudentGuardianVoter::MANAGE]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
private function voteWithRole(string $role): int
|
||||
{
|
||||
$token = $this->tokenWithSecurityUser($role);
|
||||
|
||||
return $this->voter->vote($token, (string) UserId::generate(), [StudentGuardianVoter::VIEW_STUDENT]);
|
||||
}
|
||||
|
||||
private function tokenWithSecurityUser(string $role, ?UserId $userId = null): TokenInterface
|
||||
{
|
||||
$securityUser = new SecurityUser(
|
||||
userId: $userId ?? UserId::generate(),
|
||||
email: 'test@example.com',
|
||||
hashedPassword: 'hashed',
|
||||
tenantId: $this->tenantId,
|
||||
roles: [$role],
|
||||
);
|
||||
|
||||
$token = $this->createMock(TokenInterface::class);
|
||||
$token->method('getUser')->willReturn($securityUser);
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Administration\Infrastructure\Security;
|
||||
|
||||
use App\Administration\Domain\Model\User\Role;
|
||||
use App\Administration\Infrastructure\Api\Resource\SubjectResource;
|
||||
use App\Administration\Infrastructure\Security\SubjectVoter;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
|
||||
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
|
||||
final class SubjectVoterTest extends TestCase
|
||||
{
|
||||
private SubjectVoter $voter;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->voter = new SubjectVoter();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itAbstainsForUnrelatedAttributes(): void
|
||||
{
|
||||
$token = $this->tokenWithRole(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, [SubjectVoter::VIEW]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
// --- VIEW ---
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('viewAllowedRolesProvider')]
|
||||
public function itGrantsViewToStaffRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, null, [SubjectVoter::VIEW]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function viewAllowedRolesProvider(): iterable
|
||||
{
|
||||
yield 'SUPER_ADMIN' => [Role::SUPER_ADMIN->value];
|
||||
yield 'ADMIN' => [Role::ADMIN->value];
|
||||
yield 'PROF' => [Role::PROF->value];
|
||||
yield 'VIE_SCOLAIRE' => [Role::VIE_SCOLAIRE->value];
|
||||
yield 'SECRETARIAT' => [Role::SECRETARIAT->value];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('viewDeniedRolesProvider')]
|
||||
public function itDeniesViewToNonStaffRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, null, [SubjectVoter::VIEW]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function viewDeniedRolesProvider(): iterable
|
||||
{
|
||||
yield 'PARENT' => [Role::PARENT->value];
|
||||
yield 'ELEVE' => [Role::ELEVE->value];
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function itSupportsViewWithSubjectResourceSubject(): void
|
||||
{
|
||||
$token = $this->tokenWithRole(Role::ADMIN->value);
|
||||
$subject = new SubjectResource();
|
||||
|
||||
$result = $this->voter->vote($token, $subject, [SubjectVoter::VIEW]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
// --- CREATE ---
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('adminRolesProvider')]
|
||||
public function itGrantsCreateToAdminRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, null, [SubjectVoter::CREATE]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('nonAdminRolesProvider')]
|
||||
public function itDeniesCreateToNonAdminRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, null, [SubjectVoter::CREATE]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
// --- EDIT ---
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('adminRolesProvider')]
|
||||
public function itGrantsEditToAdminRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, new SubjectResource(), [SubjectVoter::EDIT]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('nonAdminRolesProvider')]
|
||||
public function itDeniesEditToNonAdminRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, new SubjectResource(), [SubjectVoter::EDIT]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
// --- DELETE ---
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('adminRolesProvider')]
|
||||
public function itGrantsDeleteToAdminRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, new SubjectResource(), [SubjectVoter::DELETE]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_GRANTED, $result);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
#[DataProvider('nonAdminRolesProvider')]
|
||||
public function itDeniesDeleteToNonAdminRoles(string $role): void
|
||||
{
|
||||
$token = $this->tokenWithRole($role);
|
||||
|
||||
$result = $this->voter->vote($token, new SubjectResource(), [SubjectVoter::DELETE]);
|
||||
|
||||
self::assertSame(Voter::ACCESS_DENIED, $result);
|
||||
}
|
||||
|
||||
// --- Data Providers ---
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
public static function adminRolesProvider(): iterable
|
||||
{
|
||||
yield 'SUPER_ADMIN' => [Role::SUPER_ADMIN->value];
|
||||
yield 'ADMIN' => [Role::ADMIN->value];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return iterable<string, array{string}>
|
||||
*/
|
||||
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];
|
||||
}
|
||||
|
||||
private function tokenWithRole(string $role): TokenInterface
|
||||
{
|
||||
$user = $this->createMock(UserInterface::class);
|
||||
$user->method('getRoles')->willReturn([$role]);
|
||||
|
||||
$token = $this->createMock(TokenInterface::class);
|
||||
$token->method('getUser')->willReturn($user);
|
||||
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user