refactor(quick-260922-m1h): Widget-Typen an einer Stelle, Katalog aus der Registry, Kachel kennt ihr Modul
Ein neuer Widget-Typ war an sieben Stellen einzutragen; vergass man eine, fehlte die Kachel im Katalog oder die API lehnte sie mit 400 ab. - WIDGET_TYPES/WidgetType/WIDGET_MODULE_SLUGS stehen jetzt einmal in packages/shared; Registry, Katalog und die @IsIn-Whitelist der API leiten davon ab - neun wireXWidget()-Funktionen durch ein generisches registerWidget() ersetzt (idempotent, unbekannter Typ wirft in der Entwicklung) - der Katalog fuehrt keine zweite Typliste mehr, sondern leitet sie aus der Registry ab und filtert nach Modulzugriff (fail-closed, wenn die Modulliste unbekannt ist); der Abruf von /modules/active liegt auf der Dashboard-Seite, nicht im Dialog - widget-module-map.ts liest die geteilte Tabelle statt einer Kopie, die oeffentliche Funktion bleibt unveraendert Der Katalogfilter ist Komfort (T-M1H-01) — verbindlich bleibt der serverseitige Filter in DashboardService.getWidgets. Abweichung vom Plan: apps/web hing entgegen der Planannahme noch nicht von @tessera/shared ab; die Abhaengigkeit wurde ergaenzt (Lockfile). Die Dockerfiles kopieren packages/shared bereits, der Produktionsbau von Next.js und der nest build laufen unveraendert. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import { WIDGET_MODULE_SLUGS, WIDGET_TYPES } from '@tessera/shared';
|
||||
import { CreateWidgetDto } from './dto/create-widget.dto';
|
||||
import { WIDGET_MODULE_MAP, getModuleSlugForWidgetType } from './widget-module-map';
|
||||
|
||||
/**
|
||||
* quick-260922-m1h: Web und API lesen dieselbe Tabelle. Liefen sie
|
||||
* auseinander, wuerde der Katalog eine Kachel anbieten, die der Server
|
||||
* danach wieder herausfiltert (oder umgekehrt).
|
||||
*/
|
||||
describe('widget-module-map (quick-260922-m1h)', () => {
|
||||
it('WIDGET_MODULE_MAP ist die Tabelle aus @tessera/shared, keine zweite Kopie', () => {
|
||||
expect(WIDGET_MODULE_MAP).toBe(WIDGET_MODULE_SLUGS);
|
||||
});
|
||||
|
||||
it('jeder Schluessel der Tabelle ist ein bekannter Widget-Typ', () => {
|
||||
for (const type of Object.keys(WIDGET_MODULE_MAP)) {
|
||||
expect(WIDGET_TYPES).toContain(type);
|
||||
}
|
||||
});
|
||||
|
||||
it('die neun heutigen Kacheln sind Plattform-Kacheln ohne Modulbezug', () => {
|
||||
for (const type of WIDGET_TYPES) {
|
||||
expect(getModuleSlugForWidgetType(type)).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('ein unbekannter Typ liefert undefined statt zu werfen', () => {
|
||||
expect(getModuleSlugForWidgetType('gibt-es-nicht')).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Der Kern des Umbaus: die `@IsIn`-Whitelist ist keine handgepflegte zweite
|
||||
* Liste mehr. Vergisst kuenftig jemand einen Eintrag in `packages/shared`,
|
||||
* schlaegt dieser Test fehl, statt die API eine gueltige Kachel mit 400
|
||||
* ablehnen zu lassen.
|
||||
*/
|
||||
describe('CreateWidgetDto-Whitelist (quick-260922-m1h)', () => {
|
||||
async function validateType(widgetType: string) {
|
||||
const dto = plainToInstance(CreateWidgetDto, { widgetType });
|
||||
return validate(dto);
|
||||
}
|
||||
|
||||
it.each([...WIDGET_TYPES])('akzeptiert den Typ "%s"', async (widgetType) => {
|
||||
await expect(validateType(widgetType)).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it('lehnt einen Typ ab, der nicht in WIDGET_TYPES steht', async () => {
|
||||
const errors = await validateType('gibt-es-nicht');
|
||||
|
||||
expect(errors).toHaveLength(1);
|
||||
expect(errors[0].property).toBe('widgetType');
|
||||
expect(errors[0].constraints).toHaveProperty('isIn');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user