refacto: DTOs class-validator — validation active sur tous les endpoints

This commit is contained in:
2026-04-05 07:41:01 +02:00
parent 6dcb6bf4b5
commit 075afa1063
6 changed files with 209 additions and 4 deletions

View File

@@ -0,0 +1,11 @@
import { IsEnum, IsInt, Min } from 'class-validator';
import { ListStatus } from '../user-work.entity';
export class AddToListDto {
@IsInt()
@Min(1)
anilistId: number;
@IsEnum(ListStatus)
status: ListStatus;
}

View File

@@ -0,0 +1,168 @@
import { validate } from 'class-validator';
import { plainToInstance } from 'class-transformer';
import { AddToListDto } from './add-to-list.dto';
import { UpdateProgressDto } from './update-progress.dto';
import { UpdateStatusDto } from './update-status.dto';
import { SetScoreDto } from './set-score.dto';
import { ListStatus } from '../user-work.entity';
// Helper: transform plain object to class instance and validate
async function check<T extends object>(
cls: new () => T,
plain: Record<string, any>,
): Promise<string[]> {
const instance = plainToInstance(cls, plain);
const errors = await validate(instance);
return errors.flatMap((e) => Object.values(e.constraints ?? {}));
}
// ─── AddToListDto ─────────────────────────────────────────────
describe('AddToListDto', () => {
it('should accept valid input', async () => {
const errors = await check(AddToListDto, {
anilistId: 12345,
status: ListStatus.WATCHING,
});
expect(errors).toHaveLength(0);
});
it('should reject missing anilistId', async () => {
const errors = await check(AddToListDto, {
status: ListStatus.WATCHING,
});
expect(errors.length).toBeGreaterThan(0);
});
it('should reject non-integer anilistId', async () => {
const errors = await check(AddToListDto, {
anilistId: 'abc',
status: ListStatus.WATCHING,
});
expect(errors.length).toBeGreaterThan(0);
});
it('should reject anilistId < 1', async () => {
const errors = await check(AddToListDto, {
anilistId: 0,
status: ListStatus.WATCHING,
});
expect(errors.length).toBeGreaterThan(0);
});
it('should reject missing status', async () => {
const errors = await check(AddToListDto, {
anilistId: 12345,
});
expect(errors.length).toBeGreaterThan(0);
});
it('should reject invalid status', async () => {
const errors = await check(AddToListDto, {
anilistId: 12345,
status: 'binge_watching',
});
expect(errors.length).toBeGreaterThan(0);
});
it('should accept all valid ListStatus values', async () => {
for (const status of Object.values(ListStatus)) {
const errors = await check(AddToListDto, { anilistId: 1, status });
expect(errors).toHaveLength(0);
}
});
});
// ─── UpdateProgressDto ───────────────────────────────────────
describe('UpdateProgressDto', () => {
it('should accept valid progress', async () => {
const errors = await check(UpdateProgressDto, { progress: 5 });
expect(errors).toHaveLength(0);
});
it('should accept progress = 0', async () => {
const errors = await check(UpdateProgressDto, { progress: 0 });
expect(errors).toHaveLength(0);
});
it('should reject negative progress', async () => {
const errors = await check(UpdateProgressDto, { progress: -1 });
expect(errors.length).toBeGreaterThan(0);
});
it('should reject non-integer progress', async () => {
const errors = await check(UpdateProgressDto, { progress: 3.5 });
expect(errors.length).toBeGreaterThan(0);
});
it('should reject missing progress', async () => {
const errors = await check(UpdateProgressDto, {});
expect(errors.length).toBeGreaterThan(0);
});
it('should reject string progress', async () => {
const errors = await check(UpdateProgressDto, { progress: 'five' });
expect(errors.length).toBeGreaterThan(0);
});
});
// ─── UpdateStatusDto ─────────────────────────────────────────
describe('UpdateStatusDto', () => {
it('should accept valid status', async () => {
const errors = await check(UpdateStatusDto, {
status: ListStatus.COMPLETED,
});
expect(errors).toHaveLength(0);
});
it('should reject missing status', async () => {
const errors = await check(UpdateStatusDto, {});
expect(errors.length).toBeGreaterThan(0);
});
it('should reject invalid status string', async () => {
const errors = await check(UpdateStatusDto, { status: 'finished' });
expect(errors.length).toBeGreaterThan(0);
});
});
// ─── SetScoreDto ─────────────────────────────────────────────
describe('SetScoreDto', () => {
it('should accept valid score', async () => {
const errors = await check(SetScoreDto, { score: 8.5 });
expect(errors).toHaveLength(0);
});
it('should accept score = 0', async () => {
const errors = await check(SetScoreDto, { score: 0 });
expect(errors).toHaveLength(0);
});
it('should accept score = 10', async () => {
const errors = await check(SetScoreDto, { score: 10 });
expect(errors).toHaveLength(0);
});
it('should reject score > 10', async () => {
const errors = await check(SetScoreDto, { score: 11 });
expect(errors.length).toBeGreaterThan(0);
});
it('should reject negative score', async () => {
const errors = await check(SetScoreDto, { score: -1 });
expect(errors.length).toBeGreaterThan(0);
});
it('should reject missing score', async () => {
const errors = await check(SetScoreDto, {});
expect(errors.length).toBeGreaterThan(0);
});
it('should reject string score', async () => {
const errors = await check(SetScoreDto, { score: 'great' });
expect(errors.length).toBeGreaterThan(0);
});
});

View File

@@ -0,0 +1,8 @@
import { IsNumber, Min, Max } from 'class-validator';
export class SetScoreDto {
@IsNumber()
@Min(0)
@Max(10)
score: number;
}

View File

@@ -0,0 +1,7 @@
import { IsInt, Min } from 'class-validator';
export class UpdateProgressDto {
@IsInt()
@Min(0)
progress: number;
}

View File

@@ -0,0 +1,7 @@
import { IsEnum } from 'class-validator';
import { ListStatus } from '../user-work.entity';
export class UpdateStatusDto {
@IsEnum(ListStatus)
status: ListStatus;
}

View File

@@ -15,6 +15,10 @@ import { ListService } from './list.service';
import { ListStatus } from './user-work.entity';
import { AuthGuard } from '../auth/auth.guard';
import { UserService } from '../user/user.service';
import { AddToListDto } from './dto/add-to-list.dto';
import { UpdateProgressDto } from './dto/update-progress.dto';
import { UpdateStatusDto } from './dto/update-status.dto';
import { SetScoreDto } from './dto/set-score.dto';
@Controller('api/list')
@UseGuards(AuthGuard)
@@ -37,7 +41,7 @@ export class ListController {
@Post()
async addToList(
@Req() req: any,
@Body() body: { anilistId: number; status: ListStatus },
@Body() body: AddToListDto,
) {
const user = await this.userService.findOrCreate({
id: req.user.id,
@@ -51,7 +55,7 @@ export class ListController {
async updateProgress(
@Req() req: any,
@Param('id', ParseIntPipe) id: number,
@Body() body: { progress: number },
@Body() body: UpdateProgressDto,
) {
const user = await this.userService.findOrCreate({
id: req.user.id,
@@ -65,7 +69,7 @@ export class ListController {
async updateStatus(
@Req() req: any,
@Param('id', ParseIntPipe) id: number,
@Body() body: { status: ListStatus },
@Body() body: UpdateStatusDto,
) {
const user = await this.userService.findOrCreate({
id: req.user.id,
@@ -79,7 +83,7 @@ export class ListController {
async setScore(
@Req() req: any,
@Param('id', ParseIntPipe) id: number,
@Body() body: { score: number },
@Body() body: SetScoreDto,
) {
const user = await this.userService.findOrCreate({
id: req.user.id,