Barazo AppView backend
barazo.forum
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})