|
|
@@ -0,0 +1,116 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+declare(strict_types=1);
|
|
|
+
|
|
|
+namespace App\Tests\Db;
|
|
|
+
|
|
|
+use App\Db\Migrator;
|
|
|
+use PDO;
|
|
|
+use PHPUnit\Framework\TestCase;
|
|
|
+
|
|
|
+/**
|
|
|
+ * R01-N22: pin the contract that `Migrator::pendingFiles()` lets the request
|
|
|
+ * path detect a deploy-skipped migration without applying SQL itself, and
|
|
|
+ * that `migrate()` is still the apply path used by `bin/migrate.php`.
|
|
|
+ */
|
|
|
+final class MigratorTest extends TestCase
|
|
|
+{
|
|
|
+ private string $tmpDir;
|
|
|
+
|
|
|
+ protected function setUp(): void
|
|
|
+ {
|
|
|
+ $this->tmpDir = sys_get_temp_dir() . '/spw-migrator-' . bin2hex(random_bytes(4));
|
|
|
+ if (!mkdir($this->tmpDir) && !is_dir($this->tmpDir)) {
|
|
|
+ $this->fail('cannot mkdir tmp');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ protected function tearDown(): void
|
|
|
+ {
|
|
|
+ foreach (glob($this->tmpDir . '/*') ?: [] as $f) {
|
|
|
+ @unlink($f);
|
|
|
+ }
|
|
|
+ @rmdir($this->tmpDir);
|
|
|
+ }
|
|
|
+
|
|
|
+ private function makePdo(): PDO
|
|
|
+ {
|
|
|
+ $pdo = new PDO('sqlite::memory:', null, null, [
|
|
|
+ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
|
+ PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
|
|
|
+ PDO::ATTR_EMULATE_PREPARES => false,
|
|
|
+ ]);
|
|
|
+ $pdo->exec('PRAGMA foreign_keys = ON');
|
|
|
+ return $pdo;
|
|
|
+ }
|
|
|
+
|
|
|
+ private function writeMigration(string $name, string $sql): void
|
|
|
+ {
|
|
|
+ file_put_contents($this->tmpDir . DIRECTORY_SEPARATOR . $name, $sql);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testPendingFilesReturnsEverythingOnVirginDb(): void
|
|
|
+ {
|
|
|
+ $this->writeMigration('001_init.sql', 'CREATE TABLE a (x INTEGER);');
|
|
|
+ $this->writeMigration('002_more.sql', 'CREATE TABLE b (x INTEGER);');
|
|
|
+
|
|
|
+ $migrator = new Migrator($this->makePdo(), $this->tmpDir);
|
|
|
+ $this->assertSame(['001_init.sql', '002_more.sql'], $migrator->pendingFiles());
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testPendingFilesIsEmptyAfterMigrate(): void
|
|
|
+ {
|
|
|
+ $this->writeMigration('001_init.sql', 'CREATE TABLE a (x INTEGER);');
|
|
|
+ $this->writeMigration('002_more.sql', 'CREATE TABLE b (x INTEGER);');
|
|
|
+
|
|
|
+ $migrator = new Migrator($this->makePdo(), $this->tmpDir);
|
|
|
+ $migrator->migrate();
|
|
|
+ $this->assertSame([], $migrator->pendingFiles());
|
|
|
+ $this->assertSame(2, $migrator->currentVersion());
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testPendingFilesReportsOnlyNewlyAdded(): void
|
|
|
+ {
|
|
|
+ $this->writeMigration('001_init.sql', 'CREATE TABLE a (x INTEGER);');
|
|
|
+ $pdo = $this->makePdo();
|
|
|
+ (new Migrator($pdo, $this->tmpDir))->migrate();
|
|
|
+
|
|
|
+ // Operator dropped a new file in but didn't re-run migrate.
|
|
|
+ $this->writeMigration('002_more.sql', 'CREATE TABLE b (x INTEGER);');
|
|
|
+ $migrator = new Migrator($pdo, $this->tmpDir);
|
|
|
+ $this->assertSame(['002_more.sql'], $migrator->pendingFiles());
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testPendingFilesIgnoresFilesWithoutVersionPrefix(): void
|
|
|
+ {
|
|
|
+ $this->writeMigration('001_init.sql', 'CREATE TABLE a (x INTEGER);');
|
|
|
+ $this->writeMigration('README.md', '# notes');
|
|
|
+ $this->writeMigration('foo_bar.sql', 'CREATE TABLE c (x INTEGER);');
|
|
|
+
|
|
|
+ $migrator = new Migrator($this->makePdo(), $this->tmpDir);
|
|
|
+ $this->assertSame(['001_init.sql'], $migrator->pendingFiles());
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testPendingFilesCreatesSchemaVersionTable(): void
|
|
|
+ {
|
|
|
+ // Empty dir: even without any migrations the call must not blow up
|
|
|
+ // and the version table must exist (idempotent ensure).
|
|
|
+ $pdo = $this->makePdo();
|
|
|
+ $migrator = new Migrator($pdo, $this->tmpDir);
|
|
|
+ $this->assertSame([], $migrator->pendingFiles());
|
|
|
+
|
|
|
+ $row = $pdo->query("SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'")->fetch();
|
|
|
+ $this->assertIsArray($row);
|
|
|
+ $this->assertSame('schema_version', $row['name']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function testRealMigrationsDirectoryHasNoPendingAfterFullApply(): void
|
|
|
+ {
|
|
|
+ // Smoke test against the real migrations/ folder so a future reviewer
|
|
|
+ // can't drop a malformed file in without the suite noticing.
|
|
|
+ $pdo = $this->makePdo();
|
|
|
+ $migrator = new Migrator($pdo);
|
|
|
+ $migrator->migrate();
|
|
|
+ $this->assertSame([], $migrator->pendingFiles(), 'real migrations apply cleanly');
|
|
|
+ }
|
|
|
+}
|