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.
This commit is contained in:
parent
1b0b20c12d
commit
91b6241096
8 changed files with 545 additions and 0 deletions
75
src/app/Auth/Authorizer.php
Normal file
75
src/app/Auth/Authorizer.php
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Auth;
|
||||
|
||||
use App\Core\DatabaseInterface;
|
||||
|
||||
/**
|
||||
* Verification d'autorisation par PERMISSION (RG-T03), pas par nom de role : on
|
||||
* teste qu'un role detient un code de permission via role_permission. Les droits
|
||||
* sont recharges depuis la base a CHAQUE appel (mlt.md 10.4 RG-3) ; la session ne
|
||||
* porte que role_id, donc un changement RBAC prend effet a la verification suivante
|
||||
* sans invalider les sessions.
|
||||
*
|
||||
* Un role inactif (role.is_active = 0) ne confere aucune permission.
|
||||
*/
|
||||
final class Authorizer
|
||||
{
|
||||
public function __construct(private readonly DatabaseInterface $db)
|
||||
{
|
||||
}
|
||||
|
||||
public function can(int $roleId, string $permissionCode): bool
|
||||
{
|
||||
$row = $this->db->fetch(
|
||||
'SELECT 1 AS granted FROM role_permission rp '
|
||||
. 'JOIN permission p ON p.id = rp.permission_id '
|
||||
. 'JOIN role r ON r.id = rp.role_id '
|
||||
. 'WHERE rp.role_id = :role AND p.code = :code AND r.is_active = 1 LIMIT 1',
|
||||
['role' => $roleId, 'code' => $permissionCode],
|
||||
);
|
||||
|
||||
return $row !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste des codes de permission du role (pour /api/me et l'affichage UI).
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public function permissionsFor(int $roleId): array
|
||||
{
|
||||
$rows = $this->db->fetchAll(
|
||||
'SELECT p.code FROM role_permission rp '
|
||||
. 'JOIN permission p ON p.id = rp.permission_id '
|
||||
. 'JOIN role r ON r.id = rp.role_id '
|
||||
. 'WHERE rp.role_id = :role AND r.is_active = 1 ORDER BY p.code',
|
||||
['role' => $roleId],
|
||||
);
|
||||
|
||||
$codes = [];
|
||||
foreach ($rows as $row) {
|
||||
$code = $row['code'] ?? null;
|
||||
if (is_string($code)) {
|
||||
$codes[] = $code;
|
||||
}
|
||||
}
|
||||
|
||||
return $codes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Code du role (ex. 'admin', 'counter'). Lecture de metadonnee de role,
|
||||
* regroupee ici avec l'acces a role_permission pour un seul seam de donnees.
|
||||
*/
|
||||
public function roleCode(int $roleId): ?string
|
||||
{
|
||||
// Filtre is_active comme can()/permissionsFor() : un role desactive ne
|
||||
// doit exposer ni droits ni libelle exploitable (coherence de l'invariant).
|
||||
$row = $this->db->fetch('SELECT r.code FROM role r WHERE r.id = :id AND r.is_active = 1', ['id' => $roleId]);
|
||||
|
||||
return is_string($row['code'] ?? null) ? $row['code'] : null;
|
||||
}
|
||||
}
|
||||
36
src/app/Controllers/AuthenticatedController.php
Normal file
36
src/app/Controllers/AuthenticatedController.php
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Auth\Authorizer;
|
||||
use App\Auth\SessionGuard;
|
||||
use App\Auth\SessionManager;
|
||||
use App\Core\Controller;
|
||||
|
||||
/**
|
||||
* Base des controleurs proteges : fournit la session, la garde de session
|
||||
* (RG-6 + RG-T02) et le service d'autorisation (RG-T03), cables depuis la Config
|
||||
* et la Database injectees. Vit dans App\Controllers (au-dessus de Core et Auth)
|
||||
* pour ne pas inverser la dependance du Core vers Auth.
|
||||
*
|
||||
* Les hooks sont proteges et surchargeables en test pour injecter des doubles.
|
||||
*/
|
||||
abstract class AuthenticatedController extends Controller
|
||||
{
|
||||
protected function sessionManager(): SessionManager
|
||||
{
|
||||
return new SessionManager($this->config);
|
||||
}
|
||||
|
||||
protected function sessionGuard(): SessionGuard
|
||||
{
|
||||
return new SessionGuard($this->sessionManager(), $this->database, $this->config);
|
||||
}
|
||||
|
||||
protected function authorizer(): Authorizer
|
||||
{
|
||||
return new Authorizer($this->database);
|
||||
}
|
||||
}
|
||||
50
src/app/Controllers/MeController.php
Normal file
50
src/app/Controllers/MeController.php
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controllers;
|
||||
|
||||
use App\Core\Response;
|
||||
|
||||
/**
|
||||
* GET /api/me : identite et permissions de l'utilisateur de la session courante.
|
||||
*
|
||||
* Premier consommateur reel de SessionGuard (RG-6 + RG-T02) et d'Authorizer
|
||||
* (RG-T03) : la garde rejette toute session absente/expiree/inactive (401), sinon
|
||||
* on renvoie le role et la liste des permissions rechargee depuis la base.
|
||||
*
|
||||
* NB P2 : les pages admin sont encore des fichiers statiques servis par Apache
|
||||
* (hors PHP), donc non couvertes par cette garde. Leur protection arrive en P3
|
||||
* quand elles deviennent des vues rendues serveur passant par ce socle.
|
||||
*
|
||||
* Non `final` a dessein : les tests sous-classent pour injecter des doubles
|
||||
* (session en mode test + FakeDatabase) via les hooks proteges.
|
||||
*/
|
||||
class MeController extends AuthenticatedController
|
||||
{
|
||||
/**
|
||||
* @param array<string, string> $params
|
||||
*/
|
||||
public function show(array $params = []): Response
|
||||
{
|
||||
$guard = $this->sessionGuard()->check();
|
||||
|
||||
if (!$guard->authenticated || $guard->userId === null || $guard->roleId === null) {
|
||||
return $this->json(
|
||||
['data' => null, 'error' => ['code' => 'AUTH_REQUIRED', 'message' => 'Authentification requise']],
|
||||
401,
|
||||
);
|
||||
}
|
||||
|
||||
$authorizer = $this->authorizer();
|
||||
|
||||
return $this->json([
|
||||
'data' => [
|
||||
'user_id' => $guard->userId,
|
||||
'role_id' => $guard->roleId,
|
||||
'role_code' => $authorizer->roleCode($guard->roleId),
|
||||
'permissions' => $authorizer->permissionsFor($guard->roleId),
|
||||
],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
@ -14,6 +14,7 @@ use App\Auth\SessionManager;
|
|||
use App\Controllers\AuthController;
|
||||
use App\Controllers\HealthController;
|
||||
use App\Controllers\HomeController;
|
||||
use App\Controllers\MeController;
|
||||
use App\Controllers\PasswordResetController;
|
||||
use App\Core\Autoloader;
|
||||
use App\Core\Config;
|
||||
|
|
@ -57,6 +58,9 @@ try {
|
|||
$router->add('GET', '/reset_password', [PasswordResetController::class, 'showConfirm']);
|
||||
$router->add('POST', '/reset_password', [PasswordResetController::class, 'submitConfirm']);
|
||||
|
||||
// RBAC : identite + permissions de la session courante (gardee par SessionGuard).
|
||||
$router->add('GET', '/api/me', [MeController::class, 'show']);
|
||||
|
||||
$response = $router->dispatch(Request::fromGlobals());
|
||||
$response->send();
|
||||
} catch (Throwable $exception) {
|
||||
|
|
|
|||
96
tests/Integration/AuthorizerDbTest.php
Normal file
96
tests/Integration/AuthorizerDbTest.php
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
<?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));
|
||||
}
|
||||
}
|
||||
|
|
@ -55,6 +55,34 @@ final class FakeDatabase implements DatabaseInterface
|
|||
*/
|
||||
public ?array $guardUserRow = null;
|
||||
|
||||
/** Resultat de Authorizer::can() (true = permission accordee). */
|
||||
public bool $canResult = false;
|
||||
|
||||
/** Etat role.is_active modelise pour can()/permissionsFor() ; false => rien accorde. */
|
||||
public bool $roleActive = true;
|
||||
|
||||
/**
|
||||
* Trace des lectures (fetch/fetchAll) pour asserter les parametres lies
|
||||
* (ex. liaison par code de permission, RG-T03), pendant que $writes trace les ecritures.
|
||||
*
|
||||
* @var list<array{sql: string, params: array<string|int, mixed>}>
|
||||
*/
|
||||
public array $reads = [];
|
||||
|
||||
/**
|
||||
* Codes de permission renvoyes par Authorizer::permissionsFor().
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
public array $permissionCodes = [];
|
||||
|
||||
/**
|
||||
* Ligne role renvoyee pour la lecture du code de role (/api/me) ; null = absent.
|
||||
*
|
||||
* @var array<string, mixed>|null
|
||||
*/
|
||||
public ?array $roleRow = null;
|
||||
|
||||
/** Si non nul, execute() leve cette exception (simulation panne DB -> fail-closed). */
|
||||
public ?RuntimeException $failOnExecute = null;
|
||||
|
||||
|
|
@ -66,6 +94,8 @@ final class FakeDatabase implements DatabaseInterface
|
|||
|
||||
public function fetch(string $sql, array $params = []): ?array
|
||||
{
|
||||
$this->reads[] = ['sql' => $sql, 'params' => $params];
|
||||
|
||||
if (str_contains($sql, 'FROM user u JOIN role')) {
|
||||
return $this->userRow;
|
||||
}
|
||||
|
|
@ -82,6 +112,14 @@ final class FakeDatabase implements DatabaseInterface
|
|||
return $this->guardUserRow;
|
||||
}
|
||||
|
||||
if (str_contains($sql, 'SELECT 1 AS granted FROM role_permission')) {
|
||||
return ($this->canResult && $this->roleActive) ? ['granted' => 1] : null;
|
||||
}
|
||||
|
||||
if (str_contains($sql, 'FROM role r WHERE r.id')) {
|
||||
return $this->roleRow;
|
||||
}
|
||||
|
||||
if (str_contains($sql, 'SELECT lockout_until FROM login_throttle')) {
|
||||
return ['lockout_until' => $this->ipLockoutUntil];
|
||||
}
|
||||
|
|
@ -95,6 +133,16 @@ final class FakeDatabase implements DatabaseInterface
|
|||
|
||||
public function fetchAll(string $sql, array $params = []): array
|
||||
{
|
||||
$this->reads[] = ['sql' => $sql, 'params' => $params];
|
||||
|
||||
if (str_contains($sql, 'SELECT p.code FROM role_permission')) {
|
||||
if (!$this->roleActive) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return array_map(static fn (string $code): array => ['code' => $code], $this->permissionCodes);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
|
|||
100
tests/Unit/Auth/AuthorizerTest.php
Normal file
100
tests/Unit/Auth/AuthorizerTest.php
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Auth;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use App\Auth\Authorizer;
|
||||
use App\Tests\Support\FakeDatabase;
|
||||
|
||||
/**
|
||||
* Verification par permission (RG-T03) : can() / permissionsFor() / roleCode()
|
||||
* testes avec un FakeDatabase, sans base reelle.
|
||||
*/
|
||||
final class AuthorizerTest extends TestCase
|
||||
{
|
||||
private FakeDatabase $db;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->db = new FakeDatabase();
|
||||
}
|
||||
|
||||
private function authorizer(): Authorizer
|
||||
{
|
||||
return new Authorizer($this->db);
|
||||
}
|
||||
|
||||
public function testCanReturnsTrueWhenPermissionGranted(): void
|
||||
{
|
||||
$this->db->canResult = true;
|
||||
|
||||
self::assertTrue($this->authorizer()->can(1, 'product.create'));
|
||||
// RG-T03 : la verification lie le CODE de permission + le role_id (jamais
|
||||
// un nom de role). On asserte les parametres reellement lies a la requete.
|
||||
self::assertSame(['role' => 1, 'code' => 'product.create'], $this->lastRead()['params']);
|
||||
}
|
||||
|
||||
public function testCanReturnsFalseWhenNotGranted(): void
|
||||
{
|
||||
$this->db->canResult = false;
|
||||
|
||||
self::assertFalse($this->authorizer()->can(3, 'order.cancel'));
|
||||
}
|
||||
|
||||
public function testPermissionsForReturnsCodes(): void
|
||||
{
|
||||
$this->db->permissionCodes = ['order.read', 'product.read', 'stock.read'];
|
||||
|
||||
self::assertSame(
|
||||
['order.read', 'product.read', 'stock.read'],
|
||||
$this->authorizer()->permissionsFor(4),
|
||||
);
|
||||
self::assertSame(['role' => 4], $this->lastRead()['params']);
|
||||
}
|
||||
|
||||
public function testPermissionsForReturnsEmptyWhenNone(): void
|
||||
{
|
||||
$this->db->permissionCodes = [];
|
||||
|
||||
self::assertSame([], $this->authorizer()->permissionsFor(9));
|
||||
}
|
||||
|
||||
public function testRoleCodeReturnsCodeOrNull(): void
|
||||
{
|
||||
$this->db->roleRow = ['code' => 'admin'];
|
||||
self::assertSame('admin', $this->authorizer()->roleCode(1));
|
||||
|
||||
$this->db->roleRow = null;
|
||||
self::assertNull($this->authorizer()->roleCode(999));
|
||||
}
|
||||
|
||||
public function testCanDeniesWhenRoleInactive(): void
|
||||
{
|
||||
// Le role detient la permission (canResult) mais il est desactive : refus.
|
||||
$this->db->canResult = true;
|
||||
$this->db->roleActive = false;
|
||||
|
||||
self::assertFalse($this->authorizer()->can(1, 'product.create'));
|
||||
}
|
||||
|
||||
public function testPermissionsForEmptyWhenRoleInactive(): void
|
||||
{
|
||||
$this->db->permissionCodes = ['order.read', 'product.read'];
|
||||
$this->db->roleActive = false;
|
||||
|
||||
self::assertSame([], $this->authorizer()->permissionsFor(4));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{sql: string, params: array<string|int, mixed>}
|
||||
*/
|
||||
private function lastRead(): array
|
||||
{
|
||||
$reads = $this->db->reads;
|
||||
self::assertNotEmpty($reads, 'aucune lecture enregistree');
|
||||
|
||||
return $reads[array_key_last($reads)];
|
||||
}
|
||||
}
|
||||
136
tests/Unit/Auth/MeControllerTest.php
Normal file
136
tests/Unit/Auth/MeControllerTest.php
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Unit\Auth;
|
||||
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use App\Auth\Authorizer;
|
||||
use App\Auth\SessionGuard;
|
||||
use App\Auth\SessionManager;
|
||||
use App\Controllers\MeController;
|
||||
use App\Core\Config;
|
||||
use App\Core\Database;
|
||||
use App\Core\Request;
|
||||
use App\Tests\Support\FakeDatabase;
|
||||
|
||||
/**
|
||||
* Sous-classe de test : injecte une session en mode test et un FakeDatabase dans
|
||||
* la garde et l'autorisation, sans base reelle.
|
||||
*/
|
||||
final class TestMeController extends MeController
|
||||
{
|
||||
public function __construct(
|
||||
Request $request,
|
||||
Config $config,
|
||||
Database $database,
|
||||
private readonly SessionManager $testSession,
|
||||
private readonly FakeDatabase $fakeDb,
|
||||
) {
|
||||
parent::__construct($request, $config, $database);
|
||||
}
|
||||
|
||||
protected function sessionManager(): SessionManager
|
||||
{
|
||||
return $this->testSession;
|
||||
}
|
||||
|
||||
protected function sessionGuard(): SessionGuard
|
||||
{
|
||||
return new SessionGuard($this->testSession, $this->fakeDb, $this->config);
|
||||
}
|
||||
|
||||
protected function authorizer(): Authorizer
|
||||
{
|
||||
return new Authorizer($this->fakeDb);
|
||||
}
|
||||
}
|
||||
|
||||
final class MeControllerTest extends TestCase
|
||||
{
|
||||
/** @var list<string> */
|
||||
private array $touchedKeys = [];
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->setEnv('SESSION_LIFETIME_IDLE', '14400');
|
||||
$this->setEnv('SESSION_LIFETIME_ABSOLUTE', '36000');
|
||||
}
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
foreach ($this->touchedKeys as $key) {
|
||||
putenv($key);
|
||||
}
|
||||
$this->touchedKeys = [];
|
||||
}
|
||||
|
||||
private function setEnv(string $key, string $value): void
|
||||
{
|
||||
$this->touchedKeys[] = $key;
|
||||
putenv($key . '=' . $value);
|
||||
}
|
||||
|
||||
private function controller(SessionManager $session, FakeDatabase $db): TestMeController
|
||||
{
|
||||
$request = new Request('GET', '/api/me', [], [], '', '203.0.113.5');
|
||||
|
||||
return new TestMeController($request, new Config(), new Database(new Config()), $session, $db);
|
||||
}
|
||||
|
||||
public function testNoSessionReturns401(): void
|
||||
{
|
||||
$response = $this->controller(new SessionManager(new Config(), true), new FakeDatabase())->show();
|
||||
|
||||
self::assertSame(401, $response->status());
|
||||
|
||||
$body = json_decode($response->body(), true);
|
||||
self::assertIsArray($body);
|
||||
self::assertSame('AUTH_REQUIRED', $body['error']['code'] ?? null);
|
||||
}
|
||||
|
||||
public function testAuthenticatedReturnsIdentityAndPermissions(): void
|
||||
{
|
||||
$session = new SessionManager(new Config(), true);
|
||||
// Horodatages relatifs a l'instant reel : MeController appelle check() sans
|
||||
// injecter de temps (now = time()).
|
||||
$now = time();
|
||||
$session->set('user_id', 7);
|
||||
$session->set('role_id', 3);
|
||||
$session->set('logged_in_at', $now - 100);
|
||||
$session->set('last_activity', $now - 50);
|
||||
|
||||
$db = new FakeDatabase();
|
||||
$db->guardUserRow = ['is_active' => 1];
|
||||
$db->roleRow = ['code' => 'manager'];
|
||||
$db->permissionCodes = ['product.read', 'stats.read'];
|
||||
|
||||
$response = $this->controller($session, $db)->show();
|
||||
|
||||
self::assertSame(200, $response->status());
|
||||
|
||||
$body = json_decode($response->body(), true);
|
||||
self::assertIsArray($body);
|
||||
self::assertSame(7, $body['data']['user_id'] ?? null);
|
||||
self::assertSame(3, $body['data']['role_id'] ?? null);
|
||||
self::assertSame('manager', $body['data']['role_code'] ?? null);
|
||||
self::assertSame(['product.read', 'stats.read'], $body['data']['permissions'] ?? null);
|
||||
}
|
||||
|
||||
public function testInactiveUserSessionReturns401(): void
|
||||
{
|
||||
$session = new SessionManager(new Config(), true);
|
||||
$now = time();
|
||||
$session->set('user_id', 7);
|
||||
$session->set('role_id', 3);
|
||||
$session->set('logged_in_at', $now - 100);
|
||||
$session->set('last_activity', $now - 50);
|
||||
|
||||
$db = new FakeDatabase();
|
||||
$db->guardUserRow = ['is_active' => 0];
|
||||
|
||||
$response = $this->controller($session, $db)->show();
|
||||
|
||||
self::assertSame(401, $response->status());
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue