Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
220 changes: 78 additions & 142 deletions tests/supabase-schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,22 +120,6 @@
new URL("../supabase/migrations/20260713102000_revoke_supabase_admin_default_privileges.sql", import.meta.url),
"utf8",
).replace(/\s+/g, " ");
const publicationApprovalMigration = readFileSync(
new URL("../supabase/migrations/20260717131000_guard_document_publication_approval.sql", import.meta.url),
"utf8",
).replace(/\s+/g, " ");
const deleteDocumentIfIdleMigration = readFileSync(
new URL("../supabase/migrations/20260717132000_delete_document_if_idle.sql", import.meta.url),
"utf8",
).replace(/\s+/g, " ");
const defaultAclAssertionMigration = readFileSync(
new URL("../supabase/migrations/20260717161000_assert_supabase_admin_default_privileges.sql", import.meta.url),
"utf8",
).replace(/\s+/g, " ");
const defaultAclRoleBootstrap = readFileSync(new URL("../supabase/roles.sql", import.meta.url), "utf8").replace(
/\s+/g,
" ",
);
const scrubLegacyQueryTextMigration = readFileSync(
new URL("../supabase/migrations/20260713103000_scrub_legacy_rag_query_text.sql", import.meta.url),
"utf8",
Expand Down Expand Up @@ -192,13 +176,6 @@
new URL("../supabase/migrations/20260705133000_tighten_search_document_chunks_owner_scope.sql", import.meta.url),
"utf8",
).replace(/\s+/g, " ");
const searchDocumentChunksCommittedGenerationMigration = readFileSync(
new URL(
"../supabase/migrations/20260717130000_filter_search_document_chunks_committed_generation.sql",
import.meta.url,
),
"utf8",
).replace(/\s+/g, " ");
const retrievalPlanCacheMigration = readFileSync(
new URL("../supabase/migrations/20260711120000_retrieval_fn_plan_cache_mode.sql", import.meta.url),
"utf8",
Expand Down Expand Up @@ -957,39 +934,6 @@
);
});

it("filters per-document search to the committed generation before matching and limiting", () => {
for (const sql of [schema, searchDocumentChunksCommittedGenerationMigration]) {
const functionStart = sql.indexOf("create or replace function public.search_document_chunks(");
const functionEnd = sql.indexOf("$$;", functionStart);
const definition = sql.slice(functionStart, functionEnd);
const generationFilter = definition.indexOf(
"public.is_committed_document_generation(c.index_generation_id, d.index_generation_id)",
);

expect(generationFilter).toBeGreaterThanOrEqual(0);
expect(generationFilter).toBeLessThan(definition.indexOf("c.search_tsv @@ normalized.query_tsv"));
expect(generationFilter).toBeLessThan(definition.indexOf("limit least(greatest(match_count, 1), 80)"));
}

const g0 = "00000000-0000-0000-0000-000000000001";
const g1 = "00000000-0000-0000-0000-000000000002";
const candidates = [
{ id: "committed", chunkGeneration: g0, documentGeneration: g0, rank: 0.5 },
{ id: "staged-higher-rank", chunkGeneration: g1, documentGeneration: g0, rank: 0.99 },
{ id: "legacy-null", chunkGeneration: null, documentGeneration: null, rank: 0.4 },
];
const visible = candidates
.filter(
(row) =>
row.chunkGeneration === row.documentGeneration &&
(row.chunkGeneration !== null || row.documentGeneration === null),
)
.sort((left, right) => right.rank - left.rank)
.map((row) => row.id);

expect(visible).toEqual(["committed", "legacy-null"]);
});

it("surfaces stale commit generation RPCs through search_schema_health", () => {
for (const sql of [schema, searchSchemaHealthM13GuardMigration]) {
expect(sql).toContain("commit_fn_def := pg_get_functiondef(");
Expand Down Expand Up @@ -1137,92 +1081,6 @@
);
});

it("requires append-only operator evidence for owned-to-public transitions", () => {
for (const sql of [schema, publicationApprovalMigration]) {
expect(sql).toContain("create table if not exists public.document_publication_approvals");
expect(sql).toContain("check (cardinality(evidence_references) > 0)");
expect(sql).toContain("unique (document_id, expected_prior_owner_id, manifest_digest)");
expect(sql).toContain("before update or delete on public.document_publication_approvals");
expect(sql).toContain("before insert or update on public.documents");
expect(sql).toContain("if tg_op = 'INSERT' then");
expect(sql).toContain("public documents must be created as owned rows before approved publication");
expect(sql).toContain("old.owner_id is not null and new.owner_id is null");
expect(sql).toContain("approval.expected_prior_owner_id = old.owner_id");
expect(sql).toContain("create or replace function public.publish_approved_documents(");
expect(sql).toContain("for update;");
expect(sql).toContain(
"grant execute on function public.publish_approved_documents(jsonb, text, integer) to service_role;",
);
}
});

it("serializes permanent deletion against ingestion job creation", () => {
for (const sql of [schema, deleteDocumentIfIdleMigration]) {
const functionStart = sql.indexOf("create or replace function public.delete_document_if_idle(");
const functionBody = sql.slice(functionStart, sql.indexOf("$$;", functionStart));
const rowLock = functionBody.indexOf("for update;");
const activeJobCheck = functionBody.indexOf("from public.ingestion_jobs j");
const ledgerInsert = functionBody.indexOf("insert into public.storage_cleanup_jobs");
const parentDelete = functionBody.indexOf("delete from public.documents");
expect(rowLock).toBeGreaterThanOrEqual(0);
expect(rowLock).toBeLessThan(activeJobCheck);
expect(activeJobCheck).toBeLessThan(ledgerInsert);
expect(ledgerInsert).toBeLessThan(parentDelete);
expect(sql).toContain(
"grant execute on function public.delete_document_if_idle(uuid, uuid, text, text) to service_role;",
);
const retryStart = sql.indexOf("create or replace function public.retry_ingestion_job_if_idle(");
const retryBody = sql.slice(retryStart, sql.indexOf("$$;", retryStart));
expect(retryBody).toContain("for update of d, j;");
expect(retryBody.indexOf("for update of d, j;")).toBeLessThan(retryBody.indexOf("update public.ingestion_jobs"));
expect(retryBody).toContain("update public.documents");
expect(sql).toContain(
"grant execute on function public.retry_ingestion_job_if_idle(uuid, uuid, timestamptz, integer, timestamptz, timestamptz) to service_role;",
);
}
});

it("fails closed on effective supabase_admin default ACLs", () => {
for (const sql of [schema, defaultAclAssertionMigration]) {
expect(sql).toContain("create or replace function public.default_privileges_status(");
expect(sql).toContain("pg_catalog.acldefault(ot.object_code, v_role_oid)");
expect(sql).toContain("pg_catalog.aclexplode(ea.acl)");
expect(sql).toContain("bool_or(grantee not in (p_role_name, 'postgres', 'service_role'))");
expect(sql).toContain("entry like 'table:PUBLIC:%'");
expect(sql).toContain("entry like 'sequence:PUBLIC:%'");
expect(sql).toContain("entry = 'function:PUBLIC:execute'");
expect(sql).toContain("message = 'Unsafe supabase_admin default privileges; migration blocked.'");
expect(sql).toContain("Run these six statements as supabase_admin, then retry the migration:");
}

const migrationFiles = readdirSync(migrationDirectoryUrl)
.filter((fileName) => /^\d+_.+\.sql$/.test(fileName))
.sort();
expect(migrationFiles.at(-1)).toBe("20260717161000_assert_supabase_admin_default_privileges.sql");
});

it("bootstraps safe default ACLs before fresh local and preview migration replay", () => {
expect(defaultAclRoleBootstrap).toContain(
"alter default privileges for role supabase_admin revoke all privileges on tables from public, anon, authenticated, service_role;",
);
expect(defaultAclRoleBootstrap).toContain(
"alter default privileges for role supabase_admin revoke all privileges on sequences from public, anon, authenticated, service_role;",
);
expect(defaultAclRoleBootstrap).toContain(
"alter default privileges for role supabase_admin revoke execute on functions from public, anon, authenticated, service_role;",
);
expect(defaultAclRoleBootstrap).toContain(
"alter default privileges for role supabase_admin in schema public grant select, insert, update, delete on tables to service_role;",
);
expect(defaultAclRoleBootstrap).toContain(
"alter default privileges for role supabase_admin in schema public grant usage, select on sequences to service_role;",
);
expect(defaultAclRoleBootstrap).toContain(
"alter default privileges for role supabase_admin in schema public grant execute on functions to service_role;",
);
expect(defaultAclRoleBootstrap).toContain("bool_or(grantee not in ('supabase_admin', 'postgres', 'service_role'))");
});

it("scrubs legacy plaintext query text with salted irreversible placeholders", () => {
// 2026-07-13 audit finding 5: rows written before the HMAC rollout still
// held raw clinical query text. Placeholders must be salted (not bare
Expand Down Expand Up @@ -1381,3 +1239,81 @@
);
});
});

describe("Storage buckets migration chain (schema-drift fix)", () => {
// 20260527000000_bulk_ingestion.sql created storage.objects RLS policies that
// reference clinical-documents/clinical-images, but only schema.sql (not the
// migration chain) ever created the buckets themselves. A database built purely
// by replaying migrations therefore had policies pointing at buckets that did
// not exist. This migration backfills the bucket creation into the chain.
const createStorageBucketsMigration = readFileSync(

Check failure on line 1249 in tests/supabase-schema.test.ts

View workflow job for this annotation

GitHub Actions / Unit coverage

[node] tests/supabase-schema.test.ts

Error: ENOENT: no such file or directory, open '/home/runner/work/Database/Database/supabase/migrations/20260717130000_create_storage_buckets.sql' ❯ tests/supabase-schema.test.ts:1249:41 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Serialized Error: { errno: -2, code: 'ENOENT', syscall: 'open', path: '/home/runner/work/Database/Database/supabase/migrations/20260717130000_create_storage_buckets.sql' }
new URL("../supabase/migrations/20260717130000_create_storage_buckets.sql", import.meta.url),
"utf8",
).replace(/\s+/g, " ");

it("creates the private clinical-documents bucket with schema.sql's size limit and mime allowlist", () => {
expect(createStorageBucketsMigration).toContain(
"insert into storage.buckets (id, name, public, file_size_limit, allowed_mime_types)",
);
expect(createStorageBucketsMigration).toContain("'clinical-documents', 'clinical-documents', false, 157286400,");
expect(createStorageBucketsMigration).toContain("'application/pdf'");
expect(createStorageBucketsMigration).toContain(
"'application/vnd.openxmlformats-officedocument.wordprocessingml.document'",
);
expect(createStorageBucketsMigration).toContain(
"'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'",
);
expect(createStorageBucketsMigration).toContain("'text/plain'");
});

it("creates the private clinical-images bucket with schema.sql's size limit and mime allowlist", () => {
expect(createStorageBucketsMigration).toContain("'clinical-images', 'clinical-images', false, 52428800,");
expect(createStorageBucketsMigration).toContain("array['image/png', 'image/jpeg', 'image/webp']");
});

it("uses byte-precise size limits (150 MB documents, 50 MB images)", () => {
expect(createStorageBucketsMigration).toContain(String(150 * 1024 * 1024));
expect(createStorageBucketsMigration).toContain(String(50 * 1024 * 1024));
});

it("upserts idempotently, re-affirming public = false on conflict for both buckets", () => {
const marker = "on conflict (id) do update set public = false;";
const onConflictCount = createStorageBucketsMigration.split(marker).length - 1;
expect(onConflictCount).toBe(2);
const insertCount = createStorageBucketsMigration.split("insert into storage.buckets").length - 1;
expect(insertCount).toBe(2);
});

it("never flips a bucket public (regression: on-conflict must not drop the private ACL)", () => {
expect(createStorageBucketsMigration).not.toContain("public = true");
expect(createStorageBucketsMigration).not.toContain(
"'clinical-documents', 'clinical-documents', true,",
);
expect(createStorageBucketsMigration).not.toContain("'clinical-images', 'clinical-images', true,");
});

it("mirrors schema.sql's storage.buckets inserts byte-for-byte", () => {
const extractBucketInserts = (text: string) => {
const marker = "on conflict (id) do update set public = false;";
const start = text.indexOf("insert into storage.buckets");
expect(start, "storage.buckets insert not found").toBeGreaterThanOrEqual(0);
const firstEnd = text.indexOf(marker, start);
expect(firstEnd, "first bucket upsert not found").toBeGreaterThan(start);
const secondStart = text.indexOf("insert into storage.buckets", firstEnd);
expect(secondStart, "second bucket insert not found").toBeGreaterThan(firstEnd);
const secondEnd = text.indexOf(marker, secondStart);
expect(secondEnd, "second bucket upsert not found").toBeGreaterThan(secondStart);
return text.slice(start, secondEnd + marker.length);
};
expect(extractBucketInserts(createStorageBucketsMigration)).toBe(extractBucketInserts(schema));
});

it("creates the exact bucket ids the storage.objects RLS policies depend on", () => {
// Regression guard for the drift bug itself: the policy migration references
// these bucket_ids, so the bucket-creation migration must define the same ones.
expect(bulkIngestionMigration).toContain("bucket_id = 'clinical-documents'");
expect(bulkIngestionMigration).toContain("bucket_id = 'clinical-images'");
expect(createStorageBucketsMigration).toContain("'clinical-documents',");
expect(createStorageBucketsMigration).toContain("'clinical-images',");
});
});
Loading