feat(quick-261003-387): Kategorien-API - Anlegen, Sortieren, Zuordnen, Loeschen mit Verschieben, Ueberlagerung in allen Modullisten

- Verwaltungs-Endpunkte nur fuer Administratoren, statische Routen vor :key
- Loeschen verschiebt alle Eintraege inkl. persoenlicher eigener Module in einer Transaktion, ohne Ziel 409
- /modules, /modules/catalog und /module-grants/matrix liefern wirksame Kategorie und Reihenfolge
- eigene Module pruefen die Kategorie gegen die Organisation (400 bei unbekannter)
- Zugriffsinventar nachgezogen

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-03 02:39:36 +02:00
parent 8ec116c75a
commit af1878d5ee
15 changed files with 1152 additions and 37 deletions
@@ -1,4 +1,4 @@
import { ForbiddenException, NotFoundException } from '@nestjs/common';
import { BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common';
import { Role } from '@prisma/client';
import { describe, expect, it, vi } from 'vitest';
@@ -51,9 +51,18 @@ const admin = { id: 'admin1', role: Role.ADMIN };
const userA = { id: 'ua', role: Role.USER };
const userB = { id: 'ub', role: Role.USER };
// Kategorien-Dienst (quick-261003-387): die Organisation fuehrt eine feste
// Menge von Kennungen; eine andere lehnt assertCategoryKey mit 400 ab.
const KNOWN_CATEGORIES = new Set(['fleet', 'infrastructure', 'custom-modules', 'neu-angelegt']);
function setup() {
const prisma = makeFakePrisma();
return { prisma, service: new CustomModulesService(prisma as any) };
const categories = {
assertCategoryKey: vi.fn(async (_tenantId: string, key: string) => {
if (!KNOWN_CATEGORIES.has(key)) throw new BadRequestException('Unbekannte Kategorie');
}),
};
return { prisma, categories, service: new CustomModulesService(prisma as any, categories as any) };
}
describe('CustomModulesService — anlegen', () => {
@@ -106,6 +115,40 @@ describe('CustomModulesService — anlegen', () => {
});
});
describe('CustomModulesService — Kategorie gegen die Organisation pruefen', () => {
it('create: eine vorhandene Kennung (auch eine neu angelegte) ist erlaubt', async () => {
const { categories, service } = setup();
const res: any = await service.create('t1', userA, { ...dto, category: 'neu-angelegt' });
expect(res.category).toBe('neu-angelegt');
expect(categories.assertCategoryKey).toHaveBeenCalledWith('t1', 'neu-angelegt');
});
it('create: eine unbekannte Kategorie -> 400, nichts gespeichert', async () => {
const { prisma, service } = setup();
await expect(
service.create('t1', userA, { ...dto, category: 'gibtsnicht' }),
).rejects.toBeInstanceOf(BadRequestException);
expect(prisma.customModule.create).not.toHaveBeenCalled();
});
it('update: eine unbekannte Kategorie -> 400, der Eintrag bleibt unveraendert', async () => {
const { prisma, service } = setup();
const created: any = await service.create('t1', userA, dto);
await expect(
service.update('t1', userA, created.id, { category: 'gibtsnicht' }),
).rejects.toBeInstanceOf(BadRequestException);
expect(prisma.customModule.update).not.toHaveBeenCalled();
});
it('update ohne Kategorie prueft nichts', async () => {
const { categories, service } = setup();
const created: any = await service.create('t1', userA, dto);
categories.assertCategoryKey.mockClear();
await service.update('t1', userA, created.id, { name: 'Neuer Name' });
expect(categories.assertCategoryKey).not.toHaveBeenCalled();
});
});
describe('CustomModulesService — lesen', () => {
it('list liefert gemeinsame plus eigene Eintraege, nie die eines anderen Benutzers', async () => {
const { service } = setup();