corentin_wakdo/tests/Integration/AuthorizerDbTest.php
Imugiii 91b6241096 feat(rbac): autorisation par permission + garde de session cablee (GET /api/me)
Authorizer verifie une PERMISSION via role_permission (RG-T03), rechargee depuis la base a
chaque appel (10.4 RG-3) ; un role desactive ne confere rien. AuthenticatedController (App\Controllers)
cable SessionGuard (RG-6 + RG-T02) et Authorizer sans inverser la dependance du Core. MeController
expose GET /api/me (identite + permissions ; 401 si session absente/expiree/inactive) : premier
consommateur reel du SessionGuard. Tests unitaires + integration DB (auto-skippee sans base) couvrant
le predicat is_active et la liaison par code de permission.
2026-06-15 18:42:09 +00:00

96 lines
3.6 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Integration;
use PHPUnit\Framework\TestCase;
use Throwable;
use App\Auth\Authorizer;
use App\Core\Config;
use App\Core\Database;
/**
* Test d'integration de l'autorisation contre une vraie MariaDB (schema migre +
* seede). Garde anti-regression du predicat de securite `AND r.is_active = 1` et
* du filtrage par code de permission, que le double unitaire ne peut pas prouver.
*
* Auto-skip : ne s'execute que si WAKDO_DB_TESTS=1 ET base joignable.
* Isolation : cree un role jetable (code unique) + une ligne role_permission,
* supprimes en tearDown.
*/
final class AuthorizerDbTest extends TestCase
{
private Database $db;
private int $roleId = 0;
private string $roleCode = '';
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());
}
// Role jetable cree DESACTIVE, portant la permission product.read.
$this->roleCode = 'it-rbac-' . bin2hex(random_bytes(4));
$this->db->execute(
'INSERT INTO role (code, label, is_active) VALUES (:code, :label, 0)',
['code' => $this->roleCode, 'label' => 'IT RBAC'],
);
$this->roleId = (int) ($this->db->fetch('SELECT LAST_INSERT_ID() AS id')['id'] ?? 0);
$this->db->execute(
'INSERT INTO role_permission (role_id, permission_id) '
. 'SELECT :rid, id FROM permission WHERE code = :pc',
['rid' => $this->roleId, 'pc' => 'product.read'],
);
}
protected function tearDown(): void
{
if ($this->roleId === 0) {
return;
}
$this->db->execute('DELETE FROM role_permission WHERE role_id = :id', ['id' => $this->roleId]);
$this->db->execute('DELETE FROM role WHERE id = :id', ['id' => $this->roleId]);
$this->roleId = 0;
}
public function testInactiveRoleGrantsNothingThenActiveGrants(): void
{
$authz = new Authorizer($this->db);
// is_active = 0 : aucun droit ni libelle, malgre la ligne role_permission.
self::assertFalse($authz->can($this->roleId, 'product.read'));
self::assertSame([], $authz->permissionsFor($this->roleId));
self::assertNull($authz->roleCode($this->roleId));
// On active le role : le meme grant devient effectif -> c'est bien le
// predicat is_active qui gate (et non l'absence de role_permission).
$this->db->execute('UPDATE role SET is_active = 1 WHERE id = :id', ['id' => $this->roleId]);
self::assertTrue($authz->can($this->roleId, 'product.read'));
self::assertSame(['product.read'], $authz->permissionsFor($this->roleId));
self::assertSame($this->roleCode, $authz->roleCode($this->roleId));
}
public function testSeededAdminRoleFiltersByPermissionCode(): void
{
$authz = new Authorizer($this->db);
$adminId = (int) ($this->db->fetch("SELECT id FROM role WHERE code = 'admin'")['id'] ?? 0);
self::assertGreaterThan(0, $adminId, 'role admin seede attendu');
// RG-T03 : filtrage par code (admin detient product.create, pas une permission inventee).
self::assertTrue($authz->can($adminId, 'product.create'));
self::assertFalse($authz->can($adminId, 'totally.fake.permission'));
self::assertContains('role.manage', $authz->permissionsFor($adminId));
}
}