fix(test): reject custom pg-prefixed restore schemas

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-11-02 13:02:25 -06:00
1 parent 7a43aa31b9
commit 14ee3f02c4
7 files changed
+49 -8

No files matched your search

+1 -1
View File
@@ -31,7 +31,7 @@ export async function assertDistinctEmptyRestore(pair: QualificationPair) {
const [result] = await target`SELECT pg_try_advisory_lock(${a}, ${b}) AS acquired`;
targetLocked = result!.acquired === true;
if (!targetLocked) throw new Error("Backup source and restore resolve to the same actual database");
const rows = await target`SELECT 1 FROM pg_namespace WHERE nspname NOT IN ('public', 'information_schema') AND nspname NOT LIKE 'pg_%'
const rows = await target`SELECT 1 FROM pg_namespace WHERE nspname NOT IN ('public', 'information_schema') AND nspname !~ '^pg_'
UNION ALL SELECT 1 FROM pg_depend
WHERE refclassid = 'pg_namespace'::regclass AND refobjid = 'public'::regnamespace LIMIT 1`;
if (rows.length) throw new Error("Backup restore target must be empty before restore");
@@ -19,6 +19,7 @@ const pair = configuredPair;
describe("real automatic backup checkpoint qualification", () => {
let directory: string;
let source: ReturnType<typeof createDb>;
let ownsRestoreFixture = false;
const logger = { info() {}, warn() {}, error() {} };
const receipts: unknown[] = [];
const owner = () => createDatabaseBackupOwner({ connectionString: pair.sourceUrl, backupDir: directory, getRetention: async () => retention, logger });
@@ -29,6 +30,7 @@ describe("real automatic backup checkpoint qualification", () => {
directory = await mkdtemp(path.join(parent, "backup-checkpoint-qualification-"));
source = createDb(pair.sourceUrl, { maxConnections: 1 });
await assertDistinctEmptyRestore(pair);
ownsRestoreFixture = true;
await seedBackupQualificationFixture(pair.sourceUrl);
}, 120_000);
afterAll(async () => {
@@ -37,7 +39,8 @@ describe("real automatic backup checkpoint qualification", () => {
if (receiptFile) await writeFile(receiptFile, JSON.stringify({ version: 1, cases: receipts, directory,
sourceDatabase: new URL(pair.sourceUrl).pathname.slice(1), restoreDatabase: new URL(pair.restoreUrl).pathname.slice(1) }, null, 2));
} finally {
await source?.$client.end({ timeout: 5 });
try { if (ownsRestoreFixture) await emptyRestore(); }
finally { await source?.$client.end({ timeout: 5 }); }
if (directory && !process.env.PAPERCLIP_BACKUP_QUALIFICATION_RECEIPT) await rm(directory, { recursive: true, force: true });
}
});
@@ -45,9 +48,9 @@ describe("real automatic backup checkpoint qualification", () => {
const target = createDb(pair.restoreUrl, { maxConnections: 1 }).$client;
try {
const tables = await target`SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r', 'p') AND n.nspname <> 'information_schema' AND n.nspname NOT LIKE 'pg_%'`;
WHERE c.relkind IN ('r', 'p') AND n.nspname <> 'information_schema' AND n.nspname !~ '^pg_'`;
for (const { nspname, relname } of tables) await target`DROP TABLE IF EXISTS ${target(nspname)}.${target(relname)} CASCADE`;
const schemas = await target`SELECT nspname FROM pg_namespace WHERE nspname NOT IN ('information_schema') AND nspname NOT LIKE 'pg_%'`;
const schemas = await target`SELECT nspname FROM pg_namespace WHERE nspname NOT IN ('information_schema') AND nspname !~ '^pg_'`;
for (const { nspname } of schemas) await target`DROP SCHEMA ${target(nspname)} CASCADE`;
await target`CREATE SCHEMA public`;
} finally { await target.end({ timeout: 5 }); }
@@ -0,0 +1,36 @@
import { mkdtemp, readdir, rm } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { createDb } from "@paperclipai/db";
import { describe, expect, it } from "vitest";
import { assertDistinctEmptyRestore, qualifyBackupRestore, resolveBackupQualificationPair } from "../../scripts/lib/backup-qualification.js";
const configuredPair = resolveBackupQualificationPair();
if (!configuredPair) throw new Error("Backup guard qualification requires an explicit dedicated source/restore test URL pair");
const pair = configuredPair;
describe("backup restore schema guard", () => {
it("refuses a populated pgdata schema without backup or restore and preserves its rows", async () => {
const target = createDb(pair.restoreUrl, { maxConnections: 1 }).$client;
const directory = await mkdtemp(path.join(os.tmpdir(), "backup-restore-guard-"));
let createdSchema = false;
try {
await assertDistinctEmptyRestore(pair);
await target`CREATE SCHEMA pgdata`;
createdSchema = true;
await target`CREATE TABLE pgdata.retained (id integer PRIMARY KEY, value text NOT NULL)`;
await target`INSERT INTO pgdata.retained VALUES (1, 'keep this existing target data')`;
const guardFailure = await assertDistinctEmptyRestore(pair).then(() => undefined, error => error);
expect(await target`SELECT id, value FROM pgdata.retained`).toEqual([{ id: 1, value: "keep this existing target data" }]);
expect(await readdir(directory)).toEqual([]);
expect(guardFailure).toBeInstanceOf(Error);
expect(guardFailure.message).toBe("Backup restore target must be empty before restore");
await expect(qualifyBackupRestore(pair, directory, "pg_dump")).rejects.toThrow("Backup restore target must be empty before restore");
expect(await target`SELECT id, value FROM pgdata.retained`).toEqual([{ id: 1, value: "keep this existing target data" }]);
expect(await readdir(directory)).toEqual([]);
} finally {
try { if (createdSchema) await target`DROP SCHEMA pgdata CASCADE`; }
finally { await target.end({ timeout: 5 }); await rm(directory, { recursive: true, force: true }); }
}
});
});
+4 -1
View File
@@ -4,7 +4,10 @@ export default defineConfig({
...base,
test: {
...base.test,
include: ["src/__tests__/database-backup-checkpoint-qualification.integration.ts"],
include: [
"src/__tests__/database-backup-checkpoint-qualification.integration.ts",
"src/__tests__/database-backup-restore-guard.integration.ts",
],
fileParallelism: false,
maxWorkers: 1,
},