Skip to main content
Glama
migration-validator.test.ts13.7 kB
/** * Migration Validator Tests * * Tests for migration file validation and health checks. */ import { existsSync, mkdirSync, rmSync, writeFileSync } from "fs"; import { join } from "path"; import { afterAll, beforeAll, describe, expect, it } from "vitest"; import { MigrationValidator } from "../../../database/migration-validator"; import type { Migration } from "../../../database/schema-migration"; describe("MigrationValidator", () => { const testDir = join(process.cwd(), "tmp", "test-migrations"); beforeAll(() => { // Create test directory if (!existsSync(testDir)) { mkdirSync(testDir, { recursive: true }); } }); afterAll(() => { // Clean up test directory if (existsSync(testDir)) { rmSync(testDir, { recursive: true, force: true }); } }); describe("validateMigration", () => { it("should validate a valid migration", () => { // Create valid migration files writeFileSync( join(testDir, "001_test.sql"), "CREATE TABLE IF NOT EXISTS test (id TEXT PRIMARY KEY);" ); writeFileSync(join(testDir, "001_test_down.sql"), "DROP TABLE IF EXISTS test;"); const validator = new MigrationValidator(testDir); const migration: Migration = { version: 1, name: "test", upFile: "001_test.sql", downFile: "001_test_down.sql", }; const result = validator.validateMigration(migration); expect(result.isValid).toBe(true); expect(result.errors).toHaveLength(0); }); it("should detect missing up file", () => { const validator = new MigrationValidator(testDir); const migration: Migration = { version: 99, name: "missing", upFile: "099_missing.sql", downFile: "099_missing_down.sql", }; const result = validator.validateMigration(migration); expect(result.isValid).toBe(false); expect(result.errors).toContain("Up migration file not found: 099_missing.sql"); }); it("should detect missing down file", () => { // Create only up file writeFileSync(join(testDir, "002_partial.sql"), "CREATE TABLE partial (id TEXT);"); const validator = new MigrationValidator(testDir); const migration: Migration = { version: 2, name: "partial", upFile: "002_partial.sql", downFile: "002_partial_down.sql", }; const result = validator.validateMigration(migration); expect(result.isValid).toBe(false); expect(result.errors).toContain("Down migration file not found: 002_partial_down.sql"); }); it("should detect empty migration files", () => { writeFileSync(join(testDir, "003_empty.sql"), ""); writeFileSync(join(testDir, "003_empty_down.sql"), ""); const validator = new MigrationValidator(testDir); const migration: Migration = { version: 3, name: "empty", upFile: "003_empty.sql", downFile: "003_empty_down.sql", }; const result = validator.validateMigration(migration); expect(result.isValid).toBe(false); expect(result.errors).toContain("up migration file is empty"); expect(result.errors).toContain("down migration file is empty"); }); it("should detect invalid version number", () => { writeFileSync(join(testDir, "000_invalid.sql"), "SELECT 1;"); writeFileSync(join(testDir, "000_invalid_down.sql"), "SELECT 1;"); const validator = new MigrationValidator(testDir); const migration: Migration = { version: 0, name: "invalid", upFile: "000_invalid.sql", downFile: "000_invalid_down.sql", }; const result = validator.validateMigration(migration); expect(result.isValid).toBe(false); expect(result.errors).toContain("Invalid version number: 0"); }); it("should detect empty migration name", () => { writeFileSync(join(testDir, "004_noname.sql"), "SELECT 1;"); writeFileSync(join(testDir, "004_noname_down.sql"), "SELECT 1;"); const validator = new MigrationValidator(testDir); const migration: Migration = { version: 4, name: "", upFile: "004_noname.sql", downFile: "004_noname_down.sql", }; const result = validator.validateMigration(migration); expect(result.isValid).toBe(false); expect(result.errors).toContain("Migration name is empty"); }); it("should warn about DROP DATABASE", () => { writeFileSync(join(testDir, "005_dangerous.sql"), "DROP DATABASE production;"); writeFileSync(join(testDir, "005_dangerous_down.sql"), "SELECT 1;"); const validator = new MigrationValidator(testDir); const migration: Migration = { version: 5, name: "dangerous", upFile: "005_dangerous.sql", downFile: "005_dangerous_down.sql", }; const result = validator.validateMigration(migration); expect(result.isValid).toBe(false); expect(result.errors).toContain("DROP DATABASE is not allowed in migrations"); }); it("should warn about CREATE TABLE without IF NOT EXISTS", () => { writeFileSync(join(testDir, "006_notidempotent.sql"), "CREATE TABLE test (id TEXT);"); writeFileSync(join(testDir, "006_notidempotent_down.sql"), "DROP TABLE IF EXISTS test;"); const validator = new MigrationValidator(testDir); const migration: Migration = { version: 6, name: "notidempotent", upFile: "006_notidempotent.sql", downFile: "006_notidempotent_down.sql", }; const result = validator.validateMigration(migration); expect(result.isValid).toBe(true); expect(result.warnings).toContain("CREATE TABLE without IF NOT EXISTS may fail on re-run"); }); it("should warn about DROP TABLE without IF EXISTS", () => { writeFileSync( join(testDir, "007_dropnocheck.sql"), "CREATE TABLE IF NOT EXISTS test (id TEXT);" ); writeFileSync(join(testDir, "007_dropnocheck_down.sql"), "DROP TABLE test;"); const validator = new MigrationValidator(testDir); const migration: Migration = { version: 7, name: "dropnocheck", upFile: "007_dropnocheck.sql", downFile: "007_dropnocheck_down.sql", }; const result = validator.validateMigration(migration); expect(result.isValid).toBe(true); expect(result.warnings).toContain( "DROP TABLE without IF EXISTS may fail if table doesn't exist" ); }); it("should warn about comment-only migrations", () => { writeFileSync( join(testDir, "008_comments.sql"), "-- This is just a comment\n-- Another comment" ); writeFileSync(join(testDir, "008_comments_down.sql"), "-- Rollback comment"); const validator = new MigrationValidator(testDir); const migration: Migration = { version: 8, name: "comments", upFile: "008_comments.sql", downFile: "008_comments_down.sql", }; const result = validator.validateMigration(migration); expect(result.isValid).toBe(true); expect(result.warnings).toContain("up migration contains only comments"); expect(result.warnings).toContain("down migration contains only comments"); }); }); describe("validateAll", () => { it("should validate multiple migrations", () => { writeFileSync(join(testDir, "010_valid1.sql"), "CREATE TABLE IF NOT EXISTS t1 (id TEXT);"); writeFileSync(join(testDir, "010_valid1_down.sql"), "DROP TABLE IF EXISTS t1;"); writeFileSync(join(testDir, "011_valid2.sql"), "CREATE TABLE IF NOT EXISTS t2 (id TEXT);"); writeFileSync(join(testDir, "011_valid2_down.sql"), "DROP TABLE IF EXISTS t2;"); const validator = new MigrationValidator(testDir); const migrations: Migration[] = [ { version: 10, name: "valid1", upFile: "010_valid1.sql", downFile: "010_valid1_down.sql" }, { version: 11, name: "valid2", upFile: "011_valid2.sql", downFile: "011_valid2_down.sql" }, ]; const report = validator.validateAll(migrations); expect(report.isValid).toBe(true); expect(report.totalMigrations).toBe(2); expect(report.validMigrations).toBe(2); expect(report.invalidMigrations).toBe(0); }); it("should report invalid migrations in batch", () => { writeFileSync(join(testDir, "012_valid.sql"), "CREATE TABLE IF NOT EXISTS t3 (id TEXT);"); writeFileSync(join(testDir, "012_valid_down.sql"), "DROP TABLE IF EXISTS t3;"); const validator = new MigrationValidator(testDir); const migrations: Migration[] = [ { version: 12, name: "valid", upFile: "012_valid.sql", downFile: "012_valid_down.sql" }, { version: 13, name: "invalid", upFile: "013_missing.sql", downFile: "013_missing_down.sql", }, ]; const report = validator.validateAll(migrations); expect(report.isValid).toBe(false); expect(report.totalMigrations).toBe(2); expect(report.validMigrations).toBe(1); expect(report.invalidMigrations).toBe(1); }); }); describe("checkSequence", () => { it("should detect no gaps in sequential migrations", () => { const validator = new MigrationValidator(testDir); const migrations: Migration[] = [ { version: 1, name: "first", upFile: "001.sql", downFile: "001_down.sql" }, { version: 2, name: "second", upFile: "002.sql", downFile: "002_down.sql" }, { version: 3, name: "third", upFile: "003.sql", downFile: "003_down.sql" }, ]; const result = validator.checkSequence(migrations); expect(result.hasGaps).toBe(false); expect(result.gaps).toHaveLength(0); }); it("should detect gaps in migration sequence", () => { const validator = new MigrationValidator(testDir); const migrations: Migration[] = [ { version: 1, name: "first", upFile: "001.sql", downFile: "001_down.sql" }, { version: 3, name: "third", upFile: "003.sql", downFile: "003_down.sql" }, { version: 5, name: "fifth", upFile: "005.sql", downFile: "005_down.sql" }, ]; const result = validator.checkSequence(migrations); expect(result.hasGaps).toBe(true); expect(result.gaps).toContain(2); expect(result.gaps).toContain(4); }); }); describe("generateDryRunReport", () => { it("should generate report for pending migrations", () => { const validator = new MigrationValidator(testDir); const pendingMigrations: Migration[] = [ { version: 3, name: "third", upFile: "003.sql", downFile: "003_down.sql" }, { version: 4, name: "fourth", upFile: "004.sql", downFile: "004_down.sql" }, ]; const report = validator.generateDryRunReport(pendingMigrations, 2); expect(report).toContain("Current database version: 2"); expect(report).toContain("Pending migrations: 2"); expect(report).toContain("3. third"); expect(report).toContain("4. fourth"); expect(report).toContain("database will be at version 4"); }); it("should report when database is up to date", () => { const validator = new MigrationValidator(testDir); const report = validator.generateDryRunReport([], 4); expect(report).toContain("Database is up to date at version 4"); expect(report).toContain("No migrations to apply"); }); }); describe("generateRollbackPlan", () => { it("should generate rollback plan", () => { const validator = new MigrationValidator(testDir); const migrations: Migration[] = [ { version: 1, name: "first", upFile: "001.sql", downFile: "001_down.sql" }, { version: 2, name: "second", upFile: "002.sql", downFile: "002_down.sql" }, { version: 3, name: "third", upFile: "003.sql", downFile: "003_down.sql" }, ]; const plan = validator.generateRollbackPlan(3, 1, migrations); expect(plan).toContain("Current database version: 3"); expect(plan).toContain("Target version: 1"); expect(plan).toContain("Migrations to rollback: 2"); expect(plan).toContain("3. third"); expect(plan).toContain("2. second"); expect(plan).toContain("WARNING: Rollback may result in data loss"); }); it("should report when no rollback needed", () => { const validator = new MigrationValidator(testDir); const migrations: Migration[] = [ { version: 1, name: "first", upFile: "001.sql", downFile: "001_down.sql" }, ]; const plan = validator.generateRollbackPlan(1, 2, migrations); expect(plan).toContain("Target version 2 is not less than current version 1"); expect(plan).toContain("No rollback needed"); }); }); describe("getMigrationFiles", () => { it("should return empty array for non-existent directory", () => { const validator = new MigrationValidator("/nonexistent/path"); const files = validator.getMigrationFiles(); expect(files).toHaveLength(0); }); it("should parse migration files from directory", () => { // Create test migration files writeFileSync(join(testDir, "020_test_migration.sql"), "SELECT 1;"); writeFileSync(join(testDir, "020_test_migration_down.sql"), "SELECT 1;"); const validator = new MigrationValidator(testDir); const files = validator.getMigrationFiles(); const testMigration = files.find((f) => f.version === 20); expect(testMigration).toBeDefined(); expect(testMigration?.name).toBe("test_migration"); expect(testMigration?.upExists).toBe(true); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keyurgolani/ThoughtMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server