Files
slim-blog/tests/Shared/SeederTest.php
2026-03-16 02:33:18 +01:00

209 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().
*/
#[\PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations]
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);
}
}