Files
Void-Homelab/tests/db/migration_003.test.js
root 6086cf9a7a fix(schema): tighten tenant boundaries on resources/deps/creds/source_docs
Apply same composite-FK pattern as 001/002 for migration 003:
- resources: add UNIQUE (id, space_id) as FK target.
- resource_dependencies: denormalize space_id, composite FKs on both endpoints
  (enforces both ends of a dep live in the same space at the DB layer).
- resource_credentials: denormalize space_id, composite FK to resources.
- source_docs.resource_id: NOT NULL (tenancy anchor); resource_id was already
  absent from the update FIELDS list so docs cannot move resources.

Repos derive space_id from the resource on addDependency/addCredential so callers
can't fake cross-tenant assignment. 3 regression tests added.
2026-05-31 10:33:17 +10:00

58 lines
2.5 KiB
JavaScript

import { describe, it, expect, beforeEach } from 'vitest';
import { resetDb, withClient } from '../helpers/db.js';
import { migrateUp } from '../../lib/db/migrate.js';
describe('migration 003 — resources', () => {
beforeEach(async () => { await resetDb(); await migrateUp(); });
it('creates resources, resource_dependencies, resource_credentials, source_docs', async () => {
await withClient(async (c) => {
for (const t of ['resources','resource_dependencies','resource_credentials','source_docs']) {
const { rows } = await c.query(
`SELECT to_regclass('public.' || $1) AS t;`, [t]
);
expect(rows[0].t).toBe(t);
}
});
});
it('resource_dependencies rejects cross-space endpoints', async () => {
await withClient(async (c) => {
const { rows: [s1] } = await c.query(`INSERT INTO spaces(slug,name) VALUES('a','A') RETURNING id;`);
const { rows: [s2] } = await c.query(`INSERT INTO spaces(slug,name) VALUES('b','B') RETURNING id;`);
const { rows: [a] } = await c.query(
`INSERT INTO resources(space_id, slug, name, runtime_type) VALUES($1,'a','A','lxc') RETURNING id;`,
[s1.id]);
const { rows: [b] } = await c.query(
`INSERT INTO resources(space_id, slug, name, runtime_type) VALUES($1,'b','B','lxc') RETURNING id;`,
[s2.id]);
await expect(c.query(
`INSERT INTO resource_dependencies(resource_id, depends_on, space_id) VALUES($1,$2,$3);`,
[a.id, b.id, s1.id]
)).rejects.toThrow(/foreign key/i);
});
});
it('resource_credentials rejects assignment to a resource in another space', async () => {
await withClient(async (c) => {
const { rows: [s1] } = await c.query(`INSERT INTO spaces(slug,name) VALUES('a','A') RETURNING id;`);
const { rows: [s2] } = await c.query(`INSERT INTO spaces(slug,name) VALUES('b','B') RETURNING id;`);
const { rows: [r] } = await c.query(
`INSERT INTO resources(space_id, slug, name, runtime_type) VALUES($1,'r','R','lxc') RETURNING id;`,
[s1.id]);
await expect(c.query(
`INSERT INTO resource_credentials(resource_id, space_id, label, vault_path) VALUES($1,$2,'l','env:X');`,
[r.id, s2.id]
)).rejects.toThrow(/foreign key/i);
});
});
it('source_docs.resource_id is NOT NULL', async () => {
await withClient(async (c) => {
await expect(c.query(
`INSERT INTO source_docs(name, upstream_url) VALUES('n','https://x');`
)).rejects.toThrow(/not.null|null value/i);
});
});
});