1
0

MigratorTest.php 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116
  1. <?php
  2. declare(strict_types=1);
  3. namespace App\Tests\Db;
  4. use App\Db\Migrator;
  5. use PDO;
  6. use PHPUnit\Framework\TestCase;
  7. /**
  8. * R01-N22: pin the contract that `Migrator::pendingFiles()` lets the request
  9. * path detect a deploy-skipped migration without applying SQL itself, and
  10. * that `migrate()` is still the apply path used by `bin/migrate.php`.
  11. */
  12. final class MigratorTest extends TestCase
  13. {
  14. private string $tmpDir;
  15. protected function setUp(): void
  16. {
  17. $this->tmpDir = sys_get_temp_dir() . '/spw-migrator-' . bin2hex(random_bytes(4));
  18. if (!mkdir($this->tmpDir) && !is_dir($this->tmpDir)) {
  19. $this->fail('cannot mkdir tmp');
  20. }
  21. }
  22. protected function tearDown(): void
  23. {
  24. foreach (glob($this->tmpDir . '/*') ?: [] as $f) {
  25. @unlink($f);
  26. }
  27. @rmdir($this->tmpDir);
  28. }
  29. private function makePdo(): PDO
  30. {
  31. $pdo = new PDO('sqlite::memory:', null, null, [
  32. PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
  33. PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
  34. PDO::ATTR_EMULATE_PREPARES => false,
  35. ]);
  36. $pdo->exec('PRAGMA foreign_keys = ON');
  37. return $pdo;
  38. }
  39. private function writeMigration(string $name, string $sql): void
  40. {
  41. file_put_contents($this->tmpDir . DIRECTORY_SEPARATOR . $name, $sql);
  42. }
  43. public function testPendingFilesReturnsEverythingOnVirginDb(): void
  44. {
  45. $this->writeMigration('001_init.sql', 'CREATE TABLE a (x INTEGER);');
  46. $this->writeMigration('002_more.sql', 'CREATE TABLE b (x INTEGER);');
  47. $migrator = new Migrator($this->makePdo(), $this->tmpDir);
  48. $this->assertSame(['001_init.sql', '002_more.sql'], $migrator->pendingFiles());
  49. }
  50. public function testPendingFilesIsEmptyAfterMigrate(): void
  51. {
  52. $this->writeMigration('001_init.sql', 'CREATE TABLE a (x INTEGER);');
  53. $this->writeMigration('002_more.sql', 'CREATE TABLE b (x INTEGER);');
  54. $migrator = new Migrator($this->makePdo(), $this->tmpDir);
  55. $migrator->migrate();
  56. $this->assertSame([], $migrator->pendingFiles());
  57. $this->assertSame(2, $migrator->currentVersion());
  58. }
  59. public function testPendingFilesReportsOnlyNewlyAdded(): void
  60. {
  61. $this->writeMigration('001_init.sql', 'CREATE TABLE a (x INTEGER);');
  62. $pdo = $this->makePdo();
  63. (new Migrator($pdo, $this->tmpDir))->migrate();
  64. // Operator dropped a new file in but didn't re-run migrate.
  65. $this->writeMigration('002_more.sql', 'CREATE TABLE b (x INTEGER);');
  66. $migrator = new Migrator($pdo, $this->tmpDir);
  67. $this->assertSame(['002_more.sql'], $migrator->pendingFiles());
  68. }
  69. public function testPendingFilesIgnoresFilesWithoutVersionPrefix(): void
  70. {
  71. $this->writeMigration('001_init.sql', 'CREATE TABLE a (x INTEGER);');
  72. $this->writeMigration('README.md', '# notes');
  73. $this->writeMigration('foo_bar.sql', 'CREATE TABLE c (x INTEGER);');
  74. $migrator = new Migrator($this->makePdo(), $this->tmpDir);
  75. $this->assertSame(['001_init.sql'], $migrator->pendingFiles());
  76. }
  77. public function testPendingFilesCreatesSchemaVersionTable(): void
  78. {
  79. // Empty dir: even without any migrations the call must not blow up
  80. // and the version table must exist (idempotent ensure).
  81. $pdo = $this->makePdo();
  82. $migrator = new Migrator($pdo, $this->tmpDir);
  83. $this->assertSame([], $migrator->pendingFiles());
  84. $row = $pdo->query("SELECT name FROM sqlite_master WHERE type='table' AND name='schema_version'")->fetch();
  85. $this->assertIsArray($row);
  86. $this->assertSame('schema_version', $row['name']);
  87. }
  88. public function testRealMigrationsDirectoryHasNoPendingAfterFullApply(): void
  89. {
  90. // Smoke test against the real migrations/ folder so a future reviewer
  91. // can't drop a malformed file in without the suite noticing.
  92. $pdo = $this->makePdo();
  93. $migrator = new Migrator($pdo);
  94. $migrator->migrate();
  95. $this->assertSame([], $migrator->pendingFiles(), 'real migrations apply cleanly');
  96. }
  97. }