import { readFileSync } from 'node:fs'; import { join } from 'node:path'; import { ForbiddenException } from '@nestjs/common'; import { describe, expect, it, vi } from 'vitest'; import type { AuthenticatedRequest } from '../auth/types/auth-user'; import { DashboardController } from './dashboard.controller'; /** * dashboard.controller.spec — NEU (quick-260923-ad9, Task 2). Form aus * `dashboard-images.controller.spec.ts`: die Reiter-Kennung wird aus * Abfrageparameter bzw. Rumpf an den Dienst durchgereicht, fehlender * Benutzer-/Mandantenkontext führt zur vorhandenen Abweisung, und ein * quelltextlesender Wächter prüft, dass die feste Route `tabs/order` VOR * jeder Route mit Platzhalter unter demselben Präfix im Dateitext steht * (NestJS-Routenreihenfolge — in dieser Anwendung hat eine Route mit * Platzhalter schon einmal eine dahinter stehende feste Route verdeckt). */ function makeService() { return { listDashboards: vi.fn(async () => [{ id: 'dash-1' }]), createDashboard: vi.fn(async () => ({ id: 'dash-2' })), reorderDashboards: vi.fn(async () => [{ id: 'dash-1' }, { id: 'dash-2' }]), renameDashboard: vi.fn(async (id: string) => ({ id })), deleteDashboard: vi.fn(async (id: string) => ({ id })), getLayout: vi.fn(async () => ({ lg: [] })), saveLayout: vi.fn(async () => ({ id: 'layout-1' })), getWidgets: vi.fn(async () => []), addWidget: vi.fn(async () => ({ id: 'w1' })), updateWidgetConfig: vi.fn(async () => ({ id: 'w1' })), removeWidget: vi.fn(async () => ({ id: 'w1' })), getSearchProviders: vi.fn(async () => []), addSearchProvider: vi.fn(async () => ({ id: 'sp1' })), removeSearchProvider: vi.fn(async () => ({ id: 'sp1' })), }; } function makeRequest(overrides: Partial = {}): AuthenticatedRequest { return { user: { id: 'user-1', tenantId: 'tenant-1', role: 'USER', username: 'anna', mustChangePassword: false }, tenantId: 'tenant-1', ...overrides, } as AuthenticatedRequest; } describe('DashboardController — Reiter (quick-260923-ad9, Task 2)', () => { it('listDashboards: reicht userId/tenantId aus dem Sitzungsnachweis durch', async () => { const service = makeService(); const controller = new DashboardController(service as never); await controller.listDashboards(makeRequest()); expect(service.listDashboards).toHaveBeenCalledWith('user-1', 'tenant-1'); }); it('createDashboard: reicht userId/tenantId durch, kein Rumpf nötig', async () => { const service = makeService(); const controller = new DashboardController(service as never); await controller.createDashboard(makeRequest()); expect(service.createDashboard).toHaveBeenCalledWith('user-1', 'tenant-1'); }); it('reorderDashboards: reicht die Kennungsliste aus dem Rumpf durch', async () => { const service = makeService(); const controller = new DashboardController(service as never); const dto = { ids: ['dash-2', 'dash-1'] } as never; await controller.reorderDashboards(makeRequest(), dto); expect(service.reorderDashboards).toHaveBeenCalledWith('user-1', 'tenant-1', dto); }); it('renameDashboard: reicht Pfad-Kennung und Rumpf durch', async () => { const service = makeService(); const controller = new DashboardController(service as never); const dto = { name: 'Neuer Name' } as never; await controller.renameDashboard('dash-1', makeRequest(), dto); expect(service.renameDashboard).toHaveBeenCalledWith('dash-1', 'user-1', 'tenant-1', dto); }); it('deleteDashboard: reicht die Pfad-Kennung durch', async () => { const service = makeService(); const controller = new DashboardController(service as never); await controller.deleteDashboard('dash-1', makeRequest()); expect(service.deleteDashboard).toHaveBeenCalledWith('dash-1', 'user-1', 'tenant-1'); }); it('getLayout/getWidgets: reichen die Reiter-Kennung aus dem Abfrageparameter durch', async () => { const service = makeService(); const controller = new DashboardController(service as never); await controller.getLayout(makeRequest(), 'dash-1'); await controller.getWidgets(makeRequest(), 'dash-1'); expect(service.getLayout).toHaveBeenCalledWith('user-1', 'tenant-1', 'dash-1'); expect(service.getWidgets).toHaveBeenCalledWith('user-1', 'tenant-1', 'USER', 'dash-1'); }); it('fehlender Mandantenkontext führt bei den neuen Reiter-Wegen zur vorhandenen Abweisung', async () => { const service = makeService(); const controller = new DashboardController(service as never); const req = makeRequest({ user: undefined, tenantId: undefined } as never); await expect(controller.listDashboards(req)).rejects.toBeInstanceOf(ForbiddenException); await expect(controller.createDashboard(req)).rejects.toBeInstanceOf(ForbiddenException); await expect( controller.reorderDashboards(req, { ids: [] } as never), ).rejects.toBeInstanceOf(ForbiddenException); await expect( controller.renameDashboard('dash-1', req, { name: 'x' } as never), ).rejects.toBeInstanceOf(ForbiddenException); await expect(controller.deleteDashboard('dash-1', req)).rejects.toBeInstanceOf( ForbiddenException, ); }); it('Wächter: die feste Route "tabs/order" steht im Dateitext VOR jeder Route mit Platzhalter unter demselben Präfix (NestJS-Routenreihenfolge)', () => { const source = readFileSync(join(__dirname, 'dashboard.controller.ts'), 'utf-8'); const orderIndex = source.indexOf("@Put('tabs/order')"); const patchIdIndex = source.indexOf("@Patch('tabs/:id')"); const deleteIdIndex = source.indexOf("@Delete('tabs/:id')"); expect(orderIndex, '@Put(\'tabs/order\') fehlt im Quelltext').toBeGreaterThan(-1); expect(patchIdIndex, '@Patch(\'tabs/:id\') fehlt im Quelltext').toBeGreaterThan(-1); expect(deleteIdIndex, '@Delete(\'tabs/:id\') fehlt im Quelltext').toBeGreaterThan(-1); expect( orderIndex, 'tabs/order muss VOR PATCH tabs/:id deklariert sein, sonst verdeckt der Platzhalter die feste Route', ).toBeLessThan(patchIdIndex); expect( orderIndex, 'tabs/order muss VOR DELETE tabs/:id deklariert sein, sonst verdeckt der Platzhalter die feste Route', ).toBeLessThan(deleteIdIndex); }); });