Files
Marc-André Bombeck 756885eec0 fix(db-backup): take the pre-migration dump when pending migrations cannot be counted
Any error counting pending migrations (a connect or statement timeout, a
database still starting up) exited the step with "no dump before migrating",
and the migration that ran a second later went ahead without its way back.
An unknown count now takes the dump; a database that cannot be reached at
all then fails the dump, and the boot stops naming the switch that skips it.
2026-10-25 17:22:33 +01:00

71 lines
2.6 KiB
TypeScript

/**
* The pre-migration dump decides whether to take a dump from the count of
* pending migrations. A count that fails (a connect or statement timeout, a
* database still starting up) used to read as "nothing pending": the step
* exited 0 with no dump, and the migration a second later ran without its
* way back. "Cannot tell" now takes the dump.
*
* The database is a stand-in: `createDatabaseDump` is a spy, `connect`
* answers the two facts the step reads (an applied migration, some tables).
*/
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
const pendingMigrations = vi.fn<() => Promise<string[]>>();
const createDatabaseDump = vi.fn(async () => ({
dumpPath: "/b/healthlog-20261025T013000Z-pre-migration.dump",
manifest: { dump: { bytes: 1024 }, rowCounts: { "public.users": 1 } },
}));
vi.mock("../pending-migrations.mjs", () => ({
pendingMigrations: () => pendingMigrations(),
}));
vi.mock("../../src/lib/database-backup/dump-core.mjs", async (original) => ({
...(await original<Record<string, unknown>>()),
connect: async () => ({
query: async () => ({ rows: [] }),
end: async () => {},
}),
latestMigration: async () => "0488_account_deletion_grace",
listUserTables: async () => ["public.users"],
createDatabaseDump: () => createDatabaseDump(),
applyRetention: async () => [],
}));
import { preMigrationDump } from "../db-dump.mjs";
const env = {
DATABASE_URL: "postgresql://u:p@db:5432/healthlog",
DB_BACKUP_DIR: "/b",
};
const kept = () =>
({ writable: true, persistent: true, reason: "volume" }) as const;
beforeEach(() => {
vi.spyOn(console, "log").mockImplementation(() => {});
vi.spyOn(console, "error").mockImplementation(() => {});
createDatabaseDump.mockClear();
});
afterEach(() => vi.restoreAllMocks());
describe("the dump before migrating", () => {
it("takes the dump when the pending migrations cannot be counted", async () => {
pendingMigrations.mockRejectedValue(
new Error("the database system is starting up"),
);
expect(await preMigrationDump(env, { directoryStatus: kept })).toBe(0);
expect(createDatabaseDump).toHaveBeenCalledTimes(1);
});
it("takes none when nothing is pending", async () => {
pendingMigrations.mockResolvedValue([]);
expect(await preMigrationDump(env, { directoryStatus: kept })).toBe(0);
expect(createDatabaseDump).not.toHaveBeenCalled();
});
it("takes one before pending migrations", async () => {
pendingMigrations.mockResolvedValue(["0489_database_backup_run_key"]);
expect(await preMigrationDump(env, { directoryStatus: kept })).toBe(0);
expect(createDatabaseDump).toHaveBeenCalledTimes(1);
});
});