208 lines
6.7 KiB
PHP
208 lines
6.7 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Shared;
|
|
|
|
use App\Shared\Database\Seeder;
|
|
use PDO;
|
|
use PDOStatement;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* Tests unitaires pour Seeder.
|
|
*
|
|
* Vérifie que seed() insère le compte administrateur quand il est absent,
|
|
* et ne fait rien si le compte existe déjà (idempotence).
|
|
*
|
|
* PDO et PDOStatement sont mockés pour isoler le Seeder de la base de données.
|
|
* Les variables d'environnement sont définies dans setUp() et restaurées dans tearDown().
|
|
*/
|
|
final class SeederTest extends TestCase
|
|
{
|
|
/** @var PDO&MockObject */
|
|
private PDO $db;
|
|
|
|
/** @var array<string, string> Variables d'environnement sauvegardées avant chaque test */
|
|
private array $envBackup;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->db = $this->createMock(PDO::class);
|
|
|
|
$this->envBackup = [
|
|
'ADMIN_USERNAME' => $_ENV['ADMIN_USERNAME'] ?? '',
|
|
'ADMIN_EMAIL' => $_ENV['ADMIN_EMAIL'] ?? '',
|
|
'ADMIN_PASSWORD' => $_ENV['ADMIN_PASSWORD'] ?? '',
|
|
];
|
|
|
|
$_ENV['ADMIN_USERNAME'] = 'admin';
|
|
$_ENV['ADMIN_EMAIL'] = 'admin@example.com';
|
|
$_ENV['ADMIN_PASSWORD'] = 'secret1234';
|
|
}
|
|
|
|
protected function tearDown(): void
|
|
{
|
|
foreach ($this->envBackup as $key => $value) {
|
|
$_ENV[$key] = $value;
|
|
}
|
|
}
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Crée un PDOStatement mock retournant $fetchColumnValue pour fetchColumn().
|
|
*/
|
|
private function stmtReturning(mixed $fetchColumnValue): PDOStatement&MockObject
|
|
{
|
|
$stmt = $this->createMock(PDOStatement::class);
|
|
$stmt->method('execute')->willReturn(true);
|
|
$stmt->method('fetchColumn')->willReturn($fetchColumnValue);
|
|
|
|
return $stmt;
|
|
}
|
|
|
|
private function stmtForWrite(): PDOStatement&MockObject
|
|
{
|
|
$stmt = $this->createMock(PDOStatement::class);
|
|
$stmt->method('execute')->willReturn(true);
|
|
|
|
return $stmt;
|
|
}
|
|
|
|
|
|
// ── seed() — admin absent ──────────────────────────────────────
|
|
|
|
/**
|
|
* seed() doit insérer le compte admin quand aucun utilisateur
|
|
* portant ce nom d'utilisateur n'existe en base.
|
|
*/
|
|
public function testSeedInsertsAdminWhenAbsent(): void
|
|
{
|
|
$selectStmt = $this->stmtReturning(false);
|
|
$insertStmt = $this->stmtForWrite();
|
|
|
|
$this->db->expects($this->exactly(2))
|
|
->method('prepare')
|
|
->willReturnOnConsecutiveCalls($selectStmt, $insertStmt);
|
|
|
|
$insertStmt->expects($this->once())
|
|
->method('execute')
|
|
->with($this->callback(function (array $data): bool {
|
|
return $data[':username'] === 'admin'
|
|
&& $data[':email'] === 'admin@example.com'
|
|
&& $data[':role'] === 'admin'
|
|
&& isset($data[':password_hash'], $data[':created_at'])
|
|
&& password_verify('secret1234', $data[':password_hash']);
|
|
}));
|
|
|
|
Seeder::seed($this->db);
|
|
}
|
|
|
|
/**
|
|
* seed() doit normaliser le nom d'utilisateur en minuscules
|
|
* et supprimer les espaces autour.
|
|
*/
|
|
public function testSeedNormalizesUsername(): void
|
|
{
|
|
$_ENV['ADMIN_USERNAME'] = ' ADMIN ';
|
|
$_ENV['ADMIN_EMAIL'] = ' ADMIN@EXAMPLE.COM ';
|
|
|
|
$selectStmt = $this->stmtReturning(false);
|
|
$insertStmt = $this->stmtForWrite();
|
|
|
|
$this->db->method('prepare')
|
|
->willReturnOnConsecutiveCalls($selectStmt, $insertStmt);
|
|
|
|
$insertStmt->expects($this->once())
|
|
->method('execute')
|
|
->with($this->callback(function (array $data): bool {
|
|
return $data[':username'] === 'admin'
|
|
&& $data[':email'] === 'admin@example.com';
|
|
}));
|
|
|
|
Seeder::seed($this->db);
|
|
}
|
|
|
|
/**
|
|
* seed() doit stocker un hash bcrypt, jamais le mot de passe en clair.
|
|
*/
|
|
public function testSeedHashesPasswordBeforeInsert(): void
|
|
{
|
|
$selectStmt = $this->stmtReturning(false);
|
|
$insertStmt = $this->stmtForWrite();
|
|
|
|
$this->db->method('prepare')
|
|
->willReturnOnConsecutiveCalls($selectStmt, $insertStmt);
|
|
|
|
$insertStmt->expects($this->once())
|
|
->method('execute')
|
|
->with($this->callback(function (array $data): bool {
|
|
// Le hash ne doit pas être le mot de passe brut
|
|
return $data[':password_hash'] !== 'secret1234'
|
|
// Et doit être vérifiable avec password_verify
|
|
&& password_verify('secret1234', $data[':password_hash']);
|
|
}));
|
|
|
|
Seeder::seed($this->db);
|
|
}
|
|
|
|
/**
|
|
* seed() doit renseigner created_at au format 'Y-m-d H:i:s'.
|
|
*/
|
|
public function testSeedSetsCreatedAt(): void
|
|
{
|
|
$selectStmt = $this->stmtReturning(false);
|
|
$insertStmt = $this->stmtForWrite();
|
|
|
|
$this->db->method('prepare')
|
|
->willReturnOnConsecutiveCalls($selectStmt, $insertStmt);
|
|
|
|
$insertStmt->expects($this->once())
|
|
->method('execute')
|
|
->with($this->callback(function (array $data): bool {
|
|
return isset($data[':created_at'])
|
|
&& (bool) \DateTime::createFromFormat('Y-m-d H:i:s', $data[':created_at']);
|
|
}));
|
|
|
|
Seeder::seed($this->db);
|
|
}
|
|
|
|
|
|
// ── seed() — admin présent (idempotence) ───────────────────────
|
|
|
|
/**
|
|
* seed() ne doit pas exécuter d'INSERT si le compte admin existe déjà.
|
|
*/
|
|
public function testSeedDoesNotInsertWhenAdminExists(): void
|
|
{
|
|
// fetchColumn() retourne l'id existant — le compte est déjà là
|
|
$selectStmt = $this->stmtReturning('1');
|
|
|
|
$this->db->expects($this->once())
|
|
->method('prepare')
|
|
->willReturn($selectStmt);
|
|
|
|
// prepare() ne doit être appelé qu'une fois (SELECT uniquement, pas d'INSERT)
|
|
Seeder::seed($this->db);
|
|
}
|
|
|
|
/**
|
|
* seed() vérifie l'existence du compte via le nom d'utilisateur normalisé.
|
|
*/
|
|
public function testSeedChecksExistenceByNormalizedUsername(): void
|
|
{
|
|
$_ENV['ADMIN_USERNAME'] = ' Admin ';
|
|
|
|
$selectStmt = $this->stmtReturning('1');
|
|
|
|
$this->db->method('prepare')->willReturn($selectStmt);
|
|
|
|
$selectStmt->expects($this->once())
|
|
->method('execute')
|
|
->with([':username' => 'admin']);
|
|
|
|
Seeder::seed($this->db);
|
|
}
|
|
}
|