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); } }