Barazo AppView backend barazo.forum
at main 417 lines 12 kB view raw
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})