MigratorTest.php 4.4 KB

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