corentin_wakdo/tests/Unit/Admin/DashboardControllerTest.php
Imugiii ca64e5037f
All checks were successful
CI / php-lint (push) Successful in 20s
CI / secret-scan (pull_request) Successful in 8s
CI / static-tests (pull_request) Successful in 33s
CI / auto-merge (push) Has been skipped
CI / auto-merge (pull_request) Successful in 4s
CI / secret-scan (push) Successful in 8s
CI / static-tests (push) Successful in 35s
CI / php-lint (pull_request) Successful in 19s
fix(admin): retire les liens de nav vers des pages non construites
Le shell admin (layout.php) exposait, conditionnes par permission, 4 liens de
navigation vers des routes inexistantes : /admin/menus, /admin/orders,
/admin/users, /admin/roles. Un clic renvoyait un 404 JSON brut (le seed admin
detient les 4 permissions, donc l'admin voyait les 4 liens morts).

Liens retires tant que leur page n'existe pas ; les sections Operations et
Administration disparaissent (leurs seuls items etaient morts), Menus quitte la
section Catalogue. Un commentaire documente comment/quand les reactiver (avec
leur route, P3 suite / P4). Dashboard / Categories / Produits (pages reelles)
restent, la demo RBAC reste portee par le gating de Produits.

Test : DashboardControllerTest assertait le rendu du lien /admin/users (mort) ;
mis a jour en assertStringNotContainsString. Suite verte (188, 13 DB skip).
2026-06-16 11:46:24 +00:00

211 lines
7.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Admin;
use PHPUnit\Framework\TestCase;
use App\Auth\Authorizer;
use App\Auth\GuardResult;
use App\Auth\SessionGuard;
use App\Auth\SessionManager;
use App\Auth\UserDirectory;
use App\Controllers\DashboardController;
use App\Core\Config;
use App\Core\Database;
use App\Core\Request;
use App\Core\Response;
use App\Tests\Support\FakeDatabase;
/**
* Sous-classe de test : injecte session test + FakeDatabase dans la garde,
* l'autorisation et l'annuaire, sans base reelle.
*/
final class TestDashboardController extends DashboardController
{
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);
}
protected function userDirectory(): UserDirectory
{
return new UserDirectory($this->fakeDb);
}
/**
* Expose le chemin garde par permission d'AdminController::guard() (RG-T03),
* que le dashboard (auth seule) n'exerce pas.
*/
public function gated(): GuardResult|Response
{
$guard = $this->guard('user.read');
if ($guard instanceof Response) {
return $guard;
}
return $this->adminView('admin/dashboard', ['title' => 't', 'activeNav' => ''], $guard);
}
}
final class DashboardControllerTest 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): TestDashboardController
{
$request = new Request('GET', '/admin/dashboard', [], [], '', '203.0.113.5');
return new TestDashboardController($request, new Config(), new Database(new Config()), $session, $db);
}
private function authedSession(): SessionManager
{
$session = new SessionManager(new Config(), true);
$now = time();
$session->set('user_id', 1);
$session->set('role_id', 1);
$session->set('logged_in_at', $now - 100);
$session->set('last_activity', $now - 50);
return $session;
}
public function testRedirectsToLoginWithoutSession(): void
{
$response = $this->controller(new SessionManager(new Config(), true), new FakeDatabase())->index();
self::assertSame(302, $response->status());
self::assertSame('/login', $response->header('Location'));
}
public function testInactiveUserRedirectsToLogin(): void
{
$db = new FakeDatabase();
$db->guardUserRow = ['is_active' => 0];
$response = $this->controller($this->authedSession(), $db)->index();
self::assertSame(302, $response->status());
self::assertSame('/login', $response->header('Location'));
}
public function testRendersShellWhenAuthenticated(): void
{
$db = new FakeDatabase();
$db->guardUserRow = ['is_active' => 1];
$db->userDisplayRow = ['first_name' => 'Corentin', 'last_name' => 'J', 'role_label' => 'Administrateur'];
$db->permissionCodes = ['product.read', 'user.read'];
$response = $this->controller($this->authedSession(), $db)->index();
self::assertSame(200, $response->status());
$body = $response->body();
// Shell rendu (topbar/sidebar) + identite + page.
self::assertStringContainsString('admin-layout', $body);
self::assertStringContainsString('Tableau de bord', $body);
self::assertStringContainsString('Corentin J', $body);
self::assertStringContainsString('Administrateur', $body);
// Marqueur present UNIQUEMENT dans le fragment dashboard (absent du layout) :
// verifie que le contenu est bien compose DANS le shell (pas un $content vide).
self::assertStringContainsString('Bienvenue, Corentin J', $body);
// Navigation conditionnee aux permissions : un lien n'apparait que si la
// permission est presente ET la page existe.
self::assertStringContainsString('/admin/products', $body); // product.read present + page existante
// user.read est present, mais la page /admin/users n'existe pas encore :
// le lien est retire pour ne pas exposer un 404 (cf. layout.php).
self::assertStringNotContainsString('/admin/users', $body);
self::assertStringNotContainsString('/admin/roles', $body); // pas de page + role.manage absent
// Deconnexion = formulaire POST avec CSRF.
self::assertStringContainsString('action="/logout"', $body);
self::assertStringContainsString('name="_csrf"', $body);
}
public function testForbiddenWhenPermissionDenied(): void
{
// Authentifie mais sans la permission requise (RG-T03) -> 403 + page forbidden.
$db = new FakeDatabase();
$db->guardUserRow = ['is_active' => 1];
$db->userDisplayRow = ['first_name' => 'Corentin', 'last_name' => 'J', 'role_label' => 'Equipier'];
$db->canResult = false;
$response = $this->controller($this->authedSession(), $db)->gated();
self::assertSame(403, $response->status());
self::assertStringContainsString('Acces refuse', $response->body());
}
public function testGatedPageRendersWhenPermitted(): void
{
$db = new FakeDatabase();
$db->guardUserRow = ['is_active' => 1];
$db->userDisplayRow = ['first_name' => 'Corentin', 'last_name' => 'J', 'role_label' => 'Administrateur'];
$db->canResult = true;
$db->permissionCodes = ['user.read'];
$response = $this->controller($this->authedSession(), $db)->gated();
self::assertSame(200, $response->status());
}
public function testEscapesUserIdentity(): void
{
// Donnees user-editables (nom/role) : doivent etre echappees (RG-T15).
$db = new FakeDatabase();
$db->guardUserRow = ['is_active' => 1];
$db->userDisplayRow = [
'first_name' => '<script>alert(1)</script>',
'last_name' => 'x',
'role_label' => 'Admin <b>& co</b>',
];
$db->permissionCodes = ['user.read'];
$body = $this->controller($this->authedSession(), $db)->index()->body();
self::assertStringContainsString('&lt;script&gt;', $body);
self::assertStringNotContainsString('<script>alert(1)</script>', $body);
self::assertStringContainsString('&amp; co', $body);
self::assertStringNotContainsString('Admin <b>', $body);
}
}