import { BadRequestException, ValidationPipe } from '@nestjs/common'; import { LoginDto, RegisterDto } from '../modules/auth/presentation/auth.dto'; import { CreateBankCardDto, UpdateBankCardDto, } from '../modules/banking/bank-cards/presentation/bank-card.dto'; describe('DTO validation', () => { const pipe = new ValidationPipe({ forbidUnknownValues: true, transform: true, whitelist: true }); it('requires credentials and normalizes a valid email', async () => { const metadata = { type: 'body' as const, metatype: RegisterDto }; await expect(pipe.transform({}, metadata)).rejects.toBeInstanceOf( BadRequestException, ); await expect( pipe.transform({ email: 'user@example.com' }, metadata), ).rejects.toBeInstanceOf(BadRequestException); await expect( pipe.transform( { email: ' User@Example.com ', password: 'long-enough-password' }, metadata, ), ).resolves.toEqual({ email: 'user@example.com', password: 'long-enough-password', }); }); // Test intent: register accepts 8..72 printable ASCII characters only (bcrypt // reads 72 bytes, so non-ASCII could be silently truncated); login only caps // bytes so accounts created under the older policy can still sign in. describe('password policy', () => { const register = { type: 'body' as const, metatype: RegisterDto }; const login = { type: 'body' as const, metatype: LoginDto }; const body = (password: string) => ({ email: 'user@example.com', password }); const accepts = (value: unknown, metadata: typeof login | typeof register) => pipe.transform(value, metadata).then( () => true, (error: unknown) => { if (!(error instanceof BadRequestException)) throw error; return false; }, ); it.each([ ['7 chars', 'a'.repeat(7), false], ['8 chars', 'a'.repeat(8), true], ['72 chars', 'a'.repeat(72), true], ['73 chars', 'a'.repeat(73), false], ['all printable symbols and space', 'Aa1 !~"#$%&\'()*+,-./:;<=>?@[\\]^_`{|}', true], ['Cyrillic, 8+ chars', 'пароль-пароль', false], ['emoji', 'passw😀rd', false], ['tab', 'pass\tword1', false], ['newline', 'password1\n', false], ['DEL', 'password\x7f1', false], ['non-string', 12345678, false], ])('register: %s', async (_, password, accepted) => { await expect(accepts(body(password as string), register)).resolves.toBe(accepted); }); it('register keeps raw whitespace untouched', async () => { const password = ' spaced pass '; await expect(pipe.transform(body(password), register)).resolves.toEqual( body(password), ); }); it.each([ ['legacy Cyrillic password', 'пароль-пароль', true], ['short legacy password', 'short', true], ['72 bytes', 'a'.repeat(72), true], ['73 bytes', 'a'.repeat(73), false], ['36 Cyrillic letters = 72 bytes', 'я'.repeat(36), true], ['37 Cyrillic letters = 74 bytes', 'я'.repeat(37), false], ['empty', '', false], ['non-string', 12345678, false], ])('login: %s', async (_, password, accepted) => { await expect(accepts(body(password as string), login)).resolves.toBe(accepted); }); }); it('requires and validates nested money in a new card', async () => { const metadata = { type: 'body' as const, metatype: CreateBankCardDto }; const card = { id: '00000000-0000-4000-8000-000000000001', name: 'Everyday', openingDate: '2026-09-13', }; for (const openingBalance of [undefined, null, {}, { currency: 'RUB' }]) { await expect( pipe.transform({ ...card, openingBalance }, metadata), ).rejects.toBeInstanceOf(BadRequestException); } const valid = { ...card, openingBalance: { currency: 'RUB', value: '100.00' }, }; await expect(pipe.transform(valid, metadata)).resolves.toEqual(valid); }); it('keeps update fields optional while requiring version and rejecting null', async () => { const metadata = { type: 'body' as const, metatype: UpdateBankCardDto }; await expect(pipe.transform({}, metadata)).rejects.toBeInstanceOf( BadRequestException, ); await expect( pipe.transform({ version: 1, name: null }, metadata), ).rejects.toBeInstanceOf(BadRequestException); await expect( pipe.transform({ version: 1 }, metadata), ).resolves.toMatchObject({ version: 1 }); }); });