960696745f
- Migration 20261008180000: NextcloudFilesConfig (Organisation) und NextcloudFilesAccount (Mandant UND Benutzer per Zeilenschutz, nur verschlüsseltes App-Passwort) - Transportschicht ncRequest: feste Pfadanfänge, Segmentcodierung, keine Weiterleitungen, keine Cookies, Zeitgrenzen; Aufrufsperre pausiert den Ursprung nach jedem 429 und sperrt Zugangsschlüssel nach dem ersten 401 - Einstellungen: Adresse speichern/prüfen, Adresswechsel läuft alle Konten ab (mit Bestätigung) - Controller mit Verwalten nur auf Einstellungen, Seed, Modul, Registrierung in Web (Ladefunktion, Symbol folder, Navigation, Layout) und Reiter Einstellungen - RLS-Inventar fortgeschrieben, Testaufbau (tessera-nc-test) und E2E-Skripte Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
121 lines
5.3 KiB
TypeScript
121 lines
5.3 KiB
TypeScript
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);
|
|
});
|
|
});
|