test(core) : RBAC #345 - functional coverage voter + last admin guard
This commit is contained in:
195
tests/Module/Core/Api/UserApiTest.php
Normal file
195
tests/Module/Core/Api/UserApiTest.php
Normal file
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Module\Core\Api;
|
||||
|
||||
use App\Module\Core\Domain\Entity\Role;
|
||||
use App\Module\Core\Domain\Entity\User;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
|
||||
/**
|
||||
* Tests fonctionnels de l'exposition API Platform de l'entite User.
|
||||
*
|
||||
* Strategie :
|
||||
* - Les fixtures chargent 3 users : admin (is_admin=true), alice, bob.
|
||||
* - Les tests de lecture s'appuient sur les fixtures sans les modifier.
|
||||
* - Les tests de suppression et de guard "dernier admin" creent des users
|
||||
* additionnels via EntityManager, purges en tearDown.
|
||||
* - On ne supprime JAMAIS les users fixture (admin / alice / bob).
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class UserApiTest extends AbstractApiTestCase
|
||||
{
|
||||
private const TEST_USER_PREFIX = 'test_';
|
||||
private const TEST_ROLE_PREFIX = 'test_';
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
$this->cleanupTestData();
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
// --- Tests lecture collection ---
|
||||
|
||||
public function testListUsersAsAdminReturns200(): void
|
||||
{
|
||||
$client = $this->authenticatedClient('admin', 'admin');
|
||||
$response = $client->request('GET', '/api/users');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
$data = $response->toArray();
|
||||
self::assertArrayHasKey('member', $data);
|
||||
// Au moins 3 users fixture.
|
||||
self::assertGreaterThanOrEqual(3, $data['totalItems']);
|
||||
}
|
||||
|
||||
public function testListUsersAsUserWithViewPermissionReturns200(): void
|
||||
{
|
||||
// Un non-admin portant core.users.view doit pouvoir lister les users.
|
||||
$credentials = $this->createUserWithPermission('core.users.view');
|
||||
$client = $this->authenticatedClient($credentials['username'], $credentials['password']);
|
||||
$client->request('GET', '/api/users');
|
||||
|
||||
self::assertResponseIsSuccessful();
|
||||
}
|
||||
|
||||
public function testListUsersAsStandardUserReturns403(): void
|
||||
{
|
||||
// alice n'a aucune permission RBAC : acces refuse.
|
||||
$client = $this->authenticatedClient('alice', 'alice');
|
||||
$client->request('GET', '/api/users');
|
||||
|
||||
self::assertResponseStatusCodeSame(403);
|
||||
}
|
||||
|
||||
// --- Tests suppression ---
|
||||
|
||||
public function testDeleteNonAdminUserAsAdminReturns204(): void
|
||||
{
|
||||
// Confirme que la suppression d'un user non-admin fonctionne.
|
||||
$em = $this->getEm();
|
||||
|
||||
/** @var UserPasswordHasherInterface $hasher */
|
||||
$hasher = self::getContainer()->get(UserPasswordHasherInterface::class);
|
||||
|
||||
$target = new User();
|
||||
$target->setUsername('test_deletable_user');
|
||||
$target->setIsAdmin(false);
|
||||
$target->setPassword($hasher->hashPassword($target, 'secret'));
|
||||
$em->persist($target);
|
||||
$em->flush();
|
||||
$targetId = $target->getId();
|
||||
$em->clear();
|
||||
|
||||
$client = $this->authenticatedClient('admin', 'admin');
|
||||
$client->request('DELETE', '/api/users/'.$targetId);
|
||||
|
||||
self::assertResponseStatusCodeSame(204);
|
||||
|
||||
// Verification cote base : le user n'existe plus.
|
||||
$em = $this->getEm();
|
||||
$em->clear();
|
||||
self::assertNull($em->getRepository(User::class)->find($targetId));
|
||||
}
|
||||
|
||||
public function testDeleteSecondAdminReturns204(): void
|
||||
{
|
||||
// Quand il y a 2 admins, supprimer le second est autorise (garde non declenchee).
|
||||
$em = $this->getEm();
|
||||
|
||||
/** @var UserPasswordHasherInterface $hasher */
|
||||
$hasher = self::getContainer()->get(UserPasswordHasherInterface::class);
|
||||
|
||||
$secondAdmin = new User();
|
||||
$secondAdmin->setUsername('test_second_admin');
|
||||
$secondAdmin->setIsAdmin(true);
|
||||
$secondAdmin->setPassword($hasher->hashPassword($secondAdmin, 'secret'));
|
||||
$em->persist($secondAdmin);
|
||||
$em->flush();
|
||||
$secondAdminId = $secondAdmin->getId();
|
||||
$em->clear();
|
||||
|
||||
// Auth en tant qu'admin fixture, supprime le second admin.
|
||||
$client = $this->authenticatedClient('admin', 'admin');
|
||||
$client->request('DELETE', '/api/users/'.$secondAdminId);
|
||||
|
||||
self::assertResponseStatusCodeSame(204);
|
||||
|
||||
$em = $this->getEm();
|
||||
$em->clear();
|
||||
self::assertNull($em->getRepository(User::class)->find($secondAdminId));
|
||||
}
|
||||
|
||||
public function testDeleteLastAdminReturns400(): void
|
||||
{
|
||||
// Scenario "dernier admin global" : un seul admin existe (fixture admin).
|
||||
// Il tente de se supprimer lui-meme -> garde activee -> 400.
|
||||
$em = $this->getEm();
|
||||
|
||||
/** @var null|User $fixtureAdmin */
|
||||
$fixtureAdmin = $em->getRepository(User::class)->findOneBy(['username' => 'admin']);
|
||||
self::assertNotNull($fixtureAdmin, 'L\'user admin fixture doit exister.');
|
||||
$fixtureAdminId = $fixtureAdmin->getId();
|
||||
|
||||
// Garantit qu'il n'y a qu'un seul admin au moment du test :
|
||||
// s'assure que test_second_admin n'existe pas (tearDown le purge, mais
|
||||
// soyons defensifs si un test precedent n'a pas nettoye).
|
||||
$em->createQuery(
|
||||
'DELETE FROM '.User::class.' u WHERE u.username LIKE :prefix AND u.username != :admin'
|
||||
)->setParameters(['prefix' => 'test_%', 'admin' => 'admin'])->execute();
|
||||
|
||||
// Auth en tant que l'admin fixture et tente l'auto-suppression.
|
||||
$client = $this->authenticatedClient('admin', 'admin');
|
||||
$response = $client->request('DELETE', '/api/users/'.$fixtureAdminId);
|
||||
|
||||
self::assertResponseStatusCodeSame(400);
|
||||
|
||||
// Verification cote base : l'admin fixture doit toujours exister.
|
||||
$em = $this->getEm();
|
||||
$em->clear();
|
||||
self::assertNotNull(
|
||||
$em->getRepository(User::class)->find($fixtureAdminId),
|
||||
'Le dernier admin ne doit PAS etre supprime.',
|
||||
);
|
||||
}
|
||||
|
||||
public function testDeleteAsStandardUserReturns403(): void
|
||||
{
|
||||
$em = $this->getEm();
|
||||
|
||||
/** @var null|User $alice */
|
||||
$alice = $em->getRepository(User::class)->findOneBy(['username' => 'alice']);
|
||||
self::assertNotNull($alice);
|
||||
|
||||
/** @var null|User $bob */
|
||||
$bob = $em->getRepository(User::class)->findOneBy(['username' => 'bob']);
|
||||
self::assertNotNull($bob);
|
||||
|
||||
// alice sans permission ne peut pas supprimer bob.
|
||||
$client = $this->authenticatedClient('alice', 'alice');
|
||||
$client->request('DELETE', '/api/users/'.$bob->getId());
|
||||
|
||||
self::assertResponseStatusCodeSame(403);
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge les entites de test creees par cette suite.
|
||||
* Ne touche JAMAIS aux fixtures (admin / alice / bob).
|
||||
*/
|
||||
private function cleanupTestData(): void
|
||||
{
|
||||
$em = $this->getEm();
|
||||
|
||||
// Purge des users jetables crees par les tests (y compris testuser_ de createUserWithPermission).
|
||||
$em->createQuery(
|
||||
'DELETE FROM '.User::class.' u WHERE u.username LIKE :prefix'
|
||||
)->setParameter('prefix', self::TEST_USER_PREFIX.'%')->execute();
|
||||
|
||||
// Purge des roles jetables crees par createUserWithPermission.
|
||||
$em->createQuery(
|
||||
'DELETE FROM '.Role::class.' r WHERE r.code LIKE :prefix'
|
||||
)->setParameter('prefix', self::TEST_ROLE_PREFIX.'%')->execute();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user