first commit
This commit is contained in:
207
tests/Shared/SeederTest.php
Normal file
207
tests/Shared/SeederTest.php
Normal file
@@ -0,0 +1,207 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user