/** * Boot-time ValidationPipe metatype self-check (#1391). * * The check exists to fail loud at boot when the global pipe cannot see a * guarded DTO's decorated properties — the #436 class-erasure signature and * its dependency-graph cousins. Red/green arms: * * GREEN real module state: BootstrapSetupDto's three properties are * decorated and visible through the globalThis-shared storage. * RED a control class with NO decorators (the erasure shape): the * check throws PipeMetatypeCheckError naming every property. * RED-2 a control where one property is decorated and two are not: the * error names exactly the missing two — the miss list is precise, * not a blanket failure. */ import { describe, expect, it } from 'vitest'; import { IsString } from 'class-validator'; import { assertValidationPipeSeesDtoDecorators, PipeMetatypeCheckError, } from './validation-pipe-check.js'; describe('assertValidationPipeSeesDtoDecorators (#1391 boot check)', () => { it('GREEN: passes on real module state (decorated DTO visible to the pipe)', () => { expect(() => assertValidationPipeSeesDtoDecorators()).not.toThrow(); }); it('RED control: a class whose properties lost their decorators throws, naming them', async () => { // Simulate metatype erasure: an undecorated class standing where a // decorated DTO should be. Redefine the guard table for the test by // importing the module and pointing its table at the eroded class — // the check reads the table at call time, so a fresh module instance // with a swapped table reproduces the boot failure deterministically. const { PIPE_GUARDED_DTOS } = await import('./validation-pipe-check.js'); class ErodedDto { name?: string; email?: string; password?: string; } const original = PIPE_GUARDED_DTOS[0]; expect(original).toBeDefined(); // Swap in the eroded target (same declared properties, zero decorators). ( PIPE_GUARDED_DTOS as unknown as Array<{ name: string; target: object; properties: string[] }> ).splice(0, PIPE_GUARDED_DTOS.length, { name: 'ErodedDto', target: ErodedDto, properties: ['name', 'email', 'password'], }); try { expect(() => assertValidationPipeSeesDtoDecorators()).toThrow(PipeMetatypeCheckError); try { assertValidationPipeSeesDtoDecorators(); } catch (err) { const message = err instanceof Error ? err.message : ''; expect(message).toContain('ErodedDto.name'); expect(message).toContain('ErodedDto.email'); expect(message).toContain('ErodedDto.password'); } } finally { // Restore real module state for any later test in this file. (PIPE_GUARDED_DTOS as unknown as unknown[]).splice(0, PIPE_GUARDED_DTOS.length, original); } // And confirm the restore is real. expect(() => assertValidationPipeSeesDtoDecorators()).not.toThrow(); }); it('RED-2 control: a partially decorated class names exactly the missing properties', async () => { const { PIPE_GUARDED_DTOS } = await import('./validation-pipe-check.js'); class HalfErodedDto { @IsString() name?: string; email?: string; password?: string; } const original = PIPE_GUARDED_DTOS[0]; ( PIPE_GUARDED_DTOS as unknown as Array<{ name: string; target: object; properties: string[] }> ).splice(0, PIPE_GUARDED_DTOS.length, { name: 'HalfErodedDto', target: HalfErodedDto, properties: ['name', 'email', 'password'], }); try { try { assertValidationPipeSeesDtoDecorators(); expect.unreachable('partially decorated DTO must fail the boot check'); } catch (err) { const message = err instanceof Error ? err.message : ''; expect(message).toContain('HalfErodedDto.email'); expect(message).toContain('HalfErodedDto.password'); expect(message).not.toContain('HalfErodedDto.name has no'); } } finally { (PIPE_GUARDED_DTOS as unknown as unknown[]).splice(0, PIPE_GUARDED_DTOS.length, original); } }); });