diff --git a/src/booru/dto/booru-queries.dto.spec.ts b/src/booru/dto/booru-queries.dto.spec.ts new file mode 100644 index 0000000..9722383 --- /dev/null +++ b/src/booru/dto/booru-queries.dto.spec.ts @@ -0,0 +1,79 @@ +import { BadRequestException } from '@nestjs/common' +import { plainToInstance } from 'class-transformer' +import { booruQueryValuesPostsDTO } from './booru-queries.dto' + +describe('booruQueryValuesPostsDTO', () => { + describe('tags transform', () => { + it('should decode URL-encoded ampersands in tags', () => { + const dto = plainToInstance(booruQueryValuesPostsDTO, { + tags: 'panty_%26_stocking_with_garterbelt' + }) + + expect(dto.tags).toEqual(['panty_&_stocking_with_garterbelt']) + }) + + it('should split pipe-separated tags and decode each one', () => { + const dto = plainToInstance(booruQueryValuesPostsDTO, { + tags: 'panty_%26_stocking_with_garterbelt|rating%3Asafe' + }) + + expect(dto.tags).toEqual(['panty_&_stocking_with_garterbelt', 'rating:safe']) + }) + + it('should normalize array tag inputs and keep tag array shape', () => { + const dto = plainToInstance(booruQueryValuesPostsDTO, { + tags: ['panty_%26_stocking_with_garterbelt|rating%3Asafe', 'score%3A%3E100'] + }) + + expect(dto.tags).toEqual([ + 'panty_&_stocking_with_garterbelt', + 'rating:safe', + 'score:>100' + ]) + }) + + it('should normalize non-string tag input without throwing', () => { + const dto = plainToInstance(booruQueryValuesPostsDTO, { + tags: 123 + }) + + expect(dto.tags).toEqual(['123']) + }) + + it('should keep non-encoded percent tags unchanged', () => { + const dto = plainToInstance(booruQueryValuesPostsDTO, { + tags: '100%_real' + }) + + expect(dto.tags).toEqual(['100%_real']) + }) + + it('should throw BadRequestException when encoded tag decoding fails', () => { + expect(() => + plainToInstance(booruQueryValuesPostsDTO, { + tags: 'bad%25%' + }) + ).toThrow(BadRequestException) + + expect(() => + plainToInstance(booruQueryValuesPostsDTO, { + tags: 'bad%25%' + }) + ).toThrow('Invalid tag encoding') + }) + + it('should return undefined when tags is undefined', () => { + const dto = plainToInstance(booruQueryValuesPostsDTO, {}) + + expect(dto.tags).toBeUndefined() + }) + + it('should return null when tags is null', () => { + const dto = plainToInstance(booruQueryValuesPostsDTO, { + tags: null + }) + + expect(dto.tags).toBeNull() + }) + }) +}) \ No newline at end of file diff --git a/src/booru/dto/booru-queries.dto.ts b/src/booru/dto/booru-queries.dto.ts index 4a4bd12..3acc88e 100644 --- a/src/booru/dto/booru-queries.dto.ts +++ b/src/booru/dto/booru-queries.dto.ts @@ -18,6 +18,7 @@ import { Min } from 'class-validator' import { Transform } from 'class-transformer' +import { BadRequestException } from '@nestjs/common' abstract class booruEndpointsDTO { @IsFQDN() @@ -180,7 +181,26 @@ export class booruQueryValuesPostsDTO extends booruQueriesDTO { @IsArray() @ArrayNotEmpty() @ArrayNotContains(['']) - @Transform(({ value }) => value.trim().split('|')) + @Transform(({ value }) => { + if (value === undefined || value === null) { + return value + } + + return (Array.isArray(value) ? value : [value]) + .map((tag) => (typeof tag === 'string' ? tag : String(tag))) + .flatMap((tag) => tag.trim().split('|')) + .map((tag) => { + if (!/%[0-9A-Fa-f]{2}/.test(tag)) { + return tag + } + + try { + return decodeURIComponent(tag) + } catch { + throw new BadRequestException('Invalid tag encoding') + } + }) + }) @IsOptional() readonly tags: IBooruQueryValues['posts']['tags']