Files
edr-platform/apps/edr-freight-api/src/seed/freight-permissions.registry.spec.ts
Nathnael d5d7c91e24 feat(auth): add <module>:read for API access without UI exposure
`<module>:view` gates the backoffice sidebar entry, the route, and the API
read all at once, so granting a user another module's list endpoint for a form
dropdown also hands them that module's whole page.

Seed a `:read` twin for every `:view` key and teach the freight guards to
accept it wherever the matching `:view` is required — on GET/HEAD/OPTIONS
only, since class and method guards AND together and a write route without its
own method gate would otherwise be reachable. The frontend never checks
`:read`, which is what keeps the module hidden.

Twins are derived, not hand-written, so a new `:view` gets one for free.
Grants stay hand-curated in iam.position_type_permissions.
2026-08-07 12:09:11 +00:00

51 lines
2.0 KiB
TypeScript

import { EDR_FREIGHT_PERMISSIONS } from './edr-freight.seed';
import { readTwinOf } from './freight-permissions.registry';
describe('EDR_FREIGHT_PERMISSIONS', () => {
// The seeder inserts the whole catalog in one ON CONFLICT (key) DO UPDATE
// statement — a duplicated key there is a Postgres 21000 at boot, not a
// silent no-op.
it('has no duplicate keys', () => {
const keys = EDR_FREIGHT_PERMISSIONS.map((permission) => permission.key);
const duplicates = [...new Set(keys.filter((key, i) => keys.indexOf(key) !== i))];
expect(duplicates).toEqual([]);
});
// The `:read` ids are derived rather than hand-written, so a collision
// would surface here instead of as a primary-key violation on a fresh
// database.
it('has no duplicate ids', () => {
const ids = EDR_FREIGHT_PERMISSIONS.map((permission) => permission.id);
const duplicates = [...new Set(ids.filter((id, i) => ids.indexOf(id) !== i))];
expect(duplicates).toEqual([]);
});
// `:read` grants a module's GET routes without putting it in the backoffice
// sidebar. Every `:view` needs its twin or that module has no way to be
// granted API-only access.
it('gives every :view key a :read twin', () => {
const keys = new Set(EDR_FREIGHT_PERMISSIONS.map((p) => p.key));
const missing = [...keys]
.filter((key) => key.endsWith(':view'))
.map((key) => readTwinOf(key))
.filter((twin): twin is string => twin !== null && !keys.has(twin));
expect(missing).toEqual([]);
});
it('mints every :read id in the v5 block', () => {
const reads = EDR_FREIGHT_PERMISSIONS.filter((p) => p.key.endsWith(':read'));
expect(reads.length).toBeGreaterThan(0);
// Version nibble 5 — the source `:view` ids are all v4, so the two sets
// cannot overlap however many keys are added.
for (const read of reads) {
expect(read.id).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-5[0-9a-f]{3}-[0-9a-f]{4}-[0-9a-f]{12}$/,
);
}
});
});