feat(quick-260923-ad9): Reiter anlegen, umbenennen, loeschen, umsortieren - Task 2
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>
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user