From af0c93fd3421e32230e4f5e525caa06bf72f06cd Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Thu, 25 Jun 2026 21:51:08 +0300 Subject: [PATCH] fix: harden portfolio DTO validation for quantity and date fields --- .../modules/portfolio/dto/add-position.dto.ts | 5 ++- .../portfolio/dto/position.dto.spec.ts | 43 +++++++++++++++++++ .../portfolio/dto/update-position.dto.ts | 5 ++- 3 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 apps/backend/src/modules/portfolio/dto/position.dto.spec.ts diff --git a/apps/backend/src/modules/portfolio/dto/add-position.dto.ts b/apps/backend/src/modules/portfolio/dto/add-position.dto.ts index 7ed69b3..ea9e01f 100644 --- a/apps/backend/src/modules/portfolio/dto/add-position.dto.ts +++ b/apps/backend/src/modules/portfolio/dto/add-position.dto.ts @@ -8,6 +8,7 @@ import { IsIn, MaxLength, MinLength, + IsDateString, } from 'class-validator'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; @@ -31,7 +32,7 @@ export class AddPositionDto { @ApiProperty({ example: 10 }) @IsInt() - @Min(0) + @Min(1) quantity!: number; @ApiPropertyOptional({ example: 250.5 }) @@ -41,7 +42,7 @@ export class AddPositionDto { buyPrice?: number; @ApiPropertyOptional({ example: '2026-06-01' }) - @IsString() + @IsDateString() @IsOptional() buyDate?: string; diff --git a/apps/backend/src/modules/portfolio/dto/position.dto.spec.ts b/apps/backend/src/modules/portfolio/dto/position.dto.spec.ts new file mode 100644 index 0000000..9034cba --- /dev/null +++ b/apps/backend/src/modules/portfolio/dto/position.dto.spec.ts @@ -0,0 +1,43 @@ +import 'reflect-metadata'; +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { AddPositionDto } from './add-position.dto'; +import { UpdatePositionDto } from './update-position.dto'; + +describe('position DTO validation', () => { + const validateDto = async (cls: new () => T, payload: Record) => + validate(plainToInstance(cls, payload)); + + it('rejects zero quantity when adding a position', async () => { + const errors = await validateDto(AddPositionDto, { secid: 'SBER', quantity: 0 }); + + expect(errors.some((error) => error.property === 'quantity')).toBe(true); + }); + + it('rejects zero quantity when updating a position', async () => { + const errors = await validateDto(UpdatePositionDto, { quantity: 0 }); + + expect(errors.some((error) => error.property === 'quantity')).toBe(true); + }); + + it('rejects invalid buyDate values', async () => { + const addErrors = await validateDto(AddPositionDto, { + secid: 'SBER', + quantity: 1, + buyDate: 'not-a-date', + }); + const updateErrors = await validateDto(UpdatePositionDto, { buyDate: 'not-a-date' }); + + expect(addErrors.some((error) => error.property === 'buyDate')).toBe(true); + expect(updateErrors.some((error) => error.property === 'buyDate')).toBe(true); + }); + + it('accepts valid position payloads', async () => { + await expect( + validateDto(AddPositionDto, { secid: 'SBER', quantity: 1, buyDate: '2026-06-01' }), + ).resolves.toHaveLength(0); + await expect( + validateDto(UpdatePositionDto, { quantity: 2, buyDate: '2026-06-15' }), + ).resolves.toHaveLength(0); + }); +}); diff --git a/apps/backend/src/modules/portfolio/dto/update-position.dto.ts b/apps/backend/src/modules/portfolio/dto/update-position.dto.ts index 2aa0cd7..edb83aa 100644 --- a/apps/backend/src/modules/portfolio/dto/update-position.dto.ts +++ b/apps/backend/src/modules/portfolio/dto/update-position.dto.ts @@ -7,6 +7,7 @@ import { IsArray, IsIn, MaxLength, + IsDateString, } from 'class-validator'; import { ApiPropertyOptional } from '@nestjs/swagger'; @@ -24,7 +25,7 @@ const TAGS = [ export class UpdatePositionDto { @ApiPropertyOptional({ example: 15 }) @IsInt() - @Min(0) + @Min(1) @IsOptional() quantity?: number; @@ -35,7 +36,7 @@ export class UpdatePositionDto { buyPrice?: number; @ApiPropertyOptional({ example: '2026-06-15' }) - @IsString() + @IsDateString() @IsOptional() buyDate?: string;