Skip to content

Commit 7a2342d

Browse files
Merge pull request #128 from Rule-34/auto-triage/326-tag-ampersand-api
2 parents 9230ff6 + 0707a3b commit 7a2342d

2 files changed

Lines changed: 100 additions & 1 deletion

File tree

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { BadRequestException } from '@nestjs/common'
2+
import { plainToInstance } from 'class-transformer'
3+
import { booruQueryValuesPostsDTO } from './booru-queries.dto'
4+
5+
describe('booruQueryValuesPostsDTO', () => {
6+
describe('tags transform', () => {
7+
it('should decode URL-encoded ampersands in tags', () => {
8+
const dto = plainToInstance(booruQueryValuesPostsDTO, {
9+
tags: 'panty_%26_stocking_with_garterbelt'
10+
})
11+
12+
expect(dto.tags).toEqual(['panty_&_stocking_with_garterbelt'])
13+
})
14+
15+
it('should split pipe-separated tags and decode each one', () => {
16+
const dto = plainToInstance(booruQueryValuesPostsDTO, {
17+
tags: 'panty_%26_stocking_with_garterbelt|rating%3Asafe'
18+
})
19+
20+
expect(dto.tags).toEqual(['panty_&_stocking_with_garterbelt', 'rating:safe'])
21+
})
22+
23+
it('should normalize array tag inputs and keep tag array shape', () => {
24+
const dto = plainToInstance(booruQueryValuesPostsDTO, {
25+
tags: ['panty_%26_stocking_with_garterbelt|rating%3Asafe', 'score%3A%3E100']
26+
})
27+
28+
expect(dto.tags).toEqual([
29+
'panty_&_stocking_with_garterbelt',
30+
'rating:safe',
31+
'score:>100'
32+
])
33+
})
34+
35+
it('should normalize non-string tag input without throwing', () => {
36+
const dto = plainToInstance(booruQueryValuesPostsDTO, {
37+
tags: 123
38+
})
39+
40+
expect(dto.tags).toEqual(['123'])
41+
})
42+
43+
it('should keep non-encoded percent tags unchanged', () => {
44+
const dto = plainToInstance(booruQueryValuesPostsDTO, {
45+
tags: '100%_real'
46+
})
47+
48+
expect(dto.tags).toEqual(['100%_real'])
49+
})
50+
51+
it('should throw BadRequestException when encoded tag decoding fails', () => {
52+
expect(() =>
53+
plainToInstance(booruQueryValuesPostsDTO, {
54+
tags: 'bad%25%'
55+
})
56+
).toThrow(BadRequestException)
57+
58+
expect(() =>
59+
plainToInstance(booruQueryValuesPostsDTO, {
60+
tags: 'bad%25%'
61+
})
62+
).toThrow('Invalid tag encoding')
63+
})
64+
65+
it('should return undefined when tags is undefined', () => {
66+
const dto = plainToInstance(booruQueryValuesPostsDTO, {})
67+
68+
expect(dto.tags).toBeUndefined()
69+
})
70+
71+
it('should return null when tags is null', () => {
72+
const dto = plainToInstance(booruQueryValuesPostsDTO, {
73+
tags: null
74+
})
75+
76+
expect(dto.tags).toBeNull()
77+
})
78+
})
79+
})

src/booru/dto/booru-queries.dto.ts

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
Min
1919
} from 'class-validator'
2020
import { Transform } from 'class-transformer'
21+
import { BadRequestException } from '@nestjs/common'
2122

2223
abstract class booruEndpointsDTO {
2324
@IsFQDN()
@@ -180,7 +181,26 @@ export class booruQueryValuesPostsDTO extends booruQueriesDTO {
180181
@IsArray()
181182
@ArrayNotEmpty()
182183
@ArrayNotContains([''])
183-
@Transform(({ value }) => value.trim().split('|'))
184+
@Transform(({ value }) => {
185+
if (value === undefined || value === null) {
186+
return value
187+
}
188+
189+
return (Array.isArray(value) ? value : [value])
190+
.map((tag) => (typeof tag === 'string' ? tag : String(tag)))
191+
.flatMap((tag) => tag.trim().split('|'))
192+
.map((tag) => {
193+
if (!/%[0-9A-Fa-f]{2}/.test(tag)) {
194+
return tag
195+
}
196+
197+
try {
198+
return decodeURIComponent(tag)
199+
} catch {
200+
throw new BadRequestException('Invalid tag encoding')
201+
}
202+
})
203+
})
184204
@IsOptional()
185205
readonly tags: IBooruQueryValues['posts']['tags']
186206

0 commit comments

Comments
 (0)