Barazo AppView backend
barazo.forum
1import { describe, it, expect } from 'vitest'
2import {
3 createCategorySchema,
4 updateCategorySchema,
5 categoryQuerySchema,
6 maturityRatingSchema,
7 updateMaturitySchema,
8 categoryResponseSchema,
9 categoryTreeResponseSchema,
10} from '../../../src/validation/categories.js'
11
12// ---------------------------------------------------------------------------
13// maturityRatingSchema
14// ---------------------------------------------------------------------------
15
16describe('maturityRatingSchema', () => {
17 it("accepts 'safe'", () => {
18 expect(maturityRatingSchema.safeParse('safe').success).toBe(true)
19 })
20
21 it("accepts 'mature'", () => {
22 expect(maturityRatingSchema.safeParse('mature').success).toBe(true)
23 })
24
25 it("accepts 'adult'", () => {
26 expect(maturityRatingSchema.safeParse('adult').success).toBe(true)
27 })
28
29 it('rejects invalid values', () => {
30 expect(maturityRatingSchema.safeParse('nsfw').success).toBe(false)
31 expect(maturityRatingSchema.safeParse('').success).toBe(false)
32 expect(maturityRatingSchema.safeParse(123).success).toBe(false)
33 })
34})
35
36// ---------------------------------------------------------------------------
37// createCategorySchema
38// ---------------------------------------------------------------------------
39
40describe('createCategorySchema', () => {
41 const validInput = {
42 name: 'General Discussion',
43 slug: 'general-discussion',
44 }
45
46 it('accepts valid minimal input (name + slug)', () => {
47 const result = createCategorySchema.safeParse(validInput)
48 expect(result.success).toBe(true)
49 })
50
51 it('accepts valid input with all optional fields', () => {
52 const result = createCategorySchema.safeParse({
53 ...validInput,
54 description: 'A place for general discussion',
55 parentId: 'cat-parent-123',
56 sortOrder: 5,
57 maturityRating: 'mature',
58 })
59 expect(result.success).toBe(true)
60 })
61
62 // --- name ---
63
64 it('rejects empty name', () => {
65 const result = createCategorySchema.safeParse({ ...validInput, name: '' })
66 expect(result.success).toBe(false)
67 })
68
69 it('rejects name longer than 100 characters', () => {
70 const result = createCategorySchema.safeParse({
71 ...validInput,
72 name: 'a'.repeat(101),
73 })
74 expect(result.success).toBe(false)
75 })
76
77 it('accepts name exactly 100 characters', () => {
78 const result = createCategorySchema.safeParse({
79 ...validInput,
80 name: 'a'.repeat(100),
81 })
82 expect(result.success).toBe(true)
83 })
84
85 // --- slug ---
86
87 it('rejects empty slug', () => {
88 const result = createCategorySchema.safeParse({ ...validInput, slug: '' })
89 expect(result.success).toBe(false)
90 })
91
92 it('rejects slug longer than 50 characters', () => {
93 const result = createCategorySchema.safeParse({
94 ...validInput,
95 slug: 'a'.repeat(51),
96 })
97 expect(result.success).toBe(false)
98 })
99
100 it('accepts slug exactly 50 characters', () => {
101 const result = createCategorySchema.safeParse({
102 ...validInput,
103 slug: 'a'.repeat(50),
104 })
105 expect(result.success).toBe(true)
106 })
107
108 it('rejects slug with uppercase letters', () => {
109 const result = createCategorySchema.safeParse({
110 ...validInput,
111 slug: 'General',
112 })
113 expect(result.success).toBe(false)
114 })
115
116 it('rejects slug with spaces', () => {
117 const result = createCategorySchema.safeParse({
118 ...validInput,
119 slug: 'general discussion',
120 })
121 expect(result.success).toBe(false)
122 })
123
124 it('rejects slug starting with a hyphen', () => {
125 const result = createCategorySchema.safeParse({
126 ...validInput,
127 slug: '-general',
128 })
129 expect(result.success).toBe(false)
130 })
131
132 it('rejects slug ending with a hyphen', () => {
133 const result = createCategorySchema.safeParse({
134 ...validInput,
135 slug: 'general-',
136 })
137 expect(result.success).toBe(false)
138 })
139
140 it('rejects slug with consecutive hyphens', () => {
141 const result = createCategorySchema.safeParse({
142 ...validInput,
143 slug: 'general--discussion',
144 })
145 expect(result.success).toBe(false)
146 })
147
148 it('accepts valid hyphenated slug', () => {
149 const result = createCategorySchema.safeParse({
150 ...validInput,
151 slug: 'general-discussion',
152 })
153 expect(result.success).toBe(true)
154 })
155
156 it('accepts numeric slug', () => {
157 const result = createCategorySchema.safeParse({
158 ...validInput,
159 slug: '123',
160 })
161 expect(result.success).toBe(true)
162 })
163
164 // --- description ---
165
166 it('accepts missing description', () => {
167 const result = createCategorySchema.safeParse(validInput)
168 expect(result.success).toBe(true)
169 })
170
171 it('rejects description longer than 500 characters', () => {
172 const result = createCategorySchema.safeParse({
173 ...validInput,
174 description: 'a'.repeat(501),
175 })
176 expect(result.success).toBe(false)
177 })
178
179 it('accepts description exactly 500 characters', () => {
180 const result = createCategorySchema.safeParse({
181 ...validInput,
182 description: 'a'.repeat(500),
183 })
184 expect(result.success).toBe(true)
185 })
186
187 // --- sortOrder ---
188
189 it('rejects negative sortOrder', () => {
190 const result = createCategorySchema.safeParse({
191 ...validInput,
192 sortOrder: -1,
193 })
194 expect(result.success).toBe(false)
195 })
196
197 it('rejects non-integer sortOrder', () => {
198 const result = createCategorySchema.safeParse({
199 ...validInput,
200 sortOrder: 1.5,
201 })
202 expect(result.success).toBe(false)
203 })
204
205 it('accepts sortOrder of 0', () => {
206 const result = createCategorySchema.safeParse({
207 ...validInput,
208 sortOrder: 0,
209 })
210 expect(result.success).toBe(true)
211 })
212
213 // --- maturityRating ---
214
215 it('accepts valid maturityRating', () => {
216 for (const rating of ['safe', 'mature', 'adult']) {
217 const result = createCategorySchema.safeParse({
218 ...validInput,
219 maturityRating: rating,
220 })
221 expect(result.success).toBe(true)
222 }
223 })
224
225 it('rejects invalid maturityRating', () => {
226 const result = createCategorySchema.safeParse({
227 ...validInput,
228 maturityRating: 'nsfw',
229 })
230 expect(result.success).toBe(false)
231 })
232})
233
234// ---------------------------------------------------------------------------
235// updateCategorySchema
236// ---------------------------------------------------------------------------
237
238describe('updateCategorySchema', () => {
239 it('accepts empty object (all fields optional)', () => {
240 const result = updateCategorySchema.safeParse({})
241 expect(result.success).toBe(true)
242 })
243
244 it('accepts partial update with name only', () => {
245 const result = updateCategorySchema.safeParse({ name: 'New Name' })
246 expect(result.success).toBe(true)
247 })
248
249 it('accepts partial update with slug only', () => {
250 const result = updateCategorySchema.safeParse({ slug: 'new-slug' })
251 expect(result.success).toBe(true)
252 })
253
254 it('accepts partial update with all fields', () => {
255 const result = updateCategorySchema.safeParse({
256 name: 'Updated',
257 slug: 'updated',
258 description: 'Updated description',
259 parentId: 'cat-123',
260 sortOrder: 10,
261 maturityRating: 'adult',
262 })
263 expect(result.success).toBe(true)
264 })
265
266 it('rejects invalid slug in update', () => {
267 const result = updateCategorySchema.safeParse({ slug: 'INVALID' })
268 expect(result.success).toBe(false)
269 })
270
271 it('rejects empty name in update', () => {
272 const result = updateCategorySchema.safeParse({ name: '' })
273 expect(result.success).toBe(false)
274 })
275
276 it('accepts null parentId (move to root)', () => {
277 const result = updateCategorySchema.safeParse({ parentId: null })
278 expect(result.success).toBe(true)
279 if (result.success) {
280 expect(result.data.parentId).toBeNull()
281 }
282 })
283
284 it('accepts null description (clear description)', () => {
285 const result = updateCategorySchema.safeParse({ description: null })
286 expect(result.success).toBe(true)
287 if (result.success) {
288 expect(result.data.description).toBeNull()
289 }
290 })
291})
292
293// ---------------------------------------------------------------------------
294// categoryQuerySchema
295// ---------------------------------------------------------------------------
296
297describe('categoryQuerySchema', () => {
298 it('accepts empty object', () => {
299 const result = categoryQuerySchema.safeParse({})
300 expect(result.success).toBe(true)
301 })
302
303 it('accepts parentId filter', () => {
304 const result = categoryQuerySchema.safeParse({ parentId: 'cat-123' })
305 expect(result.success).toBe(true)
306 })
307})
308
309// ---------------------------------------------------------------------------
310// updateMaturitySchema
311// ---------------------------------------------------------------------------
312
313describe('updateMaturitySchema', () => {
314 it('accepts valid maturity rating', () => {
315 const result = updateMaturitySchema.safeParse({ maturityRating: 'safe' })
316 expect(result.success).toBe(true)
317 })
318
319 it('rejects invalid maturity rating', () => {
320 const result = updateMaturitySchema.safeParse({
321 maturityRating: 'extreme',
322 })
323 expect(result.success).toBe(false)
324 })
325
326 it('rejects missing maturityRating', () => {
327 const result = updateMaturitySchema.safeParse({})
328 expect(result.success).toBe(false)
329 })
330})
331
332// ---------------------------------------------------------------------------
333// categoryResponseSchema
334// ---------------------------------------------------------------------------
335
336describe('categoryResponseSchema', () => {
337 const validResponse = {
338 id: 'cat-123',
339 slug: 'general',
340 name: 'General',
341 description: null,
342 parentId: null,
343 sortOrder: 0,
344 communityDid: 'did:plc:community123',
345 maturityRating: 'safe',
346 createdAt: '2026-01-01T00:00:00.000Z',
347 updatedAt: '2026-01-01T00:00:00.000Z',
348 }
349
350 it('accepts valid category response', () => {
351 const result = categoryResponseSchema.safeParse(validResponse)
352 expect(result.success).toBe(true)
353 })
354
355 it('accepts response with non-null description and parentId', () => {
356 const result = categoryResponseSchema.safeParse({
357 ...validResponse,
358 description: 'A description',
359 parentId: 'cat-parent',
360 })
361 expect(result.success).toBe(true)
362 })
363})
364
365// ---------------------------------------------------------------------------
366// categoryTreeResponseSchema
367// ---------------------------------------------------------------------------
368
369describe('categoryTreeResponseSchema', () => {
370 it('accepts valid tree response with children', () => {
371 const result = categoryTreeResponseSchema.safeParse({
372 id: 'cat-1',
373 slug: 'parent',
374 name: 'Parent',
375 description: null,
376 parentId: null,
377 sortOrder: 0,
378 communityDid: 'did:plc:community123',
379 maturityRating: 'safe',
380 createdAt: '2026-01-01T00:00:00.000Z',
381 updatedAt: '2026-01-01T00:00:00.000Z',
382 children: [
383 {
384 id: 'cat-2',
385 slug: 'child',
386 name: 'Child',
387 description: null,
388 parentId: 'cat-1',
389 sortOrder: 0,
390 communityDid: 'did:plc:community123',
391 maturityRating: 'safe',
392 createdAt: '2026-01-01T00:00:00.000Z',
393 updatedAt: '2026-01-01T00:00:00.000Z',
394 children: [],
395 },
396 ],
397 })
398 expect(result.success).toBe(true)
399 })
400
401 it('accepts tree with empty children array', () => {
402 const result = categoryTreeResponseSchema.safeParse({
403 id: 'cat-1',
404 slug: 'leaf',
405 name: 'Leaf',
406 description: null,
407 parentId: null,
408 sortOrder: 0,
409 communityDid: 'did:plc:community123',
410 maturityRating: 'safe',
411 createdAt: '2026-01-01T00:00:00.000Z',
412 updatedAt: '2026-01-01T00:00:00.000Z',
413 children: [],
414 })
415 expect(result.success).toBe(true)
416 })
417})