import 'reflect-metadata'; import { ForbiddenException, ValidationPipe } from '@nestjs/common'; import { describe, expect, it, vi } from 'vitest'; import { ROLES_KEY } from '../auth/decorators/roles.decorator'; import { CustomModulesController } from './custom-modules.controller'; import { CreateCustomModuleDto, UpdateCustomModuleDto } 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 user = { id: 'u1', username: 'u', role: 'USER', tenantId: 't1' } as any; const proto = CustomModulesController.prototype as any; describe('CustomModulesController — Rollen (quick-260929-dzu)', () => { // Jeder Angemeldete darf persoenliche Eintraege anlegen/aendern/loeschen; die // Administrator-Pflicht fuer gemeinsame Eintraege prueft der Dienst (hangt // vom Eintrag ab, nicht von der Route) — siehe custom-modules.service.spec.ts. it.each([ 'list', 'getOne', 'create', 'update', 'remove', ])('%s traegt keine Routen-Rolle (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'), user); await controller.getOne(req('t1'), user, 'x'); await controller.create(req('t1'), user, { name: 'a', url: 'https://a.de', category: 'fleet' }); await controller.update(req('t1'), user, 'x', { name: 'b' }); await controller.remove(req('t1'), user, 'x'); expect(service.list).toHaveBeenCalledWith('t1', user); expect(service.getOne).toHaveBeenCalledWith('t1', user, 'x'); expect(service.create.mock.calls[0].slice(0, 2)).toEqual(['t1', user]); expect(service.update.mock.calls[0].slice(0, 3)).toEqual(['t1', user, 'x']); expect(service.remove).toHaveBeenCalledWith('t1', user, 'x'); }); it('wirft ForbiddenException ohne req.tenantId', async () => { const controller = new CustomModulesController(makeService() as any); await expect(controller.list(req(), user)).rejects.toBeInstanceOf(ForbiddenException); await expect(controller.getOne(req(), user, 'x')).rejects.toBeInstanceOf(ForbiddenException); await expect( controller.create(req(), user, { name: 'a', url: 'https://a.de', category: 'fleet' }), ).rejects.toBeInstanceOf(ForbiddenException); await expect(controller.remove(req(), user, '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'); }); it('die globale Pipe verwirft ownerUserId, laesst shared beim Anlegen durch', async () => { const pipe = new ValidationPipe({ whitelist: true, transform: true }); const out: any = await pipe.transform( { name: 'a', url: 'https://a.de', category: 'fleet', ownerUserId: 'evil', shared: true }, { type: 'body', metatype: CreateCustomModuleDto }, ); expect(out).not.toHaveProperty('ownerUserId'); expect(out.shared).toBe(true); }); it('die globale Pipe verwirft shared und ownerUserId beim Aendern', async () => { const pipe = new ValidationPipe({ whitelist: true, transform: true }); const out: any = await pipe.transform( { name: 'b', shared: true, ownerUserId: 'evil' }, { type: 'body', metatype: UpdateCustomModuleDto }, ); expect(out).not.toHaveProperty('shared'); expect(out).not.toHaveProperty('ownerUserId'); expect(out.name).toBe('b'); }); }); 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); }); });