corentin_wakdo/tests/Integration/RoleRepositoryDbTest.php
Imugiii de48ddf7cd
All checks were successful
CI / secret-scan (pull_request) Successful in 14s
CI / js-tests (pull_request) Successful in 28s
CI / php-lint (pull_request) Successful in 26s
CI / static-tests (pull_request) Successful in 1m1s
CI / secret-scan (push) Successful in 12s
CI / php-lint (push) Successful in 25s
CI / static-tests (push) Successful in 50s
CI / js-tests (push) Successful in 22s
CI / auto-merge (pull_request) Successful in 4s
CI / auto-merge (push) Has been skipped
feat(admin): RBAC - matrice roles/permissions + roles custom (PIN+audit diff) (P3)
Lot R du cycle P3 (Users/RBAC/Stats), dernier lot. Gestion RBAC (mlt 10.4
MANAGE_RBAC, permission role.manage) : matrice roles x permissions + roles
personnalises (RG-4). Action a fort impact (escalade de privileges) -> PIN
equipier + audit_log dans la meme transaction (RG-T13/14), throttle PIN (RG-T22).

- RoleRepository (App\Auth) : roles (CRUD, code immuable), matrice (permissionIds/
  CodesFor, setPermissions tx + variante raw replacePermissions pour enrobage
  controleur), sources visibles (role_visible_source, tx + raw). Catalogue de
  permissions fige (lecture seule).
- RoleController (role.manage) : index ; create/store (role custom : code+label+
  default_route+order_source) ; edit/update (champs role + matrice + sources, en
  UNE transaction). audit role.manage avec details=DIFF des codes de permission
  (ajoutes/retires, RG-6), calcule avant la reecriture delete-and-reinsert.
- Matrice soumise en champs SCALAIRES (perm_<id>, source_<enum>) : Request::formBody
  ne garde que les scalaires, donc pas de name[] ni de JS.
- Garde-fous anti-lockout : le role admin conserve role.manage ET reste actif ;
  code immuable apres creation ; order_source borne a l'ENUM ; code dupli -> 409.
- Vues admin/roles/{index,form}, 5 routes, nav Roles (gated role.manage).

Tests : unit 263, integration 301 / 916 assertions (WAKDO_DB_TESTS=1, dont
RoleControllerTest 12 + RoleRepositoryDbTest 4), PHPStan L6 propre.
2026-06-17 12:23:46 +00:00

138 lines
4.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Integration;
use PDOException;
use PHPUnit\Framework\TestCase;
use Throwable;
use App\Auth\RoleRepository;
use App\Core\Config;
use App\Core\Database;
/**
* RoleRepository (RBAC, mlt 10.4) contre une vraie MariaDB (schema migre + seede).
* Auto-skip si WAKDO_DB_TESTS != 1. Role jetable (code it-role-*) ; CASCADE retire
* role_permission + role_visible_source a la suppression du role (teardown simple).
*/
final class RoleRepositoryDbTest extends TestCase
{
private Database $db;
private string $code = '';
private int $permA = 0;
private int $permB = 0;
protected function setUp(): void
{
if (getenv('WAKDO_DB_TESTS') !== '1') {
self::markTestSkipped('Tests DB desactives (definir WAKDO_DB_TESTS=1 + DB_*).');
}
$this->db = new Database(new Config());
try {
$this->db->fetch('SELECT 1');
} catch (Throwable $exception) {
self::markTestSkipped('Base injoignable: ' . $exception->getMessage());
}
$this->code = 'it-role-' . bin2hex(random_bytes(4));
$this->permA = (int) ($this->db->fetch("SELECT id FROM permission WHERE code = 'stats.read'")['id'] ?? 0);
$this->permB = (int) ($this->db->fetch("SELECT id FROM permission WHERE code = 'user.read'")['id'] ?? 0);
}
protected function tearDown(): void
{
if ($this->code !== '') {
$this->db->execute('DELETE FROM role WHERE code = :c', ['c' => $this->code]); // CASCADE perms + sources
}
}
private function makeRole(RoleRepository $repo): int
{
return $repo->createRole([
'code' => $this->code,
'label' => 'IT Role',
'description' => 'jetable',
'default_route' => '/admin/dashboard',
'order_source' => null,
]);
}
public function testCreateRoleAndCodeUnique(): void
{
$repo = new RoleRepository($this->db);
$id = $this->makeRole($repo);
self::assertGreaterThan(0, $id);
$found = $repo->findRole($id);
self::assertNotNull($found);
self::assertSame($this->code, (string) $found['code']);
self::assertTrue($repo->codeExists($this->code));
self::assertFalse($repo->codeExists($this->code, $id)); // s'exclut lui-meme
$violated = false;
try {
$repo->createRole(['code' => $this->code, 'label' => 'Dup', 'description' => null, 'default_route' => null, 'order_source' => null]);
} catch (PDOException $exception) {
$violated = (string) $exception->getCode() === '23000';
}
self::assertTrue($violated, 'uk_role_code doit rejeter un doublon.');
}
public function testSetPermissionsReplacesAndExposesCodes(): void
{
$repo = new RoleRepository($this->db);
$id = $this->makeRole($repo);
$repo->setPermissions($id, [$this->permA, $this->permB]);
$ids = $repo->permissionIdsFor($id);
sort($ids);
$expected = [$this->permA, $this->permB];
sort($expected);
self::assertSame($expected, $ids);
$codes = $repo->permissionCodesFor($id);
self::assertContains('stats.read', $codes);
self::assertContains('user.read', $codes);
// Delete-and-reinsert : la nouvelle selection REMPLACE l'ancienne.
$repo->setPermissions($id, [$this->permA]);
self::assertSame([$this->permA], $repo->permissionIdsFor($id));
// 23 permissions au catalogue (fige au seed).
self::assertCount(23, $repo->allPermissions());
}
public function testSetVisibleSourcesReplaces(): void
{
$repo = new RoleRepository($this->db);
$id = $this->makeRole($repo);
$repo->setVisibleSources($id, ['counter', 'drive']);
$sources = $repo->visibleSources($id);
sort($sources);
self::assertSame(['counter', 'drive'], $sources);
$repo->setVisibleSources($id, ['kiosk']);
self::assertSame(['kiosk'], $repo->visibleSources($id));
}
public function testUpdateRoleKeepsCodeImmutable(): void
{
$repo = new RoleRepository($this->db);
$id = $this->makeRole($repo);
$repo->updateRole($id, [
'label' => 'Relabelled',
'description' => 'maj',
'default_route' => '/admin/stats',
'order_source' => 'counter',
'is_active' => 1,
]);
$updated = $repo->findRole($id);
self::assertNotNull($updated);
self::assertSame('Relabelled', (string) $updated['label']);
self::assertSame('/admin/stats', (string) $updated['default_route']);
self::assertSame('counter', (string) $updated['order_source']);
self::assertSame($this->code, (string) $updated['code']); // code inchange (immuable)
}
}