Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/scripts/adopt-prisma-baseline.test.mjs
T

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();
});
});