feat(module-grants): Freigabestufe Verwalten – Datenbank, Zugriffsprüfung und Kantinen-Einstellungen
- Migration: ModuleGrant.level (USE/MANAGE), Bestand bleibt USE - ModuleAccessService.getModuleAccessLevels als einzige Auflösung, MANAGE gewinnt - @ModuleManage(slug) am ModuleGuard, GET /modules/active liefert canManage - Kantinenabrechnung: Einstellungen für Benutzer mit Verwalten, Web-Hook useCanManageModule Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -30,8 +30,8 @@ const BOUND_MODEL_NAMES = ['tenantModuleActivation', 'moduleGrant'];
|
||||
|
||||
function makeFakePrisma(opts: {
|
||||
activations?: { moduleId: string }[];
|
||||
directGrants?: { moduleId: string }[];
|
||||
groupGrants?: { moduleId: string }[];
|
||||
directGrants?: { moduleId: string; level?: string }[];
|
||||
groupGrants?: { moduleId: string; level?: string }[];
|
||||
} = {}) {
|
||||
const activations = opts.activations ?? [];
|
||||
const directGrants = opts.directGrants ?? [];
|
||||
@@ -250,6 +250,108 @@ describe('ModuleAccessService.getAccessibleModuleIds — USER (Grant-Auflösung,
|
||||
});
|
||||
});
|
||||
|
||||
describe('ModuleAccessService.getModuleAccessLevels — Freigabestufe (261002-icv)', () => {
|
||||
it('Direkt-Grant USE: Map {mod-1: USE}', async () => {
|
||||
const prisma = makeFakePrisma({
|
||||
activations: [{ moduleId: 'mod-1' }],
|
||||
directGrants: [{ moduleId: 'mod-1', level: 'USE' }],
|
||||
});
|
||||
const service = new ModuleAccessService(prisma as any);
|
||||
|
||||
const result = await service.getModuleAccessLevels('t1', 'user-1', 'USER');
|
||||
|
||||
expect(result).toEqual(new Map([['mod-1', 'USE']]));
|
||||
});
|
||||
|
||||
it('Direkt-Grant USE plus Gruppen-Grant MANAGE auf dasselbe Modul: MANAGE gewinnt (L-02)', async () => {
|
||||
const prisma = makeFakePrisma({
|
||||
activations: [{ moduleId: 'mod-1' }],
|
||||
directGrants: [{ moduleId: 'mod-1', level: 'USE' }],
|
||||
groupGrants: [{ moduleId: 'mod-1', level: 'MANAGE' }],
|
||||
});
|
||||
const service = new ModuleAccessService(prisma as any);
|
||||
|
||||
expect((await service.getModuleAccessLevels('t1', 'user-1', 'USER')).get('mod-1')).toBe('MANAGE');
|
||||
});
|
||||
|
||||
it('Gruppen-Grant USE plus Direkt-Grant MANAGE: ebenfalls MANAGE (Reihenfolge egal)', async () => {
|
||||
const prisma = makeFakePrisma({
|
||||
activations: [{ moduleId: 'mod-1' }],
|
||||
directGrants: [{ moduleId: 'mod-1', level: 'MANAGE' }],
|
||||
groupGrants: [{ moduleId: 'mod-1', level: 'USE' }],
|
||||
});
|
||||
const service = new ModuleAccessService(prisma as any);
|
||||
|
||||
expect((await service.getModuleAccessLevels('t1', 'user-1', 'USER')).get('mod-1')).toBe('MANAGE');
|
||||
});
|
||||
|
||||
it('MANAGE nur über eine Gruppe: MANAGE', async () => {
|
||||
const prisma = makeFakePrisma({
|
||||
activations: [{ moduleId: 'mod-1' }],
|
||||
groupGrants: [{ moduleId: 'mod-1', level: 'MANAGE' }],
|
||||
});
|
||||
const service = new ModuleAccessService(prisma as any);
|
||||
|
||||
expect((await service.getModuleAccessLevels('t1', 'user-1', 'USER')).get('mod-1')).toBe('MANAGE');
|
||||
});
|
||||
|
||||
it('MANAGE-Grant auf ein deaktiviertes Modul: fehlt in der Map (T-icv-05)', async () => {
|
||||
const prisma = makeFakePrisma({
|
||||
activations: [],
|
||||
directGrants: [{ moduleId: 'mod-1', level: 'MANAGE' }],
|
||||
});
|
||||
const service = new ModuleAccessService(prisma as any);
|
||||
|
||||
expect((await service.getModuleAccessLevels('t1', 'user-1', 'USER')).has('mod-1')).toBe(false);
|
||||
});
|
||||
|
||||
it.each(['ADMIN', 'SUPER_ADMIN'] as const)(
|
||||
'%s: jedes aktive Modul mit MANAGE, ohne Grant-Abfragen (L-03)',
|
||||
async (role) => {
|
||||
const prisma = makeFakePrisma({ activations: [{ moduleId: 'mod-1' }, { moduleId: 'mod-2' }] });
|
||||
const service = new ModuleAccessService(prisma as any);
|
||||
|
||||
const result = await service.getModuleAccessLevels('t1', 'a-1', role);
|
||||
|
||||
expect(result).toEqual(new Map([['mod-1', 'MANAGE'], ['mod-2', 'MANAGE']]));
|
||||
expect(prisma.moduleGrant.findMany).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
|
||||
it('ohne Grants: leere Map', async () => {
|
||||
const prisma = makeFakePrisma({ activations: [{ moduleId: 'mod-1' }] });
|
||||
const service = new ModuleAccessService(prisma as any);
|
||||
|
||||
expect(await service.getModuleAccessLevels('t1', 'user-1', 'USER')).toEqual(new Map());
|
||||
});
|
||||
|
||||
it('Zeilen ohne level-Feld (alte Mocks) zählen als USE', async () => {
|
||||
const prisma = makeFakePrisma({
|
||||
activations: [{ moduleId: 'mod-1' }],
|
||||
directGrants: [{ moduleId: 'mod-1' }],
|
||||
});
|
||||
const service = new ModuleAccessService(prisma as any);
|
||||
|
||||
expect((await service.getModuleAccessLevels('t1', 'user-1', 'USER')).get('mod-1')).toBe('USE');
|
||||
});
|
||||
|
||||
it('findAccessibleModules liefert canManage je Zeile', async () => {
|
||||
const prisma = makeFakePrisma({
|
||||
activations: [{ moduleId: 'mod-1' }, { moduleId: 'mod-2' }],
|
||||
directGrants: [
|
||||
{ moduleId: 'mod-1', level: 'MANAGE' },
|
||||
{ moduleId: 'mod-2', level: 'USE' },
|
||||
],
|
||||
});
|
||||
const service = new ModuleAccessService(prisma as any);
|
||||
|
||||
const rows = await service.findAccessibleModules('t1', 'user-1', 'USER');
|
||||
|
||||
expect(rows.find((r: any) => r.id === 'mod-1')?.canManage).toBe(true);
|
||||
expect(rows.find((r: any) => r.id === 'mod-2')?.canManage).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('ModuleAccessService.findAccessibleModules — ordering (PERM-04)', () => {
|
||||
it('sortiert die zugänglichen Module deterministisch nach Namen aufsteigend', async () => {
|
||||
const prisma = makeFakePrisma({
|
||||
|
||||
Reference in New Issue
Block a user