c703d87a1c
- ownerUserId (NULL = gemeinsam, sonst persoenlich) mit Zeilenschutz nach Muster SearchProvider - GET nur gemeinsame + eigene, fremde persoenliche Eintraege 404 - POST fuer jeden Benutzer, shared nur fuer Administratoren (403) - PATCH/DELETE: persoenlich nur Besitzer, gemeinsam nur Administrator - Zugriffsklassifikation nachgemessen: 61/223/6 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
108 lines
4.7 KiB
TypeScript
108 lines
4.7 KiB
TypeScript
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);
|
|
});
|
|
});
|