diff --git a/backend/src/list/dto/add-to-list.dto.ts b/backend/src/list/dto/add-to-list.dto.ts new file mode 100644 index 0000000..1af961d --- /dev/null +++ b/backend/src/list/dto/add-to-list.dto.ts @@ -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; +} diff --git a/backend/src/list/dto/dto-validation.spec.ts b/backend/src/list/dto/dto-validation.spec.ts new file mode 100644 index 0000000..886e12e --- /dev/null +++ b/backend/src/list/dto/dto-validation.spec.ts @@ -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( + cls: new () => T, + plain: Record, +): Promise { + 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); + }); +}); diff --git a/backend/src/list/dto/set-score.dto.ts b/backend/src/list/dto/set-score.dto.ts new file mode 100644 index 0000000..12734d2 --- /dev/null +++ b/backend/src/list/dto/set-score.dto.ts @@ -0,0 +1,8 @@ +import { IsNumber, Min, Max } from 'class-validator'; + +export class SetScoreDto { + @IsNumber() + @Min(0) + @Max(10) + score: number; +} diff --git a/backend/src/list/dto/update-progress.dto.ts b/backend/src/list/dto/update-progress.dto.ts new file mode 100644 index 0000000..9ac3218 --- /dev/null +++ b/backend/src/list/dto/update-progress.dto.ts @@ -0,0 +1,7 @@ +import { IsInt, Min } from 'class-validator'; + +export class UpdateProgressDto { + @IsInt() + @Min(0) + progress: number; +} diff --git a/backend/src/list/dto/update-status.dto.ts b/backend/src/list/dto/update-status.dto.ts new file mode 100644 index 0000000..2ab00b3 --- /dev/null +++ b/backend/src/list/dto/update-status.dto.ts @@ -0,0 +1,7 @@ +import { IsEnum } from 'class-validator'; +import { ListStatus } from '../user-work.entity'; + +export class UpdateStatusDto { + @IsEnum(ListStatus) + status: ListStatus; +} diff --git a/backend/src/list/list.controller.ts b/backend/src/list/list.controller.ts index 39a6182..7d9846f 100644 --- a/backend/src/list/list.controller.ts +++ b/backend/src/list/list.controller.ts @@ -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,