forked from wangziqi/gongxue-base
test: harden business boundary conditions
This commit is contained in:
31
apps/server/src/expenses/dto/expense.dto.spec.ts
Normal file
31
apps/server/src/expenses/dto/expense.dto.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import 'reflect-metadata';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import { BatchRoomExpenseDto } from './expense.dto';
|
||||
|
||||
describe('BatchRoomExpenseDto boundaries', () => {
|
||||
it.each([
|
||||
{ expenses: [] },
|
||||
{ expenses: [{ roomId: 1, expenseType: 'water', amount: 0 }] },
|
||||
{ expenses: [{ roomId: 1, expenseType: 'water', amount: -1 }] },
|
||||
{ expenses: [{ roomId: 1, expenseType: 'water', amount: 1.001 }] },
|
||||
{ periodStart: '2026-02-31' },
|
||||
])('rejects invalid batch payload %#', async (override) => {
|
||||
const dto = plainToInstance(BatchRoomExpenseDto, {
|
||||
periodStart: '2026-07-01',
|
||||
periodEnd: '2026-07-31',
|
||||
expenses: [{ roomId: 1, expenseType: 'water', amount: 10 }],
|
||||
...override,
|
||||
});
|
||||
await expect(validate(dto)).resolves.not.toHaveLength(0);
|
||||
});
|
||||
|
||||
it('accepts a valid batch payload', async () => {
|
||||
const dto = plainToInstance(BatchRoomExpenseDto, {
|
||||
periodStart: '2026-07-01',
|
||||
periodEnd: '2026-07-31',
|
||||
expenses: [{ roomId: 1, expenseType: 'water', amount: 10.25 }],
|
||||
});
|
||||
await expect(validate(dto)).resolves.toHaveLength(0);
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { IsDateString, IsIn, IsInt, IsString, IsNumber, IsOptional, Matches, Min } from 'class-validator';
|
||||
import { ArrayNotEmpty, IsArray, IsDateString, IsIn, IsInt, IsISO8601, IsString, IsNumber, IsOptional, Matches, Min, ValidateNested } from 'class-validator';
|
||||
import { PartialType } from '@nestjs/mapped-types';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
@@ -13,9 +13,13 @@ export class CreateRoomExpenseDto {
|
||||
@Min(0.01)
|
||||
amount: number;
|
||||
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
@IsISO8601({ strict: true })
|
||||
@IsDateString()
|
||||
periodStart: string;
|
||||
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
@IsISO8601({ strict: true })
|
||||
@IsDateString()
|
||||
periodEnd: string;
|
||||
|
||||
@@ -39,6 +43,8 @@ export class CreatePersonalExpenseDto {
|
||||
@Min(0.01)
|
||||
amount: number;
|
||||
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
@IsISO8601({ strict: true })
|
||||
@IsDateString()
|
||||
expenseDate: string;
|
||||
|
||||
@@ -74,14 +80,38 @@ export class QueryPersonalExpenseDto {
|
||||
studentId?: number;
|
||||
}
|
||||
|
||||
export class BatchRoomExpenseDto {
|
||||
export class BatchRoomExpenseItemDto {
|
||||
@IsInt()
|
||||
roomId: number;
|
||||
|
||||
@IsString()
|
||||
expenseType: string;
|
||||
|
||||
@IsNumber({ maxDecimalPlaces: 2 })
|
||||
@Min(0.01)
|
||||
amount: number;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export class BatchRoomExpenseDto {
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
@IsISO8601({ strict: true })
|
||||
@IsDateString()
|
||||
periodStart: string;
|
||||
|
||||
@IsString()
|
||||
@Matches(/^\d{4}-\d{2}-\d{2}$/)
|
||||
@IsISO8601({ strict: true })
|
||||
@IsDateString()
|
||||
periodEnd: string;
|
||||
|
||||
expenses: { roomId: number; expenseType: string; amount: number; description?: string }[];
|
||||
@IsArray()
|
||||
@ArrayNotEmpty()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => BatchRoomExpenseItemDto)
|
||||
expenses: BatchRoomExpenseItemDto[];
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user