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:
@@ -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 })
|
||||||
|
|||||||
Reference in New Issue
Block a user