Skip to content

Commit 11b6714

Browse files
committed
test(json): add unit tests for all formatter methods
- Add beautify and minify tests with roundtrip verification - Add error handling tests for invalid, incomplete, and trailing comma JSON - Add input validation tests for non-string and empty string rejection - Add prototype pollution and constructor key safety tests - Add sortKeyAsc and sortKeyDesc tests with recursive nesting - Add sortValueAsc and sortValueDesc tests with key order checks
1 parent a077a60 commit 11b6714

1 file changed

Lines changed: 337 additions & 0 deletions

File tree

tests/json-formatter.test.ts

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
import { assertEquals } from '@std/assert'
2+
import { JsonFormatter } from '@app/json-formatter.ts'
3+
4+
Deno.test('JsonFormatter - __proto__ key does not pollute', () => {
5+
JsonFormatter.sortKeyAsc('{"__proto__":{"polluted":true},"a":1}')
6+
assertEquals(({} as Record<string, unknown>)['polluted'], undefined)
7+
})
8+
9+
Deno.test('JsonFormatter - all methods reject non-string', () => {
10+
const methods = [
11+
'beautify',
12+
'minify',
13+
'sortKeyAsc',
14+
'sortKeyDesc',
15+
'sortValueAsc',
16+
'sortValueDesc'
17+
] as const
18+
for (const method of methods) {
19+
const result = JsonFormatter[method](null as unknown as string)
20+
assertEquals(result.error, 'Input must be a non-empty string')
21+
assertEquals(result.data, undefined)
22+
}
23+
})
24+
25+
Deno.test('JsonFormatter - beautify already pretty is idempotent', () => {
26+
const pretty = JSON.stringify({ a: 1 }, null, 2)
27+
assertEquals(JsonFormatter.beautify(pretty), { data: pretty })
28+
})
29+
30+
Deno.test('JsonFormatter - beautify array', () => {
31+
assertEquals(JsonFormatter.beautify('[1,2,3]'), { data: JSON.stringify([1, 2, 3], null, 2) })
32+
})
33+
34+
Deno.test('JsonFormatter - beautify basic', () => {
35+
assertEquals(JsonFormatter.beautify('{"a":1,"b":2}'), {
36+
data: JSON.stringify({ a: 1, b: 2 }, null, 2)
37+
})
38+
})
39+
40+
Deno.test('JsonFormatter - beautify nested', () => {
41+
assertEquals(
42+
JsonFormatter.beautify('{"a":{"b":1}}'),
43+
{ data: JSON.stringify({ a: { b: 1 } }, null, 2) }
44+
)
45+
})
46+
47+
Deno.test('JsonFormatter - constructor key does not crash', () => {
48+
const result = JsonFormatter.sortKeyAsc('{"constructor":{"a":1},"b":2}')
49+
assertEquals(typeof result.data, 'string')
50+
})
51+
52+
Deno.test('JsonFormatter - extra bracket returns error', () => {
53+
const result = JsonFormatter.minify('{"a":1}}')
54+
assertEquals(result.data, undefined)
55+
assertEquals(typeof result.error, 'string')
56+
})
57+
58+
Deno.test('JsonFormatter - handles 50 levels deep nesting', () => {
59+
let deep = '1'
60+
for (let i = 0; i < 50; i++) {
61+
deep = `{"k":${deep}}`
62+
}
63+
const result = JsonFormatter.minify(deep)
64+
assertEquals(result.error, undefined)
65+
assertEquals(typeof result.data, 'string')
66+
})
67+
68+
Deno.test('JsonFormatter - incomplete JSON returns error', () => {
69+
const result = JsonFormatter.minify('{"a":')
70+
assertEquals(result.data, undefined)
71+
assertEquals(typeof result.error, 'string')
72+
})
73+
74+
Deno.test('JsonFormatter - invalid JSON returns error', () => {
75+
const result = JsonFormatter.minify('{invalid}')
76+
assertEquals(result.data, undefined)
77+
assertEquals(typeof result.error, 'string')
78+
assertEquals(result.error!.startsWith('Invalid JSON input'), true)
79+
})
80+
81+
Deno.test('JsonFormatter - minify already minified is idempotent', () => {
82+
const minified = '{"a":1,"b":2}'
83+
assertEquals(JsonFormatter.minify(minified), { data: minified })
84+
})
85+
86+
Deno.test('JsonFormatter - minify array', () => {
87+
assertEquals(JsonFormatter.minify('[1, 2, 3]'), { data: '[1,2,3]' })
88+
})
89+
90+
Deno.test('JsonFormatter - minify basic', () => {
91+
const pretty = JSON.stringify({ a: 1, b: 2 }, null, 2)
92+
assertEquals(JsonFormatter.minify(pretty), { data: '{"a":1,"b":2}' })
93+
})
94+
95+
Deno.test('JsonFormatter - minify boolean', () => {
96+
assertEquals(JsonFormatter.minify('true'), { data: 'true' })
97+
})
98+
99+
Deno.test('JsonFormatter - minify deeply nested', () => {
100+
assertEquals(JsonFormatter.minify('{"a":{"b":{"c":{"d":1}}}}'), {
101+
data: '{"a":{"b":{"c":{"d":1}}}}'
102+
})
103+
})
104+
105+
Deno.test('JsonFormatter - minify duplicate keys last wins', () => {
106+
assertEquals(JsonFormatter.minify('{"a":1,"a":2}'), { data: '{"a":2}' })
107+
})
108+
109+
Deno.test('JsonFormatter - minify emoji value', () => {
110+
assertEquals(JsonFormatter.minify('{"emoji":"😀"}'), { data: '{"emoji":"😀"}' })
111+
})
112+
113+
Deno.test('JsonFormatter - minify empty array', () => {
114+
assertEquals(JsonFormatter.minify('[]'), { data: '[]' })
115+
})
116+
117+
Deno.test('JsonFormatter - minify empty object', () => {
118+
assertEquals(JsonFormatter.minify('{}'), { data: '{}' })
119+
})
120+
121+
Deno.test('JsonFormatter - minify empty string value', () => {
122+
assertEquals(JsonFormatter.minify('{"a":""}'), { data: '{"a":""}' })
123+
})
124+
125+
Deno.test('JsonFormatter - minify float', () => {
126+
assertEquals(JsonFormatter.minify('3.14'), { data: '3.14' })
127+
})
128+
129+
Deno.test('JsonFormatter - minify negative number', () => {
130+
assertEquals(JsonFormatter.minify('-1'), { data: '-1' })
131+
})
132+
133+
Deno.test('JsonFormatter - minify negative zero becomes zero', () => {
134+
assertEquals(JsonFormatter.minify('{"n":-0}'), { data: '{"n":0}' })
135+
})
136+
137+
Deno.test('JsonFormatter - minify nested arrays', () => {
138+
assertEquals(JsonFormatter.minify('[[1,[2,[3]]]]'), { data: '[[1,[2,[3]]]]' })
139+
})
140+
141+
Deno.test('JsonFormatter - minify null literal', () => {
142+
assertEquals(JsonFormatter.minify('null'), { data: 'null' })
143+
})
144+
145+
Deno.test('JsonFormatter - minify scientific notation', () => {
146+
assertEquals(JsonFormatter.minify('{"n":1e10}'), { data: '{"n":10000000000}' })
147+
})
148+
149+
Deno.test('JsonFormatter - minify string value', () => {
150+
assertEquals(JsonFormatter.minify('"hello"'), { data: '"hello"' })
151+
})
152+
153+
Deno.test('JsonFormatter - minify zero', () => {
154+
assertEquals(JsonFormatter.minify('0'), { data: '0' })
155+
})
156+
157+
Deno.test('JsonFormatter - rejects array input', () => {
158+
assertEquals(JsonFormatter.minify([] as unknown as string), {
159+
error: 'Input must be a non-empty string'
160+
})
161+
})
162+
163+
Deno.test('JsonFormatter - rejects boolean input', () => {
164+
assertEquals(JsonFormatter.minify(true as unknown as string), {
165+
error: 'Input must be a non-empty string'
166+
})
167+
})
168+
169+
Deno.test('JsonFormatter - rejects empty string', () => {
170+
assertEquals(JsonFormatter.minify(''), { error: 'Input must be a non-empty string' })
171+
})
172+
173+
Deno.test('JsonFormatter - rejects null input', () => {
174+
assertEquals(JsonFormatter.minify(null as unknown as string), {
175+
error: 'Input must be a non-empty string'
176+
})
177+
})
178+
179+
Deno.test('JsonFormatter - rejects number input', () => {
180+
assertEquals(JsonFormatter.minify(42 as unknown as string), {
181+
error: 'Input must be a non-empty string'
182+
})
183+
})
184+
185+
Deno.test('JsonFormatter - rejects object input', () => {
186+
assertEquals(JsonFormatter.minify({} as unknown as string), {
187+
error: 'Input must be a non-empty string'
188+
})
189+
})
190+
191+
Deno.test('JsonFormatter - rejects undefined input', () => {
192+
assertEquals(JsonFormatter.minify(undefined as unknown as string), {
193+
error: 'Input must be a non-empty string'
194+
})
195+
})
196+
197+
Deno.test('JsonFormatter - rejects whitespace only', () => {
198+
assertEquals(JsonFormatter.minify(' '), { error: 'Input must be a non-empty string' })
199+
})
200+
201+
Deno.test('JsonFormatter - result has data xor error', () => {
202+
const results = [
203+
JsonFormatter.minify('{"a":1}'),
204+
JsonFormatter.beautify('{"a":1}'),
205+
JsonFormatter.sortKeyAsc('{"a":1}'),
206+
JsonFormatter.sortKeyDesc('{"a":1}'),
207+
JsonFormatter.sortValueAsc('{"a":1}'),
208+
JsonFormatter.sortValueDesc('{"a":1}'),
209+
JsonFormatter.minify('invalid'),
210+
JsonFormatter.minify(''),
211+
JsonFormatter.minify(null as unknown as string)
212+
]
213+
for (const result of results) {
214+
const hasData = result.data !== undefined
215+
const hasError = result.error !== undefined
216+
assertEquals(hasData !== hasError, true)
217+
}
218+
})
219+
220+
Deno.test('JsonFormatter - roundtrip minify then beautify then minify', () => {
221+
const inputs = ['{"a":1,"b":[1,2,3]}', '[]', '{}', 'null', '42', '"hello"', 'true', 'false']
222+
for (const input of inputs) {
223+
const m1 = JsonFormatter.minify(input)
224+
const b = JsonFormatter.beautify(m1.data!)
225+
const m2 = JsonFormatter.minify(b.data!)
226+
assertEquals(m1, m2)
227+
}
228+
})
229+
230+
Deno.test('JsonFormatter - sortKeyAsc 3 levels deep', () => {
231+
const input = JSON.stringify({ z: { y: { x: { w: 1, v: 2 }, b: 3 }, a: 4 }, m: 5 })
232+
const expected = JSON.stringify({ m: 5, z: { a: 4, y: { b: 3, x: { v: 2, w: 1 } } } }, null, 2)
233+
assertEquals(JsonFormatter.sortKeyAsc(input), { data: expected })
234+
})
235+
236+
Deno.test('JsonFormatter - sortKeyAsc array in object preserved', () => {
237+
const result = JsonFormatter.sortKeyAsc('{"b":[3,1,2],"a":1}')
238+
const parsed = JSON.parse(result.data!)
239+
assertEquals(Object.keys(parsed), ['a', 'b'])
240+
assertEquals(parsed.b, [3, 1, 2])
241+
})
242+
243+
Deno.test('JsonFormatter - sortKeyAsc array passthrough', () => {
244+
assertEquals(JsonFormatter.sortKeyAsc('[3,1,2]'), { data: JSON.stringify([3, 1, 2], null, 2) })
245+
})
246+
247+
Deno.test('JsonFormatter - sortKeyAsc basic', () => {
248+
assertEquals(
249+
JsonFormatter.sortKeyAsc('{"c":3,"a":1,"b":2}'),
250+
{ data: JSON.stringify({ a: 1, b: 2, c: 3 }, null, 2) }
251+
)
252+
})
253+
254+
Deno.test('JsonFormatter - sortKeyAsc empty object', () => {
255+
assertEquals(JsonFormatter.sortKeyAsc('{}'), { data: '{}' })
256+
})
257+
258+
Deno.test('JsonFormatter - sortKeyAsc idempotent', () => {
259+
const input = '{"c":3,"a":{"z":1,"y":2}}'
260+
const once = JsonFormatter.sortKeyAsc(input)
261+
const twice = JsonFormatter.sortKeyAsc(once.data!)
262+
assertEquals(once, twice)
263+
})
264+
265+
Deno.test('JsonFormatter - sortKeyAsc nested recursive', () => {
266+
const input = JSON.stringify({ z: { b: 2, a: 1 }, a: { d: 4, c: 3 } })
267+
const expected = JSON.stringify({ a: { c: 3, d: 4 }, z: { a: 1, b: 2 } }, null, 2)
268+
assertEquals(JsonFormatter.sortKeyAsc(input), { data: expected })
269+
})
270+
271+
Deno.test('JsonFormatter - sortKeyAsc null value in object', () => {
272+
assertEquals(
273+
JsonFormatter.sortKeyAsc('{"b":null,"a":1}'),
274+
{ data: JSON.stringify({ a: 1, b: null }, null, 2) }
275+
)
276+
})
277+
278+
Deno.test('JsonFormatter - sortKeyAsc reversed equals sortKeyDesc', () => {
279+
const input = '{"d":4,"a":1,"c":3,"b":2}'
280+
const ascKeys = Object.keys(JSON.parse(JsonFormatter.sortKeyAsc(input).data!))
281+
const descKeys = Object.keys(JSON.parse(JsonFormatter.sortKeyDesc(input).data!))
282+
assertEquals(ascKeys.toReversed(), descKeys)
283+
})
284+
285+
Deno.test('JsonFormatter - sortKeyAsc single key', () => {
286+
assertEquals(JsonFormatter.sortKeyAsc('{"a":1}'), { data: JSON.stringify({ a: 1 }, null, 2) })
287+
})
288+
289+
Deno.test('JsonFormatter - sortKeyDesc basic', () => {
290+
assertEquals(
291+
JsonFormatter.sortKeyDesc('{"a":1,"c":3,"b":2}'),
292+
{ data: JSON.stringify({ c: 3, b: 2, a: 1 }, null, 2) }
293+
)
294+
})
295+
296+
Deno.test('JsonFormatter - sortKeyDesc key order', () => {
297+
const result = JsonFormatter.sortKeyDesc('{"d":4,"a":1,"c":3,"b":2}')
298+
const keys = Object.keys(JSON.parse(result.data!))
299+
assertEquals(keys, ['d', 'c', 'b', 'a'])
300+
})
301+
302+
Deno.test('JsonFormatter - sortValueAsc basic', () => {
303+
assertEquals(
304+
JsonFormatter.sortValueAsc('{"a":"cherry","b":"apple","c":"banana"}'),
305+
{ data: JSON.stringify({ b: 'apple', c: 'banana', a: 'cherry' }, null, 2) }
306+
)
307+
})
308+
309+
Deno.test('JsonFormatter - sortValueAsc key order by value', () => {
310+
const result = JsonFormatter.sortValueAsc('{"x":"charlie","y":"alpha","z":"bravo"}')
311+
const keys = Object.keys(JSON.parse(result.data!))
312+
assertEquals(keys, ['y', 'z', 'x'])
313+
})
314+
315+
Deno.test('JsonFormatter - sortValueDesc basic', () => {
316+
assertEquals(
317+
JsonFormatter.sortValueDesc('{"a":"cherry","b":"apple","c":"banana"}'),
318+
{ data: JSON.stringify({ a: 'cherry', c: 'banana', b: 'apple' }, null, 2) }
319+
)
320+
})
321+
322+
Deno.test('JsonFormatter - toString valueOf keys do not crash', () => {
323+
const result = JsonFormatter.sortKeyAsc('{"toString":1,"valueOf":2}')
324+
assertEquals(typeof result.data, 'string')
325+
})
326+
327+
Deno.test('JsonFormatter - trailing comma returns error', () => {
328+
const result = JsonFormatter.minify('{"a":1,}')
329+
assertEquals(result.data, undefined)
330+
assertEquals(typeof result.error, 'string')
331+
})
332+
333+
Deno.test('JsonFormatter - unquoted key returns error', () => {
334+
const result = JsonFormatter.minify('{a:1}')
335+
assertEquals(result.data, undefined)
336+
assertEquals(typeof result.error, 'string')
337+
})

0 commit comments

Comments
 (0)