import { describe, it, expect } from 'vitest' import { validate } from '../validate.js' /** * permissionPrefix contract — the guard against the historical "every * role-based user 403s" bug: an explicit prefix without the leading * applicationCode segment compiles into permission constants no seeded * 4-segment grant ({app}.{module}.{section}.{action}) can ever match, * because the platform permission match is EXACT. The schema enforces * (a) the kebab 3-4 segment shape and (b) the cross-field app prefix. */ function spec(overrides: Record = {}): Record { return { name: 'Budget', pluralName: 'Budgets', module: 'budgets', section: 'budgets', appCode: 'TestV2', applicationCode: 'crm', namespace: 'TestV2', navRoute: 'budgets.budgets', fields: [{ name: 'code', type: 'string', required: true }], projectPath: '/tmp/project', ...overrides, } } describe('scaffold-controller / validate — permissionPrefix cross-field guard', () => { it('accepts a spec WITHOUT permissionPrefix (the nominal path — prefix is derived)', () => { const r = validate(spec()) expect(r.valid).toBe(true) expect(r.errors).toEqual([]) }) it('accepts a 3-segment prefix carrying the applicationCode', () => { const r = validate(spec({ permissionPrefix: 'crm.budgets.budgets' })) expect(r.valid).toBe(true) }) it('accepts a 4-segment resource-grained prefix carrying the applicationCode', () => { const r = validate(spec({ permissionPrefix: 'crm.budgets.budgets.lignes' })) expect(r.valid).toBe(true) }) it('rejects the bug signature — a prefix without the app segment', () => { const r = validate(spec({ permissionPrefix: 'budgets.budgets' })) expect(r.valid).toBe(false) // 2 segments fails the shape regex first (zod skips superRefine when the // base object parse fails) — the issue is still anchored on the field. expect(r.errors.some(e => e.startsWith('[permissionPrefix]'))).toBe(true) }) it('rejects a well-shaped 3-segment prefix carrying the WRONG app', () => { const r = validate(spec({ permissionPrefix: 'hr.budgets.budgets' })) expect(r.valid).toBe(false) expect(r.errors.some(e => e.includes('must start with "crm."'))).toBe(true) }) it('rejects malformed shapes: 2 segments, 5 segments, PascalCase', () => { for (const bad of ['crm.budgets', 'crm.budgets.budgets.lignes.extra', 'Crm.Budgets.Budgets']) { const r = validate(spec({ permissionPrefix: bad })) expect(r.valid, bad).toBe(false) expect(r.errors.some(e => e.includes('3-4 segments')), bad).toBe(true) } }) it('accepts kebab-case navRoute segments (jours-feries, order-lines)', () => { const r = validate(spec({ navRoute: 'orders.order-lines' })) expect(r.valid).toBe(true) }) })