corentin_wakdo/tests/Unit/Admin/StatsControllerTest.php
Imugiii fe07e06ee1
All checks were successful
CI / secret-scan (pull_request) Successful in 8s
CI / php-lint (pull_request) Successful in 22s
CI / static-tests (pull_request) Successful in 44s
CI / js-tests (pull_request) Successful in 27s
CI / secret-scan (push) Successful in 11s
CI / php-lint (push) Successful in 28s
CI / static-tests (push) Successful in 50s
CI / js-tests (push) Successful in 24s
CI / auto-merge (pull_request) Successful in 4s
CI / auto-merge (push) Has been skipped
feat(admin): tableau de bord statistiques (catalogue + sante stock RG-T21) (P3)
Lot S du cycle P3 (Users/RBAC/Stats). Tableau de bord stats.read sur /admin/stats,
landing par defaut du role manager (ferme le 404 : la route existe enfin).

- StatsRepository : counts() (compteurs catalogue produits/menus/categories/
  ingredients, total + actifs/disponibles via SUM(bool)) ; stockHealth()
  (repartition des ingredients actifs par bande normal/low/critical, liste
  d'alerte triee du plus critique, reutilise IngredientRepository::stockBand =
  source unique de la derivation RG-T21).
- StatsController (stats.read) + vue admin/stats/index (cartes KPI + table
  d'alerte stock) + lien nav Pilotage (gated stats.read) + route.
- Les KPIs de vente (CA, volumes) dependent du domaine commande et sont
  explicitement differes en P4.

Tests : unit 233, integration 263 / 794 assertions (WAKDO_DB_TESTS=1), PHPStan L6.
2026-06-17 10:36:32 +00:00

144 lines
4.2 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Tests\Unit\Admin;
use PHPUnit\Framework\TestCase;
use App\Auth\SessionManager;
use App\Catalogue\StatsRepository;
use App\Controllers\StatsController;
use App\Core\Config;
use App\Core\Database;
use App\Core\DatabaseInterface;
use App\Core\Request;
use App\Tests\Support\FakeDatabase;
/**
* Stub de StatsRepository : KPIs canned, sans base. Permet de tester le rendu du
* controleur independamment des requetes d'agregation (couvertes par le DbTest).
*/
final class StubStatsRepository extends StatsRepository
{
public function counts(): array
{
return [
'products' => ['total' => 53, 'available' => 50],
'categories' => ['total' => 9, 'active' => 9],
'menus' => ['total' => 13, 'available' => 12],
'ingredients' => ['total' => 7, 'active' => 6],
];
}
public function stockHealth(): array
{
return [
'active_total' => 6,
'bands' => ['normal' => 4, 'low' => 1, 'critical' => 1],
'alerts' => [
['name' => 'Cheddar', 'stock_pct' => 3, 'stock_band' => 'critical'],
['name' => 'Cornichon', 'stock_pct' => 8, 'stock_band' => 'low'],
],
];
}
}
final class TestStatsController extends StatsController
{
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 db(): DatabaseInterface
{
return $this->fakeDb;
}
protected function statsRepository(): StatsRepository
{
return new StubStatsRepository($this->fakeDb);
}
}
final class StatsControllerTest extends TestCase
{
/** @var list<string> */
private array $touchedKeys = [];
private SessionManager $session;
protected function setUp(): void
{
$this->setEnv('SESSION_LIFETIME_IDLE', '14400');
$this->setEnv('SESSION_LIFETIME_ABSOLUTE', '36000');
$this->session = new SessionManager(new Config(), true);
$now = time();
$this->session->set('user_id', 1);
$this->session->set('role_id', 2);
$this->session->set('logged_in_at', $now - 100);
$this->session->set('last_activity', $now - 50);
}
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 permittedDb(): FakeDatabase
{
$db = new FakeDatabase();
$db->guardUserRow = ['is_active' => 1];
$db->userDisplayRow = ['first_name' => 'Manon', 'last_name' => 'G', 'role_label' => 'Manager'];
$db->canResult = true;
$db->permissionCodes = ['stats.read'];
return $db;
}
private function controller(FakeDatabase $db): TestStatsController
{
$request = new Request('GET', '/admin/stats', [], [], '', '203.0.113.5');
return new TestStatsController($request, new Config(), new Database(new Config()), $this->session, $db);
}
public function testRequiresStatsRead(): void
{
$db = $this->permittedDb();
$db->canResult = false;
self::assertSame(403, $this->controller($db)->index()->status());
}
public function testRendersCatalogueCountsAndStockAlerts(): void
{
$db = $this->permittedDb();
$response = $this->controller($db)->index();
self::assertSame(200, $response->status());
$body = $response->body();
self::assertStringContainsString('Statistiques', $body);
self::assertStringContainsString('53', $body); // compteur produits
self::assertStringContainsString('Cheddar', $body); // alerte stock critique
self::assertStringContainsString('critical', $body); // bande
}
}