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.
58 lines
2.5 KiB
JavaScript
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);
|
|
});
|
|
});
|
|
});
|