213 lines
7.8 KiB
PHP
213 lines
7.8 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
|
|
namespace Tests\Shared;
|
|
|
|
use App\Shared\Database\Migrator;
|
|
use PDO;
|
|
use PDOStatement;
|
|
use PHPUnit\Framework\MockObject\MockObject;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* Tests unitaires pour Migrator.
|
|
*
|
|
* Vérifie que run() crée la table de suivi, exécute uniquement les migrations
|
|
* en attente (pas celles déjà appliquées), et appelle syncFtsIndex().
|
|
*
|
|
* Les fichiers de migration réels ne sont pas chargés : Migrator::run() est
|
|
* testé via une base SQLite en mémoire, ce qui est plus fiable qu'un mock
|
|
* de exec() et permet de vérifier l'état réel de la base après exécution.
|
|
*
|
|
* syncFtsIndex() requiert les tables posts, users et posts_fts — elles sont
|
|
* créées minimalement avant chaque test qui en a besoin.
|
|
*/
|
|
final class MigratorTest extends TestCase
|
|
{
|
|
private PDO $db;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->db = new PDO('sqlite::memory:', options: [
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
]);
|
|
|
|
// strip_tags() doit être disponible comme fonction SQLite
|
|
// (enregistrée dans container.php en production)
|
|
$this->db->sqliteCreateFunction('strip_tags', 'strip_tags', 1);
|
|
}
|
|
|
|
|
|
// ── createMigrationTable ───────────────────────────────────────
|
|
|
|
/**
|
|
* run() doit créer la table 'migrations' si elle n'existe pas.
|
|
*/
|
|
public function testRunCreatesMigrationsTable(): void
|
|
{
|
|
$this->createMinimalSchema();
|
|
|
|
Migrator::run($this->db);
|
|
|
|
$stmt = $this->db->query("SELECT name FROM sqlite_master WHERE type='table' AND name='migrations'");
|
|
$this->assertNotFalse($stmt->fetchColumn(), 'La table migrations doit exister après run()');
|
|
}
|
|
|
|
/**
|
|
* run() est idempotent : appeler run() deux fois ne génère pas d'erreur.
|
|
*/
|
|
public function testRunIsIdempotent(): void
|
|
{
|
|
$this->createMinimalSchema();
|
|
|
|
Migrator::run($this->db);
|
|
Migrator::run($this->db); // deuxième appel — ne doit pas planter
|
|
|
|
$this->addToAssertionCount(1);
|
|
}
|
|
|
|
|
|
// ── runPendingMigrations ───────────────────────────────────────
|
|
|
|
/**
|
|
* Une migration déjà enregistrée dans la table migrations
|
|
* ne doit pas être rejouée.
|
|
*/
|
|
public function testAlreadyAppliedMigrationIsSkipped(): void
|
|
{
|
|
$this->createMinimalSchema();
|
|
Migrator::run($this->db);
|
|
|
|
$before = $this->countMigrations();
|
|
|
|
// Simuler une migration future déjà appliquée (version fictive
|
|
// qui ne correspond à aucun fichier réel — ne génère pas de conflit UNIQUE)
|
|
$stmt = $this->db->prepare("INSERT INTO migrations (version, run_at) VALUES (:v, :r)");
|
|
$stmt->execute([':v' => '999_future_migration', ':r' => date('Y-m-d H:i:s')]);
|
|
|
|
Migrator::run($this->db);
|
|
$after = $this->countMigrations();
|
|
|
|
// Le nombre de migrations enregistrées ne doit pas avoir changé
|
|
$this->assertSame($before + 1, $after);
|
|
}
|
|
|
|
/**
|
|
* La table migrations doit contenir une entrée par migration exécutée.
|
|
*/
|
|
public function testRunRecordsMigrationsInTable(): void
|
|
{
|
|
$this->createMinimalSchema();
|
|
|
|
Migrator::run($this->db);
|
|
|
|
$count = $this->countMigrations();
|
|
// Le projet a au moins une migration (001_create_users)
|
|
$this->assertGreaterThan(0, $count, 'Au moins une migration doit être enregistrée');
|
|
}
|
|
|
|
|
|
// ── syncFtsIndex ───────────────────────────────────────────────
|
|
|
|
/**
|
|
* syncFtsIndex() doit insérer dans posts_fts les articles
|
|
* absents de l'index après run().
|
|
*/
|
|
public function testSyncFtsIndexInsertsUnindexedPosts(): void
|
|
{
|
|
// Exécuter les vraies migrations pour avoir le schéma complet
|
|
Migrator::run($this->db);
|
|
|
|
// Insérer un article directement en base (bypass des triggers FTS)
|
|
$this->db->exec("
|
|
INSERT INTO users (id, username, email, password_hash, role, created_at)
|
|
VALUES (1, 'alice', 'alice@example.com', 'hash', 'user', '2024-01-01 00:00:00')
|
|
");
|
|
$this->db->exec("
|
|
INSERT INTO posts (id, title, content, slug, author_id, created_at, updated_at)
|
|
VALUES (1, 'Test', '<p>Contenu</p>', 'test', 1, '2024-01-01 00:00:00', '2024-01-01 00:00:00')
|
|
");
|
|
// Supprimer l'entrée FTS pour simuler un article non indexé
|
|
$this->db->exec("DELETE FROM posts_fts WHERE rowid = 1");
|
|
|
|
// run() doit réindexer cet article via syncFtsIndex
|
|
Migrator::run($this->db);
|
|
|
|
$stmt = $this->db->query('SELECT rowid FROM posts_fts WHERE rowid = 1');
|
|
$this->assertNotFalse($stmt->fetchColumn(), "L'article doit être présent dans posts_fts après run()");
|
|
}
|
|
|
|
/**
|
|
* syncFtsIndex() ne doit pas créer de doublon pour un article
|
|
* déjà présent dans l'index.
|
|
*/
|
|
public function testSyncFtsIndexDoesNotDuplicateIndexedPosts(): void
|
|
{
|
|
Migrator::run($this->db);
|
|
|
|
// Insérer un article — le trigger FTS l'indexe automatiquement
|
|
$this->db->exec("
|
|
INSERT INTO users (id, username, email, password_hash, role, created_at)
|
|
VALUES (1, 'alice', 'alice@example.com', 'hash', 'user', '2024-01-01 00:00:00')
|
|
");
|
|
$this->db->exec("
|
|
INSERT INTO posts (id, title, content, slug, author_id, created_at, updated_at)
|
|
VALUES (1, 'Test', '<p>Contenu</p>', 'test', 1, '2024-01-01 00:00:00', '2024-01-01 00:00:00')
|
|
");
|
|
|
|
$before = (int) $this->db->query('SELECT COUNT(*) FROM posts_fts')->fetchColumn();
|
|
|
|
// Deuxième run() — ne doit pas dupliquer l'entrée FTS
|
|
Migrator::run($this->db);
|
|
|
|
$after = (int) $this->db->query('SELECT COUNT(*) FROM posts_fts')->fetchColumn();
|
|
$this->assertSame($before, $after);
|
|
}
|
|
|
|
|
|
// ── Helpers ────────────────────────────────────────────────────
|
|
|
|
/**
|
|
* Crée le schéma minimal requis par run() quand les vraies migrations
|
|
* ne sont pas chargées (posts, users, posts_fts pour syncFtsIndex).
|
|
*
|
|
* Utilisé uniquement pour les tests qui ne veulent pas dépendre
|
|
* des fichiers de migration réels.
|
|
*/
|
|
private function createMinimalSchema(): void
|
|
{
|
|
$this->db->exec('
|
|
CREATE TABLE IF NOT EXISTS users (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
username TEXT NOT NULL UNIQUE,
|
|
email TEXT NOT NULL UNIQUE,
|
|
password_hash TEXT NOT NULL,
|
|
role TEXT NOT NULL DEFAULT "user",
|
|
created_at DATETIME NOT NULL
|
|
)
|
|
');
|
|
$this->db->exec('
|
|
CREATE TABLE IF NOT EXISTS posts (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
title TEXT NOT NULL,
|
|
content TEXT NOT NULL DEFAULT "",
|
|
slug TEXT NOT NULL UNIQUE,
|
|
author_id INTEGER,
|
|
category_id INTEGER,
|
|
created_at DATETIME NOT NULL,
|
|
updated_at DATETIME NOT NULL
|
|
)
|
|
');
|
|
$this->db->exec("
|
|
CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts
|
|
USING fts5(title, content, author_username, content='posts', content_rowid='id')
|
|
");
|
|
}
|
|
|
|
private function countMigrations(): int
|
|
{
|
|
return (int) $this->db->query('SELECT COUNT(*) FROM migrations')->fetchColumn();
|
|
}
|
|
}
|