df7a5e7e8e
dashboard.service.ts: createDashboard() (Namensvergabe "Dashboard N" fuellt Luecken, D-08; Obergrenze 20 Reiter, T-AD9-06), renameDashboard() (Riegel zuerst), deleteDashboard() (letzter Reiter bleibt, D-10; Loeschen + Neu- Nummerierung als EINE withTenantTransaction), reorderDashboards() (woertlich nach FavoritesService.reorder-Muster: Exakt-Abgleich vor jedem Schreiben, kein Teilschreiben, dieselbe Abweisung fuer unvollstaendige/unbekannte/ fremde Kennungen - T-AD9-04). dashboard.controller.ts: fuenf neue Wege unter tabs; PUT tabs/order VOR PATCH/DELETE tabs/:id deklariert (Routenreihenfolge). dashboard.controller.spec.ts (neu, 8 Tests): Durchreichung, Abweisung ohne Kontext, quelltextlesender Waechter fuer die Routenreihenfolge. dashboard.service.spec.ts: 61 Tests (43 alte + 18 neue fuer Anlegen, Umbenennen inkl. DTO-Beschneidung/-Laengenpruefung, Loeschen und Umsortieren - je ein Fall fuer "fremder Reiter" und "letzter Reiter bleibt"). Deviation (Rule 1): widget-module-map.spec.ts's CreateWidgetDto-Whitelist helper needed a dashboardId fixture after Task 1 made the field required - fixed inline, out of the plan's files_modified list but directly caused by Task 1's DTO change. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
144 lines
6.2 KiB
TypeScript
144 lines
6.2 KiB
TypeScript
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> = {}): 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);
|
|
});
|
|
});
|