test(15-05): add failing test for module-filtered getWidgets
- covers every <behavior> case from 15-05-PLAN.md task 2, including the adjacency/empty/ordering/idempotency edge-probe categories and the fail-closed unresolved-slug case - RED confirmed: 4/8 fail against the current 1-arg getWidgets(userId)
This commit is contained in:
@@ -0,0 +1,212 @@
|
|||||||
|
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WIDGET_MODULE_MAP wird je Testfall über eine gemeinsame, gemockte
|
||||||
|
* Objektreferenz (`mockMap`) gesteuert: `getModuleSlugForWidgetType`
|
||||||
|
* liest `mockMap` erst zur Aufrufzeit, deshalb genügt eine Mutation vor
|
||||||
|
* jedem Test statt eines dynamischen Imports mit `vi.resetModules()` je
|
||||||
|
* Testfall (vi.mock wird von Vitest ohnehin an den Dateianfang gehoben).
|
||||||
|
*/
|
||||||
|
const mockMap: Record<string, string> = {};
|
||||||
|
|
||||||
|
vi.mock('./widget-module-map', () => ({
|
||||||
|
WIDGET_MODULE_MAP: mockMap,
|
||||||
|
getModuleSlugForWidgetType: (widgetType: string) => mockMap[widgetType],
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { DashboardService } from './dashboard.service';
|
||||||
|
|
||||||
|
function makeWidget(
|
||||||
|
overrides: Partial<{
|
||||||
|
id: string;
|
||||||
|
userId: string;
|
||||||
|
widgetType: string;
|
||||||
|
createdAt: Date;
|
||||||
|
}> = {},
|
||||||
|
) {
|
||||||
|
return {
|
||||||
|
id: overrides.id ?? 'w1',
|
||||||
|
userId: overrides.userId ?? 'user-1',
|
||||||
|
tenantId: 'tenant-1',
|
||||||
|
widgetType: overrides.widgetType ?? 'clock',
|
||||||
|
config: {},
|
||||||
|
createdAt: overrides.createdAt ?? new Date('2026-01-01'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeFakePrisma(
|
||||||
|
opts: {
|
||||||
|
widgets?: ReturnType<typeof makeWidget>[];
|
||||||
|
modules?: { id: string; slug: string }[];
|
||||||
|
} = {},
|
||||||
|
) {
|
||||||
|
const widgets = opts.widgets ?? [];
|
||||||
|
const modules = opts.modules ?? [];
|
||||||
|
|
||||||
|
return {
|
||||||
|
widgetInstance: {
|
||||||
|
findMany: vi.fn(async ({ where }: any) => {
|
||||||
|
return widgets
|
||||||
|
.filter((w) => w.userId === where.userId)
|
||||||
|
.slice()
|
||||||
|
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime());
|
||||||
|
}),
|
||||||
|
delete: vi.fn(),
|
||||||
|
},
|
||||||
|
module: {
|
||||||
|
findMany: vi.fn(async ({ where }: any) => {
|
||||||
|
const slugs: string[] = where.slug.in;
|
||||||
|
return modules.filter((m) => slugs.includes(m.slug));
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function makeFakeModuleAccessService(accessibleIds: Set<string>) {
|
||||||
|
return {
|
||||||
|
getAccessibleModuleIds: vi.fn(async () => accessibleIds),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* DashboardService.getWidgets — Modulfilter (D-22, PERM-07). Deckt jeden
|
||||||
|
* Fall aus 15-05-PLAN.md <behavior> inklusive der Edge-Probe-Kategorien
|
||||||
|
* adjacency/empty/ordering/idempotency ab.
|
||||||
|
*/
|
||||||
|
describe('DashboardService.getWidgets — Modulfilter (D-22, PERM-07)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
for (const key of Object.keys(mockMap)) delete mockMap[key];
|
||||||
|
});
|
||||||
|
|
||||||
|
it('empty/PERM-07: liefert bei leerer Zuordnungstabelle exakt die Prisma-Menge, ohne Zugriffs-Lookup', async () => {
|
||||||
|
const widgets = [
|
||||||
|
makeWidget({ id: 'w1', widgetType: 'clock' }),
|
||||||
|
makeWidget({ id: 'w2', widgetType: 'search' }),
|
||||||
|
];
|
||||||
|
const prisma = makeFakePrisma({ widgets });
|
||||||
|
const moduleAccessService = makeFakeModuleAccessService(new Set());
|
||||||
|
const service = new DashboardService(prisma as any, moduleAccessService as any);
|
||||||
|
|
||||||
|
const result = await service.getWidgets('user-1', 'tenant-1', 'USER' as any);
|
||||||
|
|
||||||
|
expect(result).toEqual(widgets);
|
||||||
|
expect(moduleAccessService.getAccessibleModuleIds).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('bleibt sichtbar, wenn der Widget-Typ in der Tabelle steht und das Modul zugänglich ist', async () => {
|
||||||
|
mockMap['tender-radar'] = 'tender-radar';
|
||||||
|
const widget = makeWidget({ id: 'w1', widgetType: 'tender-radar' });
|
||||||
|
const prisma = makeFakePrisma({
|
||||||
|
widgets: [widget],
|
||||||
|
modules: [{ id: 'mod-1', slug: 'tender-radar' }],
|
||||||
|
});
|
||||||
|
const moduleAccessService = makeFakeModuleAccessService(new Set(['mod-1']));
|
||||||
|
const service = new DashboardService(prisma as any, moduleAccessService as any);
|
||||||
|
|
||||||
|
const result = await service.getWidgets('user-1', 'tenant-1', 'USER' as any);
|
||||||
|
|
||||||
|
expect(result).toEqual([widget]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('fehlt, wenn der Widget-Typ in der Tabelle steht und das Modul nicht zugänglich ist', async () => {
|
||||||
|
mockMap['tender-radar'] = 'tender-radar';
|
||||||
|
const widget = makeWidget({ id: 'w1', widgetType: 'tender-radar' });
|
||||||
|
const prisma = makeFakePrisma({
|
||||||
|
widgets: [widget],
|
||||||
|
modules: [{ id: 'mod-1', slug: 'tender-radar' }],
|
||||||
|
});
|
||||||
|
const moduleAccessService = makeFakeModuleAccessService(new Set());
|
||||||
|
const service = new DashboardService(prisma as any, moduleAccessService as any);
|
||||||
|
|
||||||
|
const result = await service.getWidgets('user-1', 'tenant-1', 'USER' as any);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('D-03: ein ADMIN sieht ein modulgebundenes Widget eines aktiven Moduls auch ohne eigenen Grant', async () => {
|
||||||
|
mockMap['tender-radar'] = 'tender-radar';
|
||||||
|
const widget = makeWidget({ id: 'w1', widgetType: 'tender-radar' });
|
||||||
|
const prisma = makeFakePrisma({
|
||||||
|
widgets: [widget],
|
||||||
|
modules: [{ id: 'mod-1', slug: 'tender-radar' }],
|
||||||
|
});
|
||||||
|
// Simuliert den D-03-Kurzschluss der Zugriffsauflösung aus 15-01: ADMIN erhält alle aktiven Module.
|
||||||
|
const moduleAccessService = makeFakeModuleAccessService(new Set(['mod-1']));
|
||||||
|
const service = new DashboardService(prisma as any, moduleAccessService as any);
|
||||||
|
|
||||||
|
const result = await service.getWidgets('admin-1', 'tenant-1', 'ADMIN' as any);
|
||||||
|
|
||||||
|
expect(result).toEqual([widget]);
|
||||||
|
expect(moduleAccessService.getAccessibleModuleIds).toHaveBeenCalledWith(
|
||||||
|
'tenant-1',
|
||||||
|
'admin-1',
|
||||||
|
'ADMIN',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adjacency/PERM-07: ein Widget-Typ, der nicht in der Tabelle steht, bleibt unabhängig von jeder Zugriffsentscheidung erhalten', async () => {
|
||||||
|
mockMap['tender-radar'] = 'tender-radar';
|
||||||
|
const boundWidget = makeWidget({ id: 'w1', widgetType: 'tender-radar' });
|
||||||
|
const platformWidget = makeWidget({ id: 'w2', widgetType: 'clock' });
|
||||||
|
const prisma = makeFakePrisma({
|
||||||
|
widgets: [boundWidget, platformWidget],
|
||||||
|
modules: [{ id: 'mod-1', slug: 'tender-radar' }],
|
||||||
|
});
|
||||||
|
const moduleAccessService = makeFakeModuleAccessService(new Set()); // kein Zugriff auf tender-radar
|
||||||
|
const service = new DashboardService(prisma as any, moduleAccessService as any);
|
||||||
|
|
||||||
|
const result = await service.getWidgets('user-1', 'tenant-1', 'USER' as any);
|
||||||
|
|
||||||
|
expect(result).toEqual([platformWidget]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ordering/PERM-07: die Sortierung nach createdAt aufsteigend bleibt nach dem Filtern erhalten', async () => {
|
||||||
|
mockMap['tender-radar'] = 'tender-radar';
|
||||||
|
const w1 = makeWidget({ id: 'w1', widgetType: 'clock', createdAt: new Date('2026-01-01') });
|
||||||
|
const w2 = makeWidget({
|
||||||
|
id: 'w2',
|
||||||
|
widgetType: 'tender-radar',
|
||||||
|
createdAt: new Date('2026-01-02'),
|
||||||
|
});
|
||||||
|
const w3 = makeWidget({ id: 'w3', widgetType: 'search', createdAt: new Date('2026-01-03') });
|
||||||
|
const prisma = makeFakePrisma({
|
||||||
|
widgets: [w1, w2, w3],
|
||||||
|
modules: [{ id: 'mod-1', slug: 'tender-radar' }],
|
||||||
|
});
|
||||||
|
const moduleAccessService = makeFakeModuleAccessService(new Set(['mod-1']));
|
||||||
|
const service = new DashboardService(prisma as any, moduleAccessService as any);
|
||||||
|
|
||||||
|
const result = await service.getWidgets('user-1', 'tenant-1', 'USER' as any);
|
||||||
|
|
||||||
|
expect(result.map((w: any) => w.id)).toEqual(['w1', 'w2', 'w3']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('idempotency/PERM-07: getWidgets löscht keine WidgetInstance-Zeile — nur findMany wird aufgerufen', async () => {
|
||||||
|
mockMap['tender-radar'] = 'tender-radar';
|
||||||
|
const widget = makeWidget({ id: 'w1', widgetType: 'tender-radar' });
|
||||||
|
const prisma = makeFakePrisma({
|
||||||
|
widgets: [widget],
|
||||||
|
modules: [{ id: 'mod-1', slug: 'tender-radar' }],
|
||||||
|
});
|
||||||
|
const moduleAccessService = makeFakeModuleAccessService(new Set());
|
||||||
|
const service = new DashboardService(prisma as any, moduleAccessService as any);
|
||||||
|
|
||||||
|
await service.getWidgets('user-1', 'tenant-1', 'USER' as any);
|
||||||
|
await service.getWidgets('user-1', 'tenant-1', 'USER' as any);
|
||||||
|
|
||||||
|
expect(prisma.widgetInstance.delete).not.toHaveBeenCalled();
|
||||||
|
expect(prisma.widgetInstance.findMany).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Fail-Closed: existiert zu einem eingetragenen Modul-Slug kein Module-Datensatz, wird das Widget herausgefiltert', async () => {
|
||||||
|
mockMap['ghost-module'] = 'ghost-module';
|
||||||
|
const widget = makeWidget({ id: 'w1', widgetType: 'ghost-module' });
|
||||||
|
const prisma = makeFakePrisma({ widgets: [widget], modules: [] });
|
||||||
|
const moduleAccessService = makeFakeModuleAccessService(new Set(['irgendeine-id']));
|
||||||
|
const service = new DashboardService(prisma as any, moduleAccessService as any);
|
||||||
|
|
||||||
|
const result = await service.getWidgets('user-1', 'tenant-1', 'USER' as any);
|
||||||
|
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user