334 lines
13 KiB
JavaScript
334 lines
13 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
|
|
import {
|
|
BaselineAdoptionError,
|
|
REPOSITORY_ROOT,
|
|
adoptPrismaBaseline,
|
|
classifyMigrationHistory,
|
|
deployMissingLegacyMigrations,
|
|
resolveBaselineMigration,
|
|
verifyLegacyVariantCatalog,
|
|
verifyPrismaSchemaParity,
|
|
} from "./adopt-prisma-baseline.mjs";
|
|
|
|
const checksum = (value) => createHash("sha256").update(value).digest("hex");
|
|
const manifest = {
|
|
version: 1,
|
|
baselineMigration: "00000000000000_hivegrid_baseline",
|
|
baselineChecksum: checksum("baseline"),
|
|
legacyMigrationCount: 2,
|
|
migrations: [
|
|
{ name: "20260101000000_first", checksum: checksum("first") },
|
|
{ name: "20260102000000_second", checksum: checksum("second") },
|
|
],
|
|
legacyChecksumVariants: [
|
|
{
|
|
name: "20260101000000_first",
|
|
checksum: checksum("first-historical"),
|
|
sourceCommit: "a".repeat(40),
|
|
},
|
|
],
|
|
};
|
|
|
|
const success = (name, rowChecksum) => ({ name, checksum: rowChecksum, finishedAt: new Date(), rolledBackAt: null });
|
|
const completeLegacyHistory = {
|
|
tableExists: true,
|
|
rows: manifest.migrations.map((migration) => success(migration.name, migration.checksum)),
|
|
};
|
|
const baselineHistory = {
|
|
tableExists: true,
|
|
rows: [success(manifest.baselineMigration, manifest.baselineChecksum)],
|
|
};
|
|
|
|
describe("classifyMigrationHistory", () => {
|
|
it("keeps a database without migration history on the fresh baseline path", () => {
|
|
expect(classifyMigrationHistory({ tableExists: false, rows: [] }, manifest).mode).toBe("fresh");
|
|
expect(classifyMigrationHistory({ tableExists: true, rows: [] }, manifest).mode).toBe("fresh");
|
|
});
|
|
|
|
it("recognizes the committed baseline and permits later migration rows", () => {
|
|
const history = {
|
|
tableExists: true,
|
|
rows: [...baselineHistory.rows, success("20270101000000_later", checksum("later"))],
|
|
};
|
|
expect(classifyMigrationHistory(history, manifest).mode).toBe("baseline");
|
|
});
|
|
|
|
it("accepts exactly one successful checksum-identical copy of every legacy migration", () => {
|
|
expect(classifyMigrationHistory(completeLegacyHistory, manifest)).toEqual({
|
|
mode: "legacy",
|
|
detail: "complete reviewed legacy chain",
|
|
variantNames: [],
|
|
});
|
|
});
|
|
|
|
it("accepts only an explicitly reviewed historical checksum and reports its migration", () => {
|
|
const history = {
|
|
tableExists: true,
|
|
rows: [success(manifest.migrations[0].name, manifest.legacyChecksumVariants[0].checksum), completeLegacyHistory.rows[1]],
|
|
};
|
|
expect(classifyMigrationHistory(history, manifest)).toEqual({
|
|
mode: "legacy",
|
|
detail: "complete reviewed legacy chain with historical checksums",
|
|
variantNames: [manifest.migrations[0].name],
|
|
});
|
|
});
|
|
|
|
it("accepts a checksum-verified exact legacy prefix for catch-up", () => {
|
|
expect(classifyMigrationHistory({ tableExists: true, rows: completeLegacyHistory.rows.slice(0, 1) }, manifest)).toEqual({
|
|
mode: "legacy-prefix",
|
|
detail: "checksum-verified legacy prefix",
|
|
variantNames: [],
|
|
appliedMigrationCount: 1,
|
|
missingMigrations: ["20260102000000_second"],
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
label: "gapped history",
|
|
history: { tableExists: true, rows: completeLegacyHistory.rows.slice(1) },
|
|
message: /not an exact legacy prefix/,
|
|
},
|
|
{
|
|
label: "duplicate successful history",
|
|
history: { tableExists: true, rows: [completeLegacyHistory.rows[0], completeLegacyHistory.rows[0]] },
|
|
message: /not an exact legacy prefix/,
|
|
},
|
|
{
|
|
label: "changed checksum",
|
|
history: { tableExists: true, rows: [success(manifest.migrations[0].name, checksum("changed")), completeLegacyHistory.rows[1]] },
|
|
message: /checksum differs/,
|
|
},
|
|
{
|
|
label: "foreign migration",
|
|
history: { tableExists: true, rows: [...completeLegacyHistory.rows, success("foreign", checksum("foreign"))] },
|
|
message: /outside the reviewed legacy chain/,
|
|
},
|
|
{
|
|
label: "unresolved failure",
|
|
history: {
|
|
tableExists: true,
|
|
rows: [...completeLegacyHistory.rows, { name: "failed", checksum: checksum("failed"), finishedAt: null, rolledBackAt: null }],
|
|
},
|
|
message: /unresolved failed migration/,
|
|
},
|
|
{
|
|
label: "changed baseline",
|
|
history: { tableExists: true, rows: [success(manifest.baselineMigration, checksum("changed"))] },
|
|
message: /baseline checksum/,
|
|
},
|
|
])("fails closed for $label", ({ history, message }) => {
|
|
expect(() => classifyMigrationHistory(history, manifest)).toThrow(message);
|
|
});
|
|
});
|
|
|
|
describe("adoptPrismaBaseline", () => {
|
|
it("does not resolve the baseline on a fresh database", async () => {
|
|
const resolve = vi.fn();
|
|
const stdout = { write: vi.fn() };
|
|
await expect(adoptPrismaBaseline({ manifest, inspect: async () => ({ tableExists: false, rows: [] }), resolve, stdout })).resolves.toMatchObject({
|
|
mode: "fresh",
|
|
});
|
|
expect(resolve).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("resolves a complete legacy chain once and verifies the resulting baseline row", async () => {
|
|
const inspect = vi.fn().mockResolvedValueOnce(completeLegacyHistory).mockResolvedValueOnce(baselineHistory);
|
|
const resolve = vi.fn();
|
|
const stdout = { write: vi.fn() };
|
|
await expect(adoptPrismaBaseline({ manifest, inspect, resolve, stdout })).resolves.toEqual({
|
|
mode: "adopted",
|
|
detail: "complete reviewed legacy chain",
|
|
});
|
|
expect(resolve).toHaveBeenCalledOnce();
|
|
expect(resolve).toHaveBeenCalledWith({ manifest });
|
|
expect(inspect).toHaveBeenCalledTimes(2);
|
|
});
|
|
|
|
it("catches up an exact legacy prefix before resolving the baseline", async () => {
|
|
const partialLegacyHistory = { tableExists: true, rows: completeLegacyHistory.rows.slice(0, 1) };
|
|
const inspect = vi.fn().mockResolvedValueOnce(partialLegacyHistory).mockResolvedValueOnce(completeLegacyHistory).mockResolvedValueOnce(baselineHistory);
|
|
const deployMissing = vi.fn();
|
|
const resolve = vi.fn();
|
|
const stdout = { write: vi.fn() };
|
|
|
|
await expect(adoptPrismaBaseline({ manifest, inspect, deployMissing, resolve, stdout })).resolves.toEqual({
|
|
mode: "adopted",
|
|
detail: "complete reviewed legacy chain",
|
|
});
|
|
|
|
expect(deployMissing).toHaveBeenCalledOnce();
|
|
expect(deployMissing).toHaveBeenCalledWith({
|
|
manifest,
|
|
missingMigrations: ["20260102000000_second"],
|
|
});
|
|
expect(deployMissing.mock.invocationCallOrder[0]).toBeLessThan(resolve.mock.invocationCallOrder[0]);
|
|
expect(inspect).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it("verifies security-sensitive catalog contracts before resolving a reviewed historical checksum", async () => {
|
|
const variantHistory = {
|
|
tableExists: true,
|
|
rows: [success(manifest.migrations[0].name, manifest.legacyChecksumVariants[0].checksum), completeLegacyHistory.rows[1]],
|
|
};
|
|
const inspect = vi.fn().mockResolvedValueOnce(variantHistory).mockResolvedValueOnce(baselineHistory);
|
|
const verifyCatalog = vi.fn();
|
|
const resolve = vi.fn();
|
|
|
|
await expect(adoptPrismaBaseline({ manifest, inspect, verifyCatalog, resolve, stdout: { write: vi.fn() } })).resolves.toMatchObject({
|
|
mode: "adopted",
|
|
});
|
|
|
|
expect(verifyCatalog).toHaveBeenCalledWith({ variantNames: [manifest.migrations[0].name] });
|
|
expect(verifyCatalog.mock.invocationCallOrder[0]).toBeLessThan(resolve.mock.invocationCallOrder[0]);
|
|
});
|
|
|
|
it("is a no-op after the baseline has already been recorded", async () => {
|
|
const resolve = vi.fn();
|
|
await expect(adoptPrismaBaseline({ manifest, inspect: async () => baselineHistory, resolve, stdout: { write: vi.fn() } })).resolves.toMatchObject({
|
|
mode: "baseline",
|
|
});
|
|
expect(resolve).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("deployMissingLegacyMigrations", () => {
|
|
it("runs Prisma migrate deploy with a writable temporary config and an absolute Prisma config import", () => {
|
|
const cleanup = vi.fn();
|
|
const prepareArchive = vi.fn(() => ({
|
|
migrationsPath: "/tmp/reviewed-legacy/prisma/migrations",
|
|
cleanup,
|
|
}));
|
|
const tempRoot = mkdtempSync(path.join(os.tmpdir(), "hivegrid-baseline-test-"));
|
|
let configPath;
|
|
let configSource;
|
|
const spawnImpl = vi.fn((_command, args) => {
|
|
configPath = args[args.indexOf("--config") + 1];
|
|
configSource = readFileSync(configPath, "utf8");
|
|
return { status: 0 };
|
|
});
|
|
|
|
try {
|
|
expect(() =>
|
|
deployMissingLegacyMigrations({
|
|
manifest,
|
|
missingMigrations: ["20260102000000_second"],
|
|
env: { DIRECT_URL: "postgresql://redacted" },
|
|
prepareArchive,
|
|
spawnImpl,
|
|
tempRoot,
|
|
})
|
|
).not.toThrow();
|
|
|
|
expect(prepareArchive).toHaveBeenCalledWith({ manifest });
|
|
expect(spawnImpl).toHaveBeenCalledWith(
|
|
process.execPath,
|
|
expect.arrayContaining(["migrate", "deploy", "--config"]),
|
|
expect.objectContaining({
|
|
env: { DIRECT_URL: "postgresql://redacted" },
|
|
stdio: "inherit",
|
|
})
|
|
);
|
|
expect(configPath).toMatch(new RegExp(`^${tempRoot.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}/hivegrid-prisma-config-`));
|
|
expect(configPath).not.toContain(REPOSITORY_ROOT);
|
|
expect(configSource).toContain(`file://${path.join(REPOSITORY_ROOT, "node_modules/prisma/config.js")}`);
|
|
expect(existsSync(configPath)).toBe(false);
|
|
expect(cleanup).toHaveBeenCalledOnce();
|
|
} finally {
|
|
rmSync(tempRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it("rejects anything other than the exact missing manifest suffix", () => {
|
|
expect(() =>
|
|
deployMissingLegacyMigrations({
|
|
manifest,
|
|
missingMigrations: ["20260101000000_first"],
|
|
env: { DIRECT_URL: "postgresql://redacted" },
|
|
prepareArchive: vi.fn(),
|
|
})
|
|
).toThrow(/exact missing manifest suffix/);
|
|
});
|
|
});
|
|
|
|
describe("resolveBaselineMigration", () => {
|
|
it("runs the installed Prisma CLI without a package manager", () => {
|
|
const spawnImpl = vi.fn(() => ({ status: 0 }));
|
|
resolveBaselineMigration({ manifest, env: { DIRECT_URL: "postgresql://redacted" }, spawnImpl });
|
|
expect(spawnImpl).toHaveBeenCalledWith(
|
|
process.execPath,
|
|
expect.arrayContaining(["migrate", "resolve", "--applied", manifest.baselineMigration, "--config", "prisma.config.ts"]),
|
|
expect.objectContaining({ stdio: "inherit" })
|
|
);
|
|
});
|
|
|
|
it("fails closed when Prisma resolve fails", () => {
|
|
expect(() => resolveBaselineMigration({ manifest, spawnImpl: () => ({ status: 1 }) })).toThrow(BaselineAdoptionError);
|
|
});
|
|
});
|
|
|
|
describe("verifyPrismaSchemaParity", () => {
|
|
it("compares the configured live datasource with the committed Prisma schema", () => {
|
|
const spawnImpl = vi.fn(() => ({ status: 0 }));
|
|
verifyPrismaSchemaParity({ env: { DIRECT_URL: "postgresql://redacted" }, spawnImpl });
|
|
expect(spawnImpl).toHaveBeenCalledWith(
|
|
process.execPath,
|
|
expect.arrayContaining(["migrate", "diff", "--from-config-datasource", "--to-schema", "prisma/schema", "--exit-code"]),
|
|
expect.objectContaining({ stdio: "inherit" })
|
|
);
|
|
});
|
|
|
|
it.each([
|
|
{ status: 1, message: /Could not verify/ },
|
|
{ status: 2, message: /differs from the reviewed current Prisma schema/ },
|
|
{ status: 3, message: /unsupported exit status 3/ },
|
|
])("fails closed for Prisma diff exit $status", ({ status, message }) => {
|
|
expect(() => verifyPrismaSchemaParity({ spawnImpl: () => ({ status }) })).toThrow(message);
|
|
});
|
|
});
|
|
|
|
describe("verifyLegacyVariantCatalog", () => {
|
|
it("does not open a database connection for variants without an extra raw-SQL contract", async () => {
|
|
const clientFactory = vi.fn();
|
|
await expect(verifyLegacyVariantCatalog({ variantNames: ["20260517200000_08_capacitor_device_tokens"], clientFactory })).resolves.toBeUndefined();
|
|
expect(clientFactory).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("fails closed when a security-sensitive historical variant does not match the live catalog", async () => {
|
|
const client = {
|
|
connect: vi.fn(),
|
|
query: vi.fn().mockResolvedValue({ rows: [{ matches: false }] }),
|
|
end: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
await expect(
|
|
verifyLegacyVariantCatalog({
|
|
variantNames: ["20260717090000_ak_p16b_booking_overlap_constraint"],
|
|
env: { DIRECT_URL: "postgresql://redacted" },
|
|
clientFactory: () => client,
|
|
})
|
|
).rejects.toThrow(/tenant-scoped booking overlap constraint/);
|
|
expect(client.end).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("accepts a security-sensitive historical variant only after the catalog contract passes", async () => {
|
|
const client = {
|
|
connect: vi.fn(),
|
|
query: vi.fn().mockResolvedValue({ rows: [{ matches: true }] }),
|
|
end: vi.fn().mockResolvedValue(undefined),
|
|
};
|
|
await expect(
|
|
verifyLegacyVariantCatalog({
|
|
variantNames: ["20260721093000_ak_p21_support_retention_purge"],
|
|
env: { DIRECT_URL: "postgresql://redacted" },
|
|
clientFactory: () => client,
|
|
})
|
|
).resolves.toBeUndefined();
|
|
expect(client.query).toHaveBeenCalledOnce();
|
|
});
|
|
});
|