Barazo AppView backend barazo.forum
at main 324 lines 10 kB view raw
1import { describe, it, expect } from 'vitest' 2import { 3 pageStatusSchema, 4 createPageSchema, 5 updatePageSchema, 6 pageResponseSchema, 7 pageTreeResponseSchema, 8} from '../../../src/validation/pages.js' 9 10// --------------------------------------------------------------------------- 11// pageStatusSchema 12// --------------------------------------------------------------------------- 13 14describe('pageStatusSchema', () => { 15 it('accepts "draft"', () => { 16 expect(pageStatusSchema.safeParse('draft').success).toBe(true) 17 }) 18 19 it('accepts "published"', () => { 20 expect(pageStatusSchema.safeParse('published').success).toBe(true) 21 }) 22 23 it('rejects invalid status', () => { 24 expect(pageStatusSchema.safeParse('archived').success).toBe(false) 25 expect(pageStatusSchema.safeParse('').success).toBe(false) 26 expect(pageStatusSchema.safeParse(42).success).toBe(false) 27 }) 28}) 29 30// --------------------------------------------------------------------------- 31// createPageSchema 32// --------------------------------------------------------------------------- 33 34describe('createPageSchema', () => { 35 const validInput = { 36 title: 'Terms of Service', 37 slug: 'terms-of-service', 38 content: '## Hello world', 39 status: 'published' as const, 40 } 41 42 it('accepts valid minimal input (title + slug)', () => { 43 const result = createPageSchema.safeParse({ title: 'About', slug: 'about' }) 44 expect(result.success).toBe(true) 45 if (result.success) { 46 expect(result.data.content).toBe('') 47 expect(result.data.status).toBe('draft') 48 } 49 }) 50 51 it('accepts valid full input', () => { 52 const result = createPageSchema.safeParse({ 53 ...validInput, 54 metaDescription: 'Our terms of service.', 55 parentId: 'page-abc', 56 sortOrder: 5, 57 }) 58 expect(result.success).toBe(true) 59 }) 60 61 // Title validation 62 it('rejects empty title', () => { 63 expect(createPageSchema.safeParse({ ...validInput, title: '' }).success).toBe(false) 64 }) 65 66 it('rejects title over 200 chars', () => { 67 expect(createPageSchema.safeParse({ ...validInput, title: 'x'.repeat(201) }).success).toBe( 68 false 69 ) 70 }) 71 72 it('accepts title at 200 chars', () => { 73 expect(createPageSchema.safeParse({ ...validInput, title: 'x'.repeat(200) }).success).toBe(true) 74 }) 75 76 // Slug validation 77 it('rejects empty slug', () => { 78 expect(createPageSchema.safeParse({ ...validInput, slug: '' }).success).toBe(false) 79 }) 80 81 it('rejects slug over 100 chars', () => { 82 expect(createPageSchema.safeParse({ ...validInput, slug: 'a'.repeat(101) }).success).toBe(false) 83 }) 84 85 it('rejects slug with uppercase', () => { 86 expect(createPageSchema.safeParse({ ...validInput, slug: 'About-Us' }).success).toBe(false) 87 }) 88 89 it('rejects slug with spaces', () => { 90 expect(createPageSchema.safeParse({ ...validInput, slug: 'about us' }).success).toBe(false) 91 }) 92 93 it('rejects slug with consecutive hyphens', () => { 94 expect(createPageSchema.safeParse({ ...validInput, slug: 'about--us' }).success).toBe(false) 95 }) 96 97 it('rejects slug starting with a hyphen', () => { 98 expect(createPageSchema.safeParse({ ...validInput, slug: '-about' }).success).toBe(false) 99 }) 100 101 it('rejects slug ending with a hyphen', () => { 102 expect(createPageSchema.safeParse({ ...validInput, slug: 'about-' }).success).toBe(false) 103 }) 104 105 it('accepts valid slug with hyphens', () => { 106 expect(createPageSchema.safeParse({ ...validInput, slug: 'terms-of-service' }).success).toBe( 107 true 108 ) 109 }) 110 111 // Reserved slugs 112 it('rejects reserved slug "new"', () => { 113 expect(createPageSchema.safeParse({ ...validInput, slug: 'new' }).success).toBe(false) 114 }) 115 116 it('rejects reserved slug "edit"', () => { 117 expect(createPageSchema.safeParse({ ...validInput, slug: 'edit' }).success).toBe(false) 118 }) 119 120 it('rejects reserved slug "drafts"', () => { 121 expect(createPageSchema.safeParse({ ...validInput, slug: 'drafts' }).success).toBe(false) 122 }) 123 124 // Content validation 125 it('defaults content to empty string', () => { 126 const result = createPageSchema.safeParse({ title: 'Hi', slug: 'hi' }) 127 expect(result.success).toBe(true) 128 if (result.success) { 129 expect(result.data.content).toBe('') 130 } 131 }) 132 133 it('rejects content over 100_000 chars', () => { 134 expect( 135 createPageSchema.safeParse({ ...validInput, content: 'x'.repeat(100_001) }).success 136 ).toBe(false) 137 }) 138 139 it('accepts content at 100_000 chars', () => { 140 expect( 141 createPageSchema.safeParse({ ...validInput, content: 'x'.repeat(100_000) }).success 142 ).toBe(true) 143 }) 144 145 // Status validation 146 it('defaults status to draft', () => { 147 const result = createPageSchema.safeParse({ title: 'Hi', slug: 'hi' }) 148 expect(result.success).toBe(true) 149 if (result.success) { 150 expect(result.data.status).toBe('draft') 151 } 152 }) 153 154 it('rejects invalid status', () => { 155 expect(createPageSchema.safeParse({ ...validInput, status: 'archived' }).success).toBe(false) 156 }) 157 158 // metaDescription validation 159 it('accepts null metaDescription', () => { 160 expect(createPageSchema.safeParse({ ...validInput, metaDescription: null }).success).toBe(true) 161 }) 162 163 it('rejects metaDescription over 320 chars', () => { 164 expect( 165 createPageSchema.safeParse({ ...validInput, metaDescription: 'x'.repeat(321) }).success 166 ).toBe(false) 167 }) 168 169 it('accepts metaDescription at 320 chars', () => { 170 expect( 171 createPageSchema.safeParse({ ...validInput, metaDescription: 'x'.repeat(320) }).success 172 ).toBe(true) 173 }) 174 175 // parentId 176 it('accepts parentId as a string', () => { 177 expect(createPageSchema.safeParse({ ...validInput, parentId: 'page-123' }).success).toBe(true) 178 }) 179 180 it('accepts null parentId', () => { 181 expect(createPageSchema.safeParse({ ...validInput, parentId: null }).success).toBe(true) 182 }) 183 184 // sortOrder 185 it('rejects negative sortOrder', () => { 186 expect(createPageSchema.safeParse({ ...validInput, sortOrder: -1 }).success).toBe(false) 187 }) 188 189 it('rejects non-integer sortOrder', () => { 190 expect(createPageSchema.safeParse({ ...validInput, sortOrder: 1.5 }).success).toBe(false) 191 }) 192 193 it('accepts sortOrder of 0', () => { 194 expect(createPageSchema.safeParse({ ...validInput, sortOrder: 0 }).success).toBe(true) 195 }) 196}) 197 198// --------------------------------------------------------------------------- 199// updatePageSchema 200// --------------------------------------------------------------------------- 201 202describe('updatePageSchema', () => { 203 it('accepts empty object (all fields optional)', () => { 204 expect(updatePageSchema.safeParse({}).success).toBe(true) 205 }) 206 207 it('accepts partial updates', () => { 208 expect(updatePageSchema.safeParse({ title: 'New Title' }).success).toBe(true) 209 expect(updatePageSchema.safeParse({ status: 'published' }).success).toBe(true) 210 expect(updatePageSchema.safeParse({ content: 'New content' }).success).toBe(true) 211 }) 212 213 it('rejects empty title', () => { 214 expect(updatePageSchema.safeParse({ title: '' }).success).toBe(false) 215 }) 216 217 it('rejects empty slug', () => { 218 expect(updatePageSchema.safeParse({ slug: '' }).success).toBe(false) 219 }) 220 221 it('accepts nullable parentId', () => { 222 expect(updatePageSchema.safeParse({ parentId: null }).success).toBe(true) 223 }) 224 225 it('accepts nullable metaDescription', () => { 226 expect(updatePageSchema.safeParse({ metaDescription: null }).success).toBe(true) 227 }) 228 229 it('rejects reserved slug "new"', () => { 230 expect(updatePageSchema.safeParse({ slug: 'new' }).success).toBe(false) 231 }) 232 233 it('rejects reserved slug "edit"', () => { 234 expect(updatePageSchema.safeParse({ slug: 'edit' }).success).toBe(false) 235 }) 236 237 it('rejects reserved slug "drafts"', () => { 238 expect(updatePageSchema.safeParse({ slug: 'drafts' }).success).toBe(false) 239 }) 240}) 241 242// --------------------------------------------------------------------------- 243// pageResponseSchema 244// --------------------------------------------------------------------------- 245 246describe('pageResponseSchema', () => { 247 it('accepts a valid page response', () => { 248 const result = pageResponseSchema.safeParse({ 249 id: 'page-001', 250 slug: 'about', 251 title: 'About Us', 252 content: '## About', 253 status: 'published', 254 metaDescription: null, 255 parentId: null, 256 sortOrder: 0, 257 communityDid: 'did:plc:community123', 258 createdAt: '2026-01-01T00:00:00.000Z', 259 updatedAt: '2026-01-01T00:00:00.000Z', 260 }) 261 expect(result.success).toBe(true) 262 }) 263 264 it('rejects missing required fields', () => { 265 expect(pageResponseSchema.safeParse({ id: 'page-001' }).success).toBe(false) 266 }) 267}) 268 269// --------------------------------------------------------------------------- 270// pageTreeResponseSchema 271// --------------------------------------------------------------------------- 272 273describe('pageTreeResponseSchema', () => { 274 it('accepts a page with empty children', () => { 275 const result = pageTreeResponseSchema.safeParse({ 276 id: 'page-001', 277 slug: 'about', 278 title: 'About Us', 279 content: '## About', 280 status: 'published', 281 metaDescription: null, 282 parentId: null, 283 sortOrder: 0, 284 communityDid: 'did:plc:community123', 285 createdAt: '2026-01-01T00:00:00.000Z', 286 updatedAt: '2026-01-01T00:00:00.000Z', 287 children: [], 288 }) 289 expect(result.success).toBe(true) 290 }) 291 292 it('accepts nested children', () => { 293 const result = pageTreeResponseSchema.safeParse({ 294 id: 'page-001', 295 slug: 'legal', 296 title: 'Legal', 297 content: '', 298 status: 'published', 299 metaDescription: null, 300 parentId: null, 301 sortOrder: 0, 302 communityDid: 'did:plc:community123', 303 createdAt: '2026-01-01T00:00:00.000Z', 304 updatedAt: '2026-01-01T00:00:00.000Z', 305 children: [ 306 { 307 id: 'page-002', 308 slug: 'terms', 309 title: 'Terms', 310 content: '## Terms', 311 status: 'published', 312 metaDescription: null, 313 parentId: 'page-001', 314 sortOrder: 0, 315 communityDid: 'did:plc:community123', 316 createdAt: '2026-01-01T00:00:00.000Z', 317 updatedAt: '2026-01-01T00:00:00.000Z', 318 children: [], 319 }, 320 ], 321 }) 322 expect(result.success).toBe(true) 323 }) 324})