import 'reflect-metadata'; import { GUARDS_METADATA } from '@nestjs/common/constants'; import { describe, expect, it, vi } from 'vitest'; import { ROLES_KEY } from '../auth/decorators/roles.decorator'; import { MODULE_MANAGE_KEY, MODULE_SLUG_KEY, ModuleGuard } from '../module-registry/module.guard'; import { NextcloudStatusController } from './nextcloud-status.controller'; const proto = NextcloudStatusController.prototype as unknown as Record; describe('NextcloudStatusController Metadaten', () => { it('Klasse traegt Modul-Slug und ModuleGuard', () => { expect(Reflect.getMetadata(MODULE_SLUG_KEY, NextcloudStatusController)).toBe( 'nextcloud-status', ); expect(Reflect.getMetadata(GUARDS_METADATA, NextcloudStatusController)).toContain(ModuleGuard); }); it.each(['list', 'logo', 'subscribe', 'unsubscribe'])('%s bleibt auf Benutzen-Ebene', (name) => { expect(Reflect.getMetadata(MODULE_MANAGE_KEY, proto[name] as object)).toBeUndefined(); expect(Reflect.getMetadata(ROLES_KEY, proto[name] as object)).toBeUndefined(); }); it.each([ 'create', 'update', 'remove', 'checkAll', 'checkOne', 'uploadLogo', 'removeLogo', ])('%s verlangt Verwalten ohne Rollen-Decorator', (name) => { const fn = proto[name] as object; expect(Reflect.getMetadata(MODULE_MANAGE_KEY, fn)).toBe(true); expect(Reflect.getMetadata(MODULE_SLUG_KEY, fn)).toBe('nextcloud-status'); expect(Reflect.getMetadata(ROLES_KEY, fn)).toBeUndefined(); }); it('die statische Route POST instances/check steht vor jedem Handler mit :id (kein 404-Shadowing)', () => { const names = Object.getOwnPropertyNames(NextcloudStatusController.prototype).filter( (n) => n !== 'constructor' && typeof proto[n] === 'function', ); const pathOf = (n: string) => Reflect.getMetadata('path', proto[n] as object) as string; expect(pathOf('checkAll')).toBe('instances/check'); const checkIndex = names.indexOf('checkAll'); const idHandlers = names.filter((n) => (pathOf(n) ?? '').includes(':id')); expect(idHandlers.length).toBeGreaterThan(0); for (const name of idHandlers) { expect(checkIndex, `checkAll vor ${name}`).toBeLessThan(names.indexOf(name)); } }); }); describe('NextcloudStatusController Glocke (quick-261002-kxc)', () => { const pathOf = (n: string) => Reflect.getMetadata('path', proto[n] as object) as string; const methodOf = (n: string) => Reflect.getMetadata('method', proto[n] as object) as number; it('POST und DELETE instances/:id/subscription', () => { expect(pathOf('subscribe')).toBe('instances/:id/subscription'); expect(pathOf('unsubscribe')).toBe('instances/:id/subscription'); // RequestMethod.POST = 1, DELETE = 3 expect(methodOf('subscribe')).toBe(1); expect(methodOf('unsubscribe')).toBe(3); }); it('Benutzer kommt aus dem Token, Mandant aus der Anfrage — nie aus Body oder Pfad', async () => { const service = { listForTenant: vi.fn().mockResolvedValue({ instances: [] }), checkAllForTenant: vi.fn().mockResolvedValue({ instances: [] }), }; const alerts = { subscribe: vi.fn().mockResolvedValue({ subscribed: true }), unsubscribe: vi.fn().mockResolvedValue({ subscribed: false }), }; const controller = new NextcloudStatusController(service as never, alerts as never); const req = { tenantId: 't1', body: { userId: 'fremd', tenantId: 'fremd' } } as never; const user = { id: 'u1' } as never; expect(await controller.subscribe(req, user, 'i1')).toEqual({ subscribed: true }); expect(alerts.subscribe).toHaveBeenCalledWith('t1', 'u1', 'i1'); expect(await controller.unsubscribe(req, user, 'i1')).toEqual({ subscribed: false }); expect(alerts.unsubscribe).toHaveBeenCalledWith('t1', 'u1', 'i1'); await controller.list(req, user); expect(service.listForTenant).toHaveBeenCalledWith('t1', 'u1'); await controller.checkAll(req, user); expect(service.checkAllForTenant).toHaveBeenCalledWith('t1', 'u1'); }); it('ohne Mandantenkontext -> 403', async () => { const controller = new NextcloudStatusController({} as never, {} as never); await expect(controller.subscribe({} as never, { id: 'u1' } as never, 'i1')).rejects.toThrow( 'Kein Mandantenkontext', ); }); });