fix(domains): null in den Einstellungen ergibt 400 statt 500 (WR-04)

@IsOptional liess null durch; der Dienst scheiterte dann an trim()/map() oder an
einer NOT-NULL-Spalte. Felder duerfen fehlen, aber nicht null sein (Kontext-
nummern bleiben ausdruecklich null-faehig).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-10-08 11:27:41 +02:00
parent 9ecf191fb4
commit 268d6d55a4
2 changed files with 61 additions and 10 deletions
@@ -0,0 +1,41 @@
import 'reflect-metadata';
import { plainToInstance } from 'class-transformer';
import { validate } from 'class-validator';
import { describe, expect, it } from 'vitest';
import { SaveDomainsSettingsDto } from './domains-settings.dto';
async function errorsOf(plain: Record<string, unknown>) {
const errors = await validate(plainToInstance(SaveDomainsSettingsDto, plain));
return errors.map((e) => e.property).sort();
}
describe('SaveDomainsSettingsDto — null ist keine fehlende Angabe', () => {
it('ein leerer Rumpf und nur gesetzte Felder sind gueltig', async () => {
expect(await errorsOf({})).toEqual([]);
expect(
await errorsOf({
environment: 'DEMO',
demoUser: 'u',
demoPassword: '',
defaultNameServers: ['ns1.example.com', 'ns2.example.com'],
}),
).toEqual([]);
});
it.each([
'environment',
'confirmLive',
'demoUser',
'demoPassword',
'liveUser',
'livePassword',
'defaultNameServers',
])('%s: null -> Validierungsfehler (400) statt TypeError im Dienst', async (field) => {
expect(await errorsOf({ [field]: null })).toEqual([field]);
});
it('Kontextnummern duerfen ausdruecklich null sein (Zugang loeschen)', async () => {
expect(await errorsOf({ demoContext: null, liveContext: null })).toEqual([]);
expect(await errorsOf({ demoContext: 'x' })).toEqual(['demoContext']);
});
});
@@ -4,7 +4,6 @@ import {
IsBoolean, IsBoolean,
IsIn, IsIn,
IsInt, IsInt,
IsOptional,
IsString, IsString,
Max, Max,
MaxLength, MaxLength,
@@ -12,6 +11,17 @@ import {
ValidateIf, ValidateIf,
} from 'class-validator'; } from 'class-validator';
/**
* Teilspeichern: ein Feld darf FEHLEN (`undefined`), aber nicht `null` sein —
* `@IsOptional()` liesse `null` durch und der Dienst scheiterte dann mit einem
* 500 statt einer 400 (`null.trim()`, `null.map()`, NOT-NULL-Spalte).
*/
const isPresent = (_object: unknown, value: unknown): boolean => value !== undefined;
/** Kontextnummern duerfen zusaetzlich ausdruecklich `null` sein (= Zugang loeschen). */
const isPresentOrNull = (_object: unknown, value: unknown): boolean =>
value !== null && value !== undefined;
/** /**
* Einstellungen des Moduls Domains (quick-261008-dts). Jedes Feld ist * Einstellungen des Moduls Domains (quick-261008-dts). Jedes Feld ist
* optional — gespeichert wird nur, was mitgeschickt wird (jede Karte der * optional — gespeichert wird nur, was mitgeschickt wird (jede Karte der
@@ -19,48 +29,48 @@ import {
* Passwort laesst das gespeicherte unveraendert. * Passwort laesst das gespeicherte unveraendert.
*/ */
export class SaveDomainsSettingsDto { export class SaveDomainsSettingsDto {
@IsOptional() @ValidateIf(isPresent)
@IsIn(['DEMO', 'LIVE']) @IsIn(['DEMO', 'LIVE'])
environment?: 'DEMO' | 'LIVE'; environment?: 'DEMO' | 'LIVE';
/** Ausdrueckliche Bestaetigung fuer den Wechsel auf das Live-System. */ /** Ausdrueckliche Bestaetigung fuer den Wechsel auf das Live-System. */
@IsOptional() @ValidateIf(isPresent)
@IsBoolean() @IsBoolean()
confirmLive?: boolean; confirmLive?: boolean;
@IsOptional() @ValidateIf(isPresent)
@IsString() @IsString()
@MaxLength(100) @MaxLength(100)
demoUser?: string; demoUser?: string;
@IsOptional() @ValidateIf(isPresent)
@IsString() @IsString()
@MaxLength(200) @MaxLength(200)
demoPassword?: string; demoPassword?: string;
@ValidateIf((_o, value) => value !== null && value !== undefined) @ValidateIf(isPresentOrNull)
@IsInt() @IsInt()
@Min(1) @Min(1)
@Max(2147483647) @Max(2147483647)
demoContext?: number | null; demoContext?: number | null;
@IsOptional() @ValidateIf(isPresent)
@IsString() @IsString()
@MaxLength(100) @MaxLength(100)
liveUser?: string; liveUser?: string;
@IsOptional() @ValidateIf(isPresent)
@IsString() @IsString()
@MaxLength(200) @MaxLength(200)
livePassword?: string; livePassword?: string;
@ValidateIf((_o, value) => value !== null && value !== undefined) @ValidateIf(isPresentOrNull)
@IsInt() @IsInt()
@Min(1) @Min(1)
@Max(2147483647) @Max(2147483647)
liveContext?: number | null; liveContext?: number | null;
@IsOptional() @ValidateIf(isPresent)
@IsArray() @IsArray()
@ArrayMaxSize(6) @ArrayMaxSize(6)
@IsString({ each: true }) @IsString({ each: true })