feat(api,web): eigene Module — Tabelle, API, Seitenleiste, Rahmen-Seite
- Tabelle CustomModule mit Zeilenschutz (tenant_isolation_policy), Migration 20260929120000 - API /custom-modules: Lesen für jeden Angemeldeten, Schreiben nur Administrator, nur https ohne Zugangsdaten - Seitenleiste zeigt eigene Module unter ihrer Kategorie, Rahmen-Seite mit Sandbox und „In neuem Tab öffnen“ - MODULE_CATEGORIES als gemeinsame Liste, Zugriffsklassifikation nachgemessen fortgeschrieben Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
import 'reflect-metadata';
|
||||
import { ForbiddenException, ValidationPipe } from '@nestjs/common';
|
||||
import { Role } from '@prisma/client';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ROLES_KEY } from '../auth/decorators/roles.decorator';
|
||||
import { CustomModulesController } from './custom-modules.controller';
|
||||
import { CreateCustomModuleDto } from './dto/custom-module.dto';
|
||||
|
||||
function makeService() {
|
||||
return {
|
||||
list: vi.fn(async (..._args: unknown[]) => []),
|
||||
getOne: vi.fn(async (..._args: unknown[]) => ({})),
|
||||
create: vi.fn(async (..._args: unknown[]) => ({})),
|
||||
update: vi.fn(async (..._args: unknown[]) => ({})),
|
||||
remove: vi.fn(async (..._args: unknown[]) => ({ deleted: true })),
|
||||
};
|
||||
}
|
||||
|
||||
const req = (tenantId?: string) => ({ tenantId }) as any;
|
||||
const proto = CustomModulesController.prototype as any;
|
||||
|
||||
describe('CustomModulesController — Rollen (T-9WC-01)', () => {
|
||||
it.each(['create', 'update', 'remove'])('%s ist nur fuer ADMIN und SUPER_ADMIN offen', (name) => {
|
||||
expect(Reflect.getMetadata(ROLES_KEY, proto[name])).toEqual([Role.ADMIN, Role.SUPER_ADMIN]);
|
||||
});
|
||||
|
||||
it.each(['list', 'getOne'])('%s traegt keine Rollen (jeder Angemeldete)', (name) => {
|
||||
expect(Reflect.getMetadata(ROLES_KEY, proto[name])).toBeUndefined();
|
||||
});
|
||||
|
||||
it('haengt an Pfad custom-modules', () => {
|
||||
expect(Reflect.getMetadata('path', CustomModulesController)).toBe('custom-modules');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CustomModulesController — Mandant', () => {
|
||||
it('reicht req.tenantId an den Dienst weiter', async () => {
|
||||
const service = makeService();
|
||||
const controller = new CustomModulesController(service as any);
|
||||
await controller.list(req('t1'));
|
||||
await controller.getOne(req('t1'), 'x');
|
||||
await controller.create(req('t1'), { name: 'a', url: 'https://a.de', category: 'fleet' });
|
||||
await controller.update(req('t1'), 'x', { name: 'b' });
|
||||
await controller.remove(req('t1'), 'x');
|
||||
expect(service.list).toHaveBeenCalledWith('t1');
|
||||
expect(service.getOne).toHaveBeenCalledWith('t1', 'x');
|
||||
expect(service.create.mock.calls[0][0]).toBe('t1');
|
||||
expect(service.update.mock.calls[0].slice(0, 2)).toEqual(['t1', 'x']);
|
||||
expect(service.remove).toHaveBeenCalledWith('t1', 'x');
|
||||
});
|
||||
|
||||
it('wirft ForbiddenException ohne req.tenantId', async () => {
|
||||
const controller = new CustomModulesController(makeService() as any);
|
||||
await expect(controller.list(req())).rejects.toBeInstanceOf(ForbiddenException);
|
||||
await expect(controller.getOne(req(), 'x')).rejects.toBeInstanceOf(ForbiddenException);
|
||||
await expect(
|
||||
controller.create(req(), { name: 'a', url: 'https://a.de', category: 'fleet' }),
|
||||
).rejects.toBeInstanceOf(ForbiddenException);
|
||||
await expect(controller.remove(req(), 'x')).rejects.toBeInstanceOf(ForbiddenException);
|
||||
});
|
||||
|
||||
it('die globale Pipe verwirft ein untergeschobenes tenantId (T-9WC-07)', async () => {
|
||||
const pipe = new ValidationPipe({ whitelist: true, transform: true });
|
||||
const out: any = await pipe.transform(
|
||||
{ name: 'a', url: 'https://a.de', category: 'fleet', tenantId: 'evil' },
|
||||
{ type: 'body', metatype: CreateCustomModuleDto },
|
||||
);
|
||||
expect(out).not.toHaveProperty('tenantId');
|
||||
});
|
||||
});
|
||||
|
||||
describe('CustomModulesController — Routen-Reihenfolge (statisch vor :id)', () => {
|
||||
it('deklariert list vor getOne', () => {
|
||||
const methods = Object.getOwnPropertyNames(CustomModulesController.prototype);
|
||||
const listIdx = methods.indexOf('list');
|
||||
const idIdx = methods.indexOf('getOne');
|
||||
expect(listIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(idIdx).toBeGreaterThanOrEqual(0);
|
||||
expect(listIdx).toBeLessThan(idIdx);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user