feat(quick-261008-who): eigene Module beim Start vorladen — Durchstich
- Spalte User.customModulePreloadIds, PUT /custom-modules/:id/preload (Grenze 8, 404 fuer Fremdes) - Vorlade-Planer und -Komponente, Speicher preload()/pinned, Ersatzgroesse im Behaelter - E2E-Skript gegen die API Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
-- quick-261008-who: Kennungen eigener Module, die der Benutzer beim Start vorladen laesst.
|
||||
ALTER TABLE "User" ADD COLUMN "customModulePreloadIds" TEXT[] NOT NULL DEFAULT ARRAY[]::TEXT[];
|
||||
@@ -50,6 +50,12 @@ model User {
|
||||
// sonst das durch parseDashboardBackground (@tessera/shared) normalisierte
|
||||
// Objekt, auch { kind: 'none' } fuer bewusst "kein Hintergrund"
|
||||
dashboardBackground Json?
|
||||
// quick-261008-who: Kennungen eigener Module (CustomModule.id), die dieser
|
||||
// Benutzer beim Start vorladen laesst. Bewusst eine Spalte der Benutzerzeile
|
||||
// statt einer Tabelle (Zeilenschutz der Benutzerzeile gilt schon). Tote
|
||||
// Kennungen (geloeschte oder nicht mehr sichtbare Module) werden beim Lesen
|
||||
// geschnitten und beim naechsten Schreiben entfernt.
|
||||
customModulePreloadIds String[] @default([])
|
||||
// Willkommensmail aus der Benutzerverwaltung: Zeitpunkt des letzten
|
||||
// Versands; null = nie gesendet
|
||||
welcomeMailSentAt DateTime?
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
import 'reflect-metadata';
|
||||
import { ForbiddenException, ValidationPipe } from '@nestjs/common';
|
||||
import { ForbiddenException, RequestMethod, ValidationPipe } from '@nestjs/common';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { ROLES_KEY } from '../auth/decorators/roles.decorator';
|
||||
import { CustomModulesController } from './custom-modules.controller';
|
||||
import { CreateCustomModuleDto, UpdateCustomModuleDto } from './dto/custom-module.dto';
|
||||
import {
|
||||
CreateCustomModuleDto,
|
||||
SetCustomModulePreloadDto,
|
||||
UpdateCustomModuleDto,
|
||||
} from './dto/custom-module.dto';
|
||||
|
||||
function makeService() {
|
||||
return {
|
||||
@@ -12,6 +16,7 @@ function makeService() {
|
||||
create: vi.fn(async (..._args: unknown[]) => ({})),
|
||||
update: vi.fn(async (..._args: unknown[]) => ({})),
|
||||
remove: vi.fn(async (..._args: unknown[]) => ({ deleted: true })),
|
||||
setPreload: vi.fn(async (..._args: unknown[]) => ({})),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -29,6 +34,7 @@ describe('CustomModulesController — Rollen (quick-260929-dzu)', () => {
|
||||
'create',
|
||||
'update',
|
||||
'remove',
|
||||
'setPreload',
|
||||
])('%s traegt keine Routen-Rolle (jeder Angemeldete)', (name) => {
|
||||
expect(Reflect.getMetadata(ROLES_KEY, proto[name])).toBeUndefined();
|
||||
});
|
||||
@@ -105,3 +111,25 @@ describe('CustomModulesController — Routen-Reihenfolge (statisch vor :id)', ()
|
||||
expect(listIdx).toBeLessThan(idIdx);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CustomModulesController — Vorladen (quick-261008-who)', () => {
|
||||
it('PUT :id/preload mit Metadaten, reicht Mandant und Benutzer durch', async () => {
|
||||
expect(Reflect.getMetadata('path', proto.setPreload)).toBe(':id/preload');
|
||||
expect(Reflect.getMetadata('method', proto.setPreload)).toBe(RequestMethod.PUT);
|
||||
const service = makeService();
|
||||
const controller = new CustomModulesController(service as any);
|
||||
await controller.setPreload(req('t1'), user, 'x', { preload: true });
|
||||
expect(service.setPreload).toHaveBeenCalledWith('t1', user, 'x', true);
|
||||
await expect(controller.setPreload(req(), user, 'x', { preload: true })).rejects.toBeInstanceOf(
|
||||
ForbiddenException,
|
||||
);
|
||||
});
|
||||
|
||||
it('das DTO lehnt fehlendes oder nicht-boolesches preload ab', async () => {
|
||||
const pipe = new ValidationPipe({ whitelist: true, transform: true });
|
||||
const meta = { type: 'body' as const, metatype: SetCustomModulePreloadDto };
|
||||
await expect(pipe.transform({}, meta)).rejects.toBeDefined();
|
||||
await expect(pipe.transform({ preload: 'ja' }, meta)).rejects.toBeDefined();
|
||||
await expect(pipe.transform({ preload: true }, meta)).resolves.toMatchObject({ preload: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,12 +7,17 @@ import {
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import { CurrentUser } from '../auth/decorators/current-user.decorator';
|
||||
import type { AuthenticatedRequest, AuthUser } from '../auth/types/auth-user';
|
||||
import { CustomModulesService } from './custom-modules.service';
|
||||
import { CreateCustomModuleDto, UpdateCustomModuleDto } from './dto/custom-module.dto';
|
||||
import {
|
||||
CreateCustomModuleDto,
|
||||
SetCustomModulePreloadDto,
|
||||
UpdateCustomModuleDto,
|
||||
} from './dto/custom-module.dto';
|
||||
|
||||
/**
|
||||
* Eigene Module (quick-260929-9wc, erweitert in quick-260929-dzu). Jeder
|
||||
@@ -56,6 +61,21 @@ export class CustomModulesController {
|
||||
return this.service.getOne(this.requireTenantId(req), user, id);
|
||||
}
|
||||
|
||||
/**
|
||||
* quick-261008-who: „Beim Start vorladen“ fuer den Aufrufer schalten. PUT mit
|
||||
* zwei Pfadsegmenten — kann `@Get(':id')` nicht verschatten. Kein `@Roles`:
|
||||
* jeder Angemeldete schaltet die eigene Wahl.
|
||||
*/
|
||||
@Put(':id/preload')
|
||||
async setPreload(
|
||||
@Req() req: AuthenticatedRequest,
|
||||
@CurrentUser() user: AuthUser,
|
||||
@Param('id') id: string,
|
||||
@Body() dto: SetCustomModulePreloadDto,
|
||||
) {
|
||||
return this.service.setPreload(this.requireTenantId(req), user, id, dto.preload);
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(
|
||||
@Req() req: AuthenticatedRequest,
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import { BadRequestException, ForbiddenException, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
ForbiddenException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Role } from '@prisma/client';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
@@ -43,7 +48,18 @@ function makeFakePrisma() {
|
||||
rows.delete(where.id);
|
||||
}),
|
||||
};
|
||||
return { customModule, rows };
|
||||
// quick-261008-who: Benutzerzeilen mit der Vorlade-Wahl (nur die eigene Zeile).
|
||||
const users = new Map<string, { customModulePreloadIds: string[] }>();
|
||||
const user = {
|
||||
findUnique: vi.fn(async ({ where }: { where: { id: string } }) => {
|
||||
return users.get(where.id) ?? { customModulePreloadIds: [] };
|
||||
}),
|
||||
update: vi.fn(async ({ where, data }: { where: { id: string }; data: any }) => {
|
||||
users.set(where.id, { customModulePreloadIds: data.customModulePreloadIds });
|
||||
return users.get(where.id);
|
||||
}),
|
||||
};
|
||||
return { customModule, user, users, rows };
|
||||
}
|
||||
|
||||
const dto = { name: 'Wiki', url: 'https://example.com', category: 'infrastructure' as const };
|
||||
@@ -62,7 +78,11 @@ function setup() {
|
||||
if (!KNOWN_CATEGORIES.has(key)) throw new BadRequestException('Unbekannte Kategorie');
|
||||
}),
|
||||
};
|
||||
return { prisma, categories, service: new CustomModulesService(prisma as any, categories as any) };
|
||||
return {
|
||||
prisma,
|
||||
categories,
|
||||
service: new CustomModulesService(prisma as any, categories as any),
|
||||
};
|
||||
}
|
||||
|
||||
describe('CustomModulesService — anlegen', () => {
|
||||
@@ -308,15 +328,114 @@ describe('CustomModulesService — RLS-Bindung', () => {
|
||||
await service.getOne('t1', userA, mine.id);
|
||||
await service.update('t1', userA, mine.id, { name: 'a' });
|
||||
await service.update('t1', admin, shared.id, { name: 'b' });
|
||||
const calls = vi.mocked(forTenant).mock.calls;
|
||||
// Die Vorlade-Wahl liegt in der Benutzerzeile und wird ohne Benutzerkontext
|
||||
// gelesen (Muster dashboardBackground) — diese Aufrufe hier ausblenden.
|
||||
const calls = vi.mocked(forTenant).mock.calls.filter((c) => c.length === 3 || c === undefined);
|
||||
// create shared: ohne Benutzer
|
||||
expect(calls[0]).toEqual([prisma, 't1']);
|
||||
expect(vi.mocked(forTenant).mock.calls[0]).toEqual([prisma, 't1']);
|
||||
// create personal + list + getOne + (update personal: Laden + Schreiben)
|
||||
expect(calls[0]).toEqual([prisma, 't1', 'ua']);
|
||||
expect(calls[1]).toEqual([prisma, 't1', 'ua']);
|
||||
expect(calls[2]).toEqual([prisma, 't1', 'ua']);
|
||||
expect(calls[3]).toEqual([prisma, 't1', 'ua']);
|
||||
// update shared als Admin: Laden mit Benutzer, Schreiben ohne
|
||||
expect(calls[calls.length - 2]).toEqual([prisma, 't1', 'admin1']);
|
||||
expect(calls[calls.length - 1]).toEqual([prisma, 't1']);
|
||||
const all = vi.mocked(forTenant).mock.calls;
|
||||
expect(all[all.length - 2]).toEqual([prisma, 't1', 'admin1']);
|
||||
expect(all[all.length - 1]).toEqual([prisma, 't1']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('CustomModulesService — Vorladen (quick-261008-who)', () => {
|
||||
it('list: preload nur fuer Module aus der eigenen Zeile, Benutzer B bleibt unberuehrt', async () => {
|
||||
const { prisma, service } = setup();
|
||||
const shared: any = await service.create('t1', admin, {
|
||||
...dto,
|
||||
name: 'Gemeinsam',
|
||||
shared: true,
|
||||
});
|
||||
await service.setPreload('t1', userA, shared.id, true);
|
||||
const resA: any[] = await service.list('t1', userA);
|
||||
expect(resA.find((r) => r.id === shared.id).preload).toBe(true);
|
||||
const resB: any[] = await service.list('t1', userB);
|
||||
expect(resB.find((r) => r.id === shared.id).preload).toBe(false);
|
||||
for (const call of prisma.user.findUnique.mock.calls) {
|
||||
expect(call[0].where).toHaveProperty('id');
|
||||
}
|
||||
expect(prisma.user.findUnique.mock.calls.at(-1)?.[0].where).toEqual({ id: 'ub' });
|
||||
});
|
||||
|
||||
it('getOne liefert preload fuer den Aufrufer', async () => {
|
||||
const { service } = setup();
|
||||
const mine: any = await service.create('t1', userA, dto);
|
||||
expect(((await service.getOne('t1', userA, mine.id)) as any).preload).toBe(false);
|
||||
await service.setPreload('t1', userA, mine.id, true);
|
||||
expect(((await service.getOne('t1', userA, mine.id)) as any).preload).toBe(true);
|
||||
});
|
||||
|
||||
it('setPreload(true) schreibt nur die eigene Zeile, ohne Doppelte, Antwort mit Zaehler', async () => {
|
||||
const { prisma, service } = setup();
|
||||
const mine: any = await service.create('t1', userA, dto);
|
||||
const first = await service.setPreload('t1', userA, mine.id, true);
|
||||
const second = await service.setPreload('t1', userA, mine.id, true);
|
||||
expect(first).toEqual({ id: mine.id, preload: true, preloadCount: 1 });
|
||||
expect(second).toEqual({ id: mine.id, preload: true, preloadCount: 1 });
|
||||
for (const call of prisma.user.update.mock.calls) {
|
||||
expect(call[0].where).toEqual({ id: 'ua' });
|
||||
}
|
||||
expect(prisma.users.get('ua')?.customModulePreloadIds).toEqual([mine.id]);
|
||||
});
|
||||
|
||||
it('setPreload bereinigt tote Kennungen beim Schreiben', async () => {
|
||||
const { prisma, service } = setup();
|
||||
const mine: any = await service.create('t1', userA, dto);
|
||||
prisma.users.set('ua', { customModulePreloadIds: ['geloescht-1', 'geloescht-2'] });
|
||||
await service.setPreload('t1', userA, mine.id, true);
|
||||
expect(prisma.users.get('ua')?.customModulePreloadIds).toEqual([mine.id]);
|
||||
});
|
||||
|
||||
it('setPreload(true) bei acht sichtbaren vorgeladenen -> 409 ohne Schreibzugriff', async () => {
|
||||
const { prisma, service } = setup();
|
||||
const ids: string[] = [];
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const m: any = await service.create('t1', userA, { ...dto, name: `M${i}` });
|
||||
ids.push(m.id);
|
||||
}
|
||||
for (const id of ids.slice(0, 8)) await service.setPreload('t1', userA, id, true);
|
||||
prisma.user.update.mockClear();
|
||||
await expect(service.setPreload('t1', userA, ids[8], true)).rejects.toBeInstanceOf(
|
||||
ConflictException,
|
||||
);
|
||||
expect(prisma.user.update).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('tote Kennungen zaehlen bei der Grenze nicht mit', async () => {
|
||||
const { prisma, service } = setup();
|
||||
const mine: any = await service.create('t1', userA, dto);
|
||||
prisma.users.set('ua', {
|
||||
customModulePreloadIds: Array.from({ length: 8 }, (_, i) => `tot-${i}`),
|
||||
});
|
||||
await expect(service.setPreload('t1', userA, mine.id, true)).resolves.toMatchObject({
|
||||
preloadCount: 1,
|
||||
});
|
||||
});
|
||||
|
||||
it('setPreload(false) entfernt die Kennung', async () => {
|
||||
const { prisma, service } = setup();
|
||||
const mine: any = await service.create('t1', userA, dto);
|
||||
await service.setPreload('t1', userA, mine.id, true);
|
||||
const res = await service.setPreload('t1', userA, mine.id, false);
|
||||
expect(res).toEqual({ id: mine.id, preload: false, preloadCount: 0 });
|
||||
expect(prisma.users.get('ua')?.customModulePreloadIds).toEqual([]);
|
||||
});
|
||||
|
||||
it('fremde persoenliche oder unbekannte Kennung -> 404 ohne Schreibzugriff', async () => {
|
||||
const { prisma, service } = setup();
|
||||
const mine: any = await service.create('t1', userA, dto);
|
||||
await expect(service.setPreload('t1', userB, mine.id, true)).rejects.toBeInstanceOf(
|
||||
NotFoundException,
|
||||
);
|
||||
await expect(service.setPreload('t1', userA, 'nope', true)).rejects.toBeInstanceOf(
|
||||
NotFoundException,
|
||||
);
|
||||
expect(prisma.user.update).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
import { ForbiddenException, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import {
|
||||
ConflictException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Role } from '@prisma/client';
|
||||
import { CUSTOM_MODULE_PRELOAD_MAX } from '@tessera/shared';
|
||||
import { ModuleCategoriesService } from '../module-categories/module-categories.service';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { forTenant } from '../prisma/prisma-tenant.extension';
|
||||
@@ -28,9 +34,17 @@ function isAdmin(caller: CustomModuleCaller): boolean {
|
||||
}
|
||||
|
||||
/** Aus der Zeile die Antwort machen: `personal` statt der Besitzer-Kennung. */
|
||||
function toResponse<T extends { ownerUserId: string | null }>(row: T) {
|
||||
function toResponse<T extends { id: string; ownerUserId: string | null }>(
|
||||
row: T,
|
||||
preloadIds?: ReadonlySet<string>,
|
||||
) {
|
||||
const { ownerUserId, ...rest } = row;
|
||||
return { ...rest, personal: ownerUserId !== null };
|
||||
// `preload` nur dort, wo die Wahl des Aufrufers gelesen wurde (list/getOne).
|
||||
return {
|
||||
...rest,
|
||||
personal: ownerUserId !== null,
|
||||
...(preloadIds ? { preload: preloadIds.has(row.id) } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -55,6 +69,15 @@ function toResponse<T extends { ownerUserId: string | null }>(row: T) {
|
||||
* Rollenpruefung (Administrator) sitzt vorher im Dienst. Zusaetzlich pruefen
|
||||
* alle Methoden `row.tenantId` und `row.ownerUserId` in der Anwendung, solange
|
||||
* der RLS-Schalter aus ist.
|
||||
*
|
||||
* VORLADEN (quick-261008-who): Welche Module ein Benutzer beim Start vorladen
|
||||
* laesst, steht in SEINER Benutzerzeile (`User.customModulePreloadIds`, Muster
|
||||
* `dashboardBackground`) — nicht am Modul, damit die Wahl eines Benutzers nie
|
||||
* auf einen anderen wirkt. Gelesen und geschrieben wird nur die eigene Zeile
|
||||
* (`where: { id: caller.id }`, kein Kennungsparameter). `list`/`getOne` melden
|
||||
* `preload` nur fuer Module, die der Aufrufer sieht; tote Kennungen
|
||||
* (geloeschte oder entzogene Module) werden dabei geschnitten und beim
|
||||
* naechsten Schreiben entfernt. Grenze: `CUSTOM_MODULE_PRELOAD_MAX` (409).
|
||||
*/
|
||||
@Injectable()
|
||||
export class CustomModulesService {
|
||||
@@ -71,14 +94,67 @@ export class CustomModulesService {
|
||||
orderBy: { name: 'asc' },
|
||||
select: CUSTOM_MODULE_SELECT,
|
||||
});
|
||||
return rows.map(toResponse);
|
||||
const preloadIds = await this.readPreloadIds(tenantId, caller);
|
||||
return rows.map((row) => toResponse(row, preloadIds));
|
||||
}
|
||||
|
||||
async getOne(tenantId: string, caller: CustomModuleCaller, id: string) {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId, caller.id);
|
||||
const row = await this.loadVisible(tenantPrisma, tenantId, caller, id);
|
||||
const { tenantId: _omit, ...result } = row;
|
||||
return toResponse(result);
|
||||
const preloadIds = await this.readPreloadIds(tenantId, caller);
|
||||
return toResponse(result, preloadIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* „Beim Start vorladen“ fuer den Aufrufer ein- oder ausschalten. 404 fuer
|
||||
* fremde oder fehlende Kennungen (ohne Schreibzugriff), 409 ab
|
||||
* `CUSTOM_MODULE_PRELOAD_MAX` sichtbaren vorgeladenen Modulen. Geschrieben
|
||||
* wird die bereinigte Liste (nur sichtbare Module, ohne Doppelte).
|
||||
*/
|
||||
async setPreload(tenantId: string, caller: CustomModuleCaller, id: string, preload: boolean) {
|
||||
const tenantPrisma = forTenant(this.prisma, tenantId, caller.id);
|
||||
await this.loadVisible(tenantPrisma, tenantId, caller, id);
|
||||
const visible = await tenantPrisma.customModule.findMany({
|
||||
where: { tenantId, OR: [{ ownerUserId: null }, { ownerUserId: caller.id }] },
|
||||
select: { id: true },
|
||||
});
|
||||
const visibleIds = new Set(visible.map((row) => row.id));
|
||||
const stored = await this.readPreloadIds(tenantId, caller);
|
||||
const current = [...stored].filter((storedId) => visibleIds.has(storedId));
|
||||
|
||||
let next: string[];
|
||||
if (preload) {
|
||||
if (current.includes(id)) {
|
||||
next = current;
|
||||
} else if (current.length >= CUSTOM_MODULE_PRELOAD_MAX) {
|
||||
throw new ConflictException(
|
||||
`Es können höchstens ${CUSTOM_MODULE_PRELOAD_MAX} eigene Module beim Start vorgeladen werden`,
|
||||
);
|
||||
} else {
|
||||
next = [...current, id];
|
||||
}
|
||||
} else {
|
||||
next = current.filter((storedId) => storedId !== id);
|
||||
}
|
||||
|
||||
// Nur die eigene Zeile (T-WHO-01): kein Kennungsparameter vom Client.
|
||||
const userPrisma = forTenant(this.prisma, tenantId);
|
||||
await userPrisma.user.update({
|
||||
where: { id: caller.id },
|
||||
data: { customModulePreloadIds: next },
|
||||
});
|
||||
return { id, preload, preloadCount: next.length };
|
||||
}
|
||||
|
||||
/** Gespeicherte Vorlade-Kennungen des Aufrufers (nur seine eigene Zeile). */
|
||||
private async readPreloadIds(tenantId: string, caller: CustomModuleCaller) {
|
||||
const userPrisma = forTenant(this.prisma, tenantId);
|
||||
const user = await userPrisma.user.findUnique({
|
||||
where: { id: caller.id },
|
||||
select: { customModulePreloadIds: true },
|
||||
});
|
||||
return new Set(user?.customModulePreloadIds ?? []);
|
||||
}
|
||||
|
||||
async create(tenantId: string, caller: CustomModuleCaller, dto: CreateCustomModuleDto) {
|
||||
|
||||
@@ -2,7 +2,11 @@ import 'reflect-metadata';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { CreateCustomModuleDto, UpdateCustomModuleDto } from './custom-module.dto';
|
||||
import {
|
||||
CreateCustomModuleDto,
|
||||
SetCustomModulePreloadDto,
|
||||
UpdateCustomModuleDto,
|
||||
} from './custom-module.dto';
|
||||
|
||||
async function errorsFor<T extends object>(cls: new () => T, plain: Record<string, unknown>) {
|
||||
const dto = plainToInstance(cls, plain);
|
||||
@@ -31,21 +35,25 @@ describe('CreateCustomModuleDto', () => {
|
||||
|
||||
// quick-261003-387: das DTO prueft nur das Format der Kennung; ob die
|
||||
// Organisation sie fuehrt, entscheidet der Dienst (400, siehe Dienst-Spec).
|
||||
it.each(['', 'Gross', 'mit leerzeichen', '-fuehrend', 'x'.repeat(61)])(
|
||||
'lehnt die Kategorie-Kennung %j ab',
|
||||
async (category) => {
|
||||
expect(await errorsFor(CreateCustomModuleDto, { ...valid, category })).toContain('category');
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
'',
|
||||
'Gross',
|
||||
'mit leerzeichen',
|
||||
'-fuehrend',
|
||||
'x'.repeat(61),
|
||||
])('lehnt die Kategorie-Kennung %j ab', async (category) => {
|
||||
expect(await errorsFor(CreateCustomModuleDto, { ...valid, category })).toContain('category');
|
||||
});
|
||||
|
||||
it.each(['fleet', 'custom-modules', 'werkzeuge-tools'])(
|
||||
'akzeptiert die Kategorie-Kennung %s',
|
||||
async (category) => {
|
||||
expect(await errorsFor(CreateCustomModuleDto, { ...valid, category })).not.toContain(
|
||||
'category',
|
||||
);
|
||||
},
|
||||
);
|
||||
it.each([
|
||||
'fleet',
|
||||
'custom-modules',
|
||||
'werkzeuge-tools',
|
||||
])('akzeptiert die Kategorie-Kennung %s', async (category) => {
|
||||
expect(await errorsFor(CreateCustomModuleDto, { ...valid, category })).not.toContain(
|
||||
'category',
|
||||
);
|
||||
});
|
||||
|
||||
it.each(['', ' '])('lehnt den Namen %j ab', async (name) => {
|
||||
expect(await errorsFor(CreateCustomModuleDto, { ...valid, name })).toContain('name');
|
||||
@@ -73,7 +81,9 @@ describe('UpdateCustomModuleDto', () => {
|
||||
|
||||
it('prueft jedes gesetzte Feld gleich', async () => {
|
||||
expect(await errorsFor(UpdateCustomModuleDto, { url: 'http://example.com' })).toContain('url');
|
||||
expect(await errorsFor(UpdateCustomModuleDto, { category: 'Nicht Gueltig' })).toContain('category');
|
||||
expect(await errorsFor(UpdateCustomModuleDto, { category: 'Nicht Gueltig' })).toContain(
|
||||
'category',
|
||||
);
|
||||
expect(await errorsFor(UpdateCustomModuleDto, { name: ' ' })).toContain('name');
|
||||
});
|
||||
|
||||
@@ -85,3 +95,16 @@ describe('UpdateCustomModuleDto', () => {
|
||||
expect(await errorsFor(UpdateCustomModuleDto, { [field]: null })).toContain(field);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SetCustomModulePreloadDto (quick-261008-who)', () => {
|
||||
it('akzeptiert true und false', async () => {
|
||||
expect(await errorsFor(SetCustomModulePreloadDto, { preload: true })).toEqual([]);
|
||||
expect(await errorsFor(SetCustomModulePreloadDto, { preload: false })).toEqual([]);
|
||||
});
|
||||
|
||||
it('lehnt fehlendes oder nicht-boolesches preload ab', async () => {
|
||||
expect(await errorsFor(SetCustomModulePreloadDto, {})).toContain('preload');
|
||||
expect(await errorsFor(SetCustomModulePreloadDto, { preload: 'ja' })).toContain('preload');
|
||||
expect(await errorsFor(SetCustomModulePreloadDto, { preload: 1 })).toContain('preload');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,3 +90,9 @@ export class UpdateCustomModuleDto extends PartialType(
|
||||
OmitType(CreateCustomModuleDto, ['shared'] as const),
|
||||
{ skipNullProperties: false },
|
||||
) {}
|
||||
|
||||
/** quick-261008-who: Koerper von `PUT /custom-modules/:id/preload`. */
|
||||
export class SetCustomModulePreloadDto {
|
||||
@IsBoolean()
|
||||
preload!: boolean;
|
||||
}
|
||||
|
||||
@@ -262,7 +262,8 @@ export class UserController {
|
||||
throw new ForbiddenException('Cannot access users from other tenants');
|
||||
}
|
||||
|
||||
const { passwordHash, ...result } = user;
|
||||
// quick-261008-who (T-WHO-02): die persoenliche Vorlade-Wahl verlaesst die Antwort nicht.
|
||||
const { passwordHash, customModulePreloadIds, ...result } = user;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -293,7 +294,8 @@ export class UserController {
|
||||
tenantId,
|
||||
});
|
||||
|
||||
const { passwordHash, ...result } = user;
|
||||
// quick-261008-who (T-WHO-02): die persoenliche Vorlade-Wahl verlaesst die Antwort nicht.
|
||||
const { passwordHash, customModulePreloadIds, ...result } = user;
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -351,7 +353,7 @@ export class UserController {
|
||||
isActive: dto.isActive,
|
||||
});
|
||||
|
||||
const { passwordHash, ...result } = updated;
|
||||
const { passwordHash, customModulePreloadIds, ...result } = updated;
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user