import 'reflect-metadata'; import { ForbiddenException } from '@nestjs/common'; import { describe, expect, it, vi } from 'vitest'; import { ROLES_KEY } from '../auth/decorators/roles.decorator'; import { MODULE_MANAGE_KEY, MODULE_SLUG_KEY } from '../module-registry/module.guard'; import { NextcloudFilesController } from './nextcloud-files.controller'; const proto = NextcloudFilesController.prototype as any; const req = (tenantId?: string) => ({ tenantId }) as any; /** Verwalten (Administratoren und Freigabestufe Verwalten). Spaetere Aufgaben ergaenzen nichts hier. */ const MANAGE_HANDLERS = ['getSettings', 'saveSettings', 'testSettings']; /** Alle Handler mit Routenpfad, in Deklarationsreihenfolge. */ function routeHandlers(): string[] { return Object.getOwnPropertyNames(NextcloudFilesController.prototype).filter( (n) => n !== 'constructor' && typeof proto[n] === 'function' && Reflect.getMetadata('path', proto[n]) !== undefined, ); } /** * Wiederverwendbar (spaetere Aufgaben fuegen nur Handler hinzu): jeder Handler, * dessen Pfad ein `:` enthaelt, steht NACH allen statischen Handlern. */ function expectParamRoutesLast(controller: { prototype: object }): void { const p = controller.prototype as any; const names = Object.getOwnPropertyNames(controller.prototype).filter( (n) => n !== 'constructor' && typeof p[n] === 'function' && Reflect.getMetadata('path', p[n]) !== undefined, ); const isParam = (n: string) => String(Reflect.getMetadata('path', p[n])).includes(':'); const firstParam = names.findIndex(isParam); if (firstParam === -1) return; names.slice(firstParam).forEach((n) => { expect(isParam(n), `${n} steht nach einer Parameterroute, ist aber statisch`).toBe(true); }); } describe('NextcloudFilesController — Metadaten', () => { it('haengt an modules/nextcloud-files und traegt @UseModule(nextcloud-files)', () => { expect(Reflect.getMetadata('path', NextcloudFilesController)).toBe('modules/nextcloud-files'); expect(Reflect.getMetadata(MODULE_SLUG_KEY, NextcloudFilesController)).toBe('nextcloud-files'); }); it('Einstellungen und Pruefung verlangen Verwalten, ohne Rollen-Decorator', () => { for (const name of MANAGE_HANDLERS) { expect(Reflect.getMetadata(MODULE_MANAGE_KEY, proto[name]), name).toBe(true); expect(Reflect.getMetadata(ROLES_KEY, proto[name]), name).toBeUndefined(); } }); it('alle anderen Handler stehen auf Benutzen-Ebene', () => { const others = routeHandlers().filter((n) => !MANAGE_HANDLERS.includes(n)); expect(others).toContain('getStatus'); for (const name of others) { expect(Reflect.getMetadata(MODULE_MANAGE_KEY, proto[name]), name).toBeUndefined(); expect(Reflect.getMetadata(ROLES_KEY, proto[name]), name).toBeUndefined(); } }); it('Pfade und Methoden', () => { const route = (name: string) => [ Reflect.getMetadata('method', proto[name]), Reflect.getMetadata('path', proto[name]), ]; // RequestMethod: GET 0, POST 1, PUT 2, DELETE 3 expect(route('getStatus')).toEqual([0, 'status']); expect(route('getSettings')).toEqual([0, 'settings']); expect(route('saveSettings')).toEqual([2, 'settings']); expect(route('testSettings')).toEqual([1, 'settings/test']); }); it('POST settings/test antwortet 200, nicht 201', () => { expect(Reflect.getMetadata('__httpCode__', proto.testSettings)).toBe(200); }); }); describe('NextcloudFilesController — Routen-Reihenfolge (statisch vor Parameter)', () => { it('deklariert jeden Handler mit :-Pfad nach allen statischen Handlern', () => { expectParamRoutesLast(NextcloudFilesController); }); }); describe('NextcloudFilesController — Delegation', () => { function makeSettings() { return { getStatus: vi.fn(async (..._a: unknown[]) => ({ configured: true })), getSettings: vi.fn(async (..._a: unknown[]) => ({ baseUrl: null, connectedAccounts: 0 })), saveSettings: vi.fn(async (..._a: unknown[]) => ({})), testAddress: vi.fn(async (..._a: unknown[]) => ({ ok: true })), }; } it('reicht den Mandanten aus dem Token weiter, nie aus dem Body', async () => { const settings = makeSettings(); const controller = new NextcloudFilesController(settings as any); await controller.getStatus(req('t1')); await controller.getSettings(req('t1')); await controller.saveSettings(req('t1'), { baseUrl: 'https://x.example' } as any); await controller.testSettings(req('t1'), { baseUrl: 'https://x.example' } as any); expect(settings.getStatus).toHaveBeenCalledWith('t1'); expect(settings.getSettings).toHaveBeenCalledWith('t1'); expect(settings.saveSettings).toHaveBeenCalledWith('t1', { baseUrl: 'https://x.example' }); expect(settings.testAddress).toHaveBeenCalledWith('https://x.example'); }); it('ohne Mandantenkontext: ForbiddenException', async () => { const controller = new NextcloudFilesController(makeSettings() as any); await expect(controller.getStatus(req(undefined))).rejects.toBeInstanceOf(ForbiddenException); await expect(controller.getSettings(req(undefined))).rejects.toBeInstanceOf(ForbiddenException); await expect( controller.testSettings(req(undefined), { baseUrl: 'https://x.example' } as any), ).rejects.toBeInstanceOf(ForbiddenException); }); });