table = Env::$dbPrefix . 'testing_migrations'; $this->connection->executeStatement("DROP TABLE IF EXISTS {$this->table}"); $this->store = $this->getServiceWithOverrides(Store::class, [ 'table' => $this->table, ]); $this->store->ensureMigrationsTable(); } public function testItReturnsCorrectStatus(): void { $this->store->startMigration('Migration_20221024_080348'); $this->store->completeMigration('Migration_20221024_080348'); $this->store->startMigration('Migration_20221025_120345'); $this->store->failMigration('Migration_20221025_120345', 'test-error'); $this->store->startMigration('Migration_20221026_160151'); $this->store->startMigration('Migration_20221023_080348'); $this->store->completeMigration('Migration_20221023_080348'); $this->store->startMigration('Migration_20221028_200226'); $this->store->completeMigration('Migration_20221028_200226'); $this->store->startMigration('Migration_20221029_212308'); $this->store->failMigration('Migration_20221029_212308', 'test-error'); $this->store->startMigration('Migration_20221030_223752'); $migrator = $this->createMigrator(); $status = $migrator->getStatus(); // completed Db $data = $status[0]; $this->assertSame('Migration_20221024_080348', $data['name']); $this->assertSame('completed', $data['status']); $this->assertSame(Repository::MIGRATIONS_LEVEL_DB, $data['level']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, (string)$data['started_at']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, (string)$data['completed_at']); $this->assertSame(0, (int)$data['retries']); $this->assertNull($data['error']); $this->assertFalse($data['unknown']); // failed Db $data = $status[1]; $this->assertSame('Migration_20221025_120345', $data['name']); $this->assertSame('failed', $data['status']); $this->assertSame(Repository::MIGRATIONS_LEVEL_DB, $data['level']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, (string)$data['started_at']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, (string)$data['completed_at']); $this->assertSame(0, (int)$data['retries']); $this->assertSame($data['error'], 'test-error'); $this->assertFalse($data['unknown']); // started Db $data = $status[2]; $this->assertSame('Migration_20221026_160151', $data['name']); $this->assertSame('started', $data['status']); $this->assertSame(Repository::MIGRATIONS_LEVEL_DB, $data['level']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, (string)$data['started_at']); $this->assertNull($data['completed_at']); $this->assertSame(0, (int)$data['retries']); $this->assertNull($data['error']); $this->assertFalse($data['unknown']); // new Db $data = $status[3]; $this->assertSame('Migration_20221027_180501', $data['name']); $this->assertSame('new', $data['status']); $this->assertSame(Repository::MIGRATIONS_LEVEL_DB, $data['level']); $this->assertNull($data['started_at']); $this->assertNull($data['completed_at']); $this->assertNull($data['retries']); $this->assertNull($data['error']); $this->assertFalse($data['unknown']); // completed App $data = $status[4]; $this->assertSame('Migration_20221023_080348', $data['name']); $this->assertSame('completed', $data['status']); $this->assertSame(Repository::MIGRATIONS_LEVEL_APP, $data['level']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, (string)$data['started_at']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, (string)$data['completed_at']); $this->assertSame(0, (int)$data['retries']); $this->assertNull($data['error']); $this->assertFalse($data['unknown']); // new App $data = $status[5]; $this->assertSame('Migration_20221025_120346', $data['name']); $this->assertSame('new', $data['status']); $this->assertSame(Repository::MIGRATIONS_LEVEL_APP, $data['level']); $this->assertNull($data['started_at']); $this->assertNull($data['completed_at']); $this->assertNull($data['retries']); $this->assertNull($data['error']); $this->assertFalse($data['unknown']); // unknown completed (unknown = stored in DB but missing in the file system) $data = $status[6]; $this->assertSame('Migration_20221028_200226', $data['name']); $this->assertSame('completed', $data['status']); $this->assertNull($data['level']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, (string)$data['started_at']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, (string)$data['completed_at']); $this->assertSame(0, (int)$data['retries']); $this->assertNull($data['error']); $this->assertTrue($data['unknown']); // unknown failed (unknown = stored in DB but missing in the file system) $data = $status[7]; $this->assertSame('Migration_20221029_212308', $data['name']); $this->assertSame('failed', $data['status']); $this->assertNull($data['level']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, (string)$data['started_at']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, (string)$data['completed_at']); $this->assertSame(0, (int)$data['retries']); $this->assertSame($data['error'], 'test-error'); $this->assertTrue($data['unknown']); // unknown started (unknown = stored in DB but missing in the file system) $data = $status[8]; $this->assertSame('Migration_20221030_223752', $data['name']); $this->assertSame('started', $data['status']); $this->assertNull($data['level']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, (string)$data['started_at']); $this->assertNull($data['completed_at']); $this->assertSame(0, (int)$data['retries']); $this->assertNull($data['error']); $this->assertTrue($data['unknown']); } public function testItReturnsEmptyStatusWhenNoMigrationsExist(): void { // no defined & no processed migrations $this->assertSame([], $this->createMigrator(__DIR__ . '/TestMigrationsEmpty')->getStatus()); } public function testItReturnsStatusForUnprocessedMigrations(): void { $newMigrationFieldsDb = [ 'level' => Repository::MIGRATIONS_LEVEL_DB, 'status' => 'new', 'started_at' => null, 'completed_at' => null, 'retries' => null, 'error' => null, 'unknown' => false, ]; $newMigrationFieldsApp = array_merge($newMigrationFieldsDb, [ 'level' => Repository::MIGRATIONS_LEVEL_APP, ]); $migrator = $this->createMigrator(); $this->assertSame([ ['name' => 'Migration_20221024_080348'] + $newMigrationFieldsDb, // Db ['name' => 'Migration_20221025_120345'] + $newMigrationFieldsDb, // Db ['name' => 'Migration_20221026_160151'] + $newMigrationFieldsDb, // Db ['name' => 'Migration_20221027_180501'] + $newMigrationFieldsDb, // Db ['name' => 'Migration_20221023_080348'] + $newMigrationFieldsApp, // App ['name' => 'Migration_20221025_120346'] + $newMigrationFieldsApp, // App ], $migrator->getStatus()); } public function testItRunsAllMigrations(): void { $migrator = $this->createMigrator(); $this->assertEmpty($this->store->getAll()); $migrator->run(); $processed = $this->store->getAll(); $this->assertCount(6, $processed); $migrations = [ 'Migration_20221024_080348', // Db 'Migration_20221025_120345', // Db 'Migration_20221026_160151', // Db 'Migration_20221027_180501', // Db 'Migration_20221023_080348', // App 'Migration_20221025_120346', // App ]; foreach ($processed as $i => $data) { $this->assertSame(strval($i + 1), $data['id']); $this->assertSame($migrations[$i], $data['name']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, $data['started_at']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, $data['completed_at']); $this->assertSame(0, (int)$data['retries']); $this->assertNull($data['error']); } } public function testItRunsNewMigrations(): void { $this->store->startMigration('Migration_20221024_080348'); $this->store->completeMigration('Migration_20221024_080348'); $this->store->startMigration('Migration_20221026_160151'); $this->store->completeMigration('Migration_20221026_160151'); $this->assertCount(2, $this->store->getAll()); $migrator = $this->createMigrator(); $migrator->run(); $processed = $this->store->getAll(); $this->assertCount(6, $processed); $migrations = [ 'Migration_20221024_080348', // Db 'Migration_20221026_160151', // Db 'Migration_20221025_120345', // Db 'Migration_20221027_180501', // Db 'Migration_20221023_080348', // App 'Migration_20221025_120346', // App ]; foreach ($processed as $i => $data) { $this->assertSame($migrations[$i], $data['name']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, $data['started_at']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, $data['completed_at']); $this->assertSame(0, (int)$data['retries']); $this->assertNull($data['error']); } } public function testItSkipsUnknownMigrations(): void { $this->store->startMigration('Migration_20221028_200226'); $this->store->completeMigration('Migration_20221028_200226'); $this->store->startMigration('Migration_20221029_212308'); $this->store->failMigration('Migration_20221029_212308', 'test-error'); $this->store->startMigration('Migration_20221030_223752'); $this->assertCount(3, $this->store->getAll()); $migrator = $this->createMigrator(); $migrator->run(); // no error thrown } public function testItCallsLoggerWhenRunningMigrations(): void { $migrator = $this->createMigrator(); $migrator->run($this->makeEmpty(Logger::class, [ 'logBefore' => $this->exactly(1), 'logMigrationStarted' => $this->exactly(5), 'logMigrationCompleted' => $this->exactly(4), 'logAfter' => $this->exactly(1), ])); $processed = $this->store->getAll(); $this->assertCount(6, $processed); } public function testItRetriesWhenRunningMigrationExists(): void { $this->store->startMigration('Migration_20221025_120345'); $migrator = $this->createMigrator(); $migrator->run(); $processed = $this->store->getAll(); $this->assertCount(6, $processed); $data = $processed[0]; $this->assertSame('Migration_20221025_120345', $data['name']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, $data['started_at']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, $data['completed_at']); $this->assertSame(1, (int)$data['retries']); $this->assertNull($data['error']); } public function testItRetriesWhenFailedMigrationExists(): void { $this->store->startMigration('Migration_20221026_160151'); $this->store->failMigration('Migration_20221026_160151', 'test-error'); $migrator = $this->createMigrator(); $migrator->run(); $processed = $this->store->getAll(); $this->assertCount(6, $processed); $data = $processed[0]; $this->assertSame('Migration_20221026_160151', $data['name']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, $data['started_at']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, $data['completed_at']); $this->assertSame(1, (int)$data['retries']); $this->assertNull($data['error']); } public function testItFailsBrokenMigration(): void { $this->expectException(MigratorException::class); $this->expectExceptionMessage('Migration "MailPoet\Migrations\Db\Migration_20221023_040819" failed. Details: Testing failing migration.'); $migrator = $this->createMigrator(__DIR__ . '/TestMigrationsFail'); $migrator->run(); $processed = $this->store->getAll(); $this->assertCount(1, $processed); $data = $processed[0]; $this->assertSame('Migration_20221023_040819', $data['name']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, $data['started_at']); $this->assertStringMatchesFormat(self::DATE_TIME_FORMAT, $data['completed_at']); $this->assertNull($data['retries']); $this->assertSame($data['error'], 'Testing failing migration.'); } /** @return Migrator */ private function createMigrator(string $migrationsDir = __DIR__ . '/TestMigrations'): Migrator { $repository = $this->getServiceWithOverrides(Repository::class, [ 'migrationsDir' => $migrationsDir, ]); $runner = $this->getServiceWithOverrides(Runner::class, [ 'store' => $this->store, ]); return $this->getServiceWithOverrides(Migrator::class, [ 'repository' => $repository, 'runner' => $runner, 'store' => $this->store, ]); } public function _after() { parent::_after(); $this->connection->executeStatement("DROP TABLE IF EXISTS {$this->table}"); } }