Barazo AppView backend
barazo.forum
1import { describe, it, expect } from 'vitest'
2import { searchQuerySchema } from '../../../src/validation/search.js'
3
4// ---------------------------------------------------------------------------
5// Tests
6// ---------------------------------------------------------------------------
7
8describe('search validation schemas', () => {
9 // =========================================================================
10 // searchQuerySchema
11 // =========================================================================
12
13 describe('searchQuerySchema', () => {
14 it('parses a valid query with all fields', () => {
15 const result = searchQuerySchema.safeParse({
16 q: 'test query',
17 category: 'general',
18 author: 'did:plc:user123',
19 dateFrom: '2026-01-01T00:00:00Z',
20 dateTo: '2026-02-01T00:00:00Z',
21 type: 'topics',
22 limit: '10',
23 cursor: 'abc123base64',
24 })
25
26 expect(result.success).toBe(true)
27 if (result.success) {
28 expect(result.data.q).toBe('test query')
29 expect(result.data.category).toBe('general')
30 expect(result.data.author).toBe('did:plc:user123')
31 expect(result.data.dateFrom).toBe('2026-01-01T00:00:00Z')
32 expect(result.data.dateTo).toBe('2026-02-01T00:00:00Z')
33 expect(result.data.type).toBe('topics')
34 expect(result.data.limit).toBe(10)
35 expect(result.data.cursor).toBe('abc123base64')
36 }
37 })
38
39 it('parses a minimal valid query (only q)', () => {
40 const result = searchQuerySchema.safeParse({ q: 'hello' })
41
42 expect(result.success).toBe(true)
43 if (result.success) {
44 expect(result.data.q).toBe('hello')
45 }
46 })
47
48 it('fails when q is missing', () => {
49 const result = searchQuerySchema.safeParse({})
50 expect(result.success).toBe(false)
51 })
52
53 it('fails when q is empty string', () => {
54 const result = searchQuerySchema.safeParse({ q: '' })
55 expect(result.success).toBe(false)
56 })
57
58 it('fails when q exceeds 500 characters', () => {
59 const result = searchQuerySchema.safeParse({ q: 'a'.repeat(501) })
60 expect(result.success).toBe(false)
61 })
62
63 it('accepts q at exactly 500 characters', () => {
64 const result = searchQuerySchema.safeParse({ q: 'a'.repeat(500) })
65 expect(result.success).toBe(true)
66 })
67
68 it("defaults type to 'all' when not provided", () => {
69 const result = searchQuerySchema.safeParse({ q: 'test' })
70
71 expect(result.success).toBe(true)
72 if (result.success) {
73 expect(result.data.type).toBe('all')
74 }
75 })
76
77 it('defaults limit to 25 when not provided', () => {
78 const result = searchQuerySchema.safeParse({ q: 'test' })
79
80 expect(result.success).toBe(true)
81 if (result.success) {
82 expect(result.data.limit).toBe(25)
83 }
84 })
85
86 it("accepts type 'topics'", () => {
87 const result = searchQuerySchema.safeParse({
88 q: 'test',
89 type: 'topics',
90 })
91 expect(result.success).toBe(true)
92 if (result.success) {
93 expect(result.data.type).toBe('topics')
94 }
95 })
96
97 it("accepts type 'replies'", () => {
98 const result = searchQuerySchema.safeParse({
99 q: 'test',
100 type: 'replies',
101 })
102 expect(result.success).toBe(true)
103 if (result.success) {
104 expect(result.data.type).toBe('replies')
105 }
106 })
107
108 it("accepts type 'all'", () => {
109 const result = searchQuerySchema.safeParse({ q: 'test', type: 'all' })
110 expect(result.success).toBe(true)
111 if (result.success) {
112 expect(result.data.type).toBe('all')
113 }
114 })
115
116 it('fails for invalid type value', () => {
117 const result = searchQuerySchema.safeParse({
118 q: 'test',
119 type: 'invalid',
120 })
121 expect(result.success).toBe(false)
122 })
123
124 it('fails when limit is below minimum (0)', () => {
125 const result = searchQuerySchema.safeParse({ q: 'test', limit: '0' })
126 expect(result.success).toBe(false)
127 })
128
129 it('fails when limit exceeds maximum (101)', () => {
130 const result = searchQuerySchema.safeParse({ q: 'test', limit: '101' })
131 expect(result.success).toBe(false)
132 })
133
134 it('accepts limit at boundary 1', () => {
135 const result = searchQuerySchema.safeParse({ q: 'test', limit: '1' })
136 expect(result.success).toBe(true)
137 if (result.success) {
138 expect(result.data.limit).toBe(1)
139 }
140 })
141
142 it('accepts limit at boundary 100', () => {
143 const result = searchQuerySchema.safeParse({ q: 'test', limit: '100' })
144 expect(result.success).toBe(true)
145 if (result.success) {
146 expect(result.data.limit).toBe(100)
147 }
148 })
149
150 it('transforms string limit to number', () => {
151 const result = searchQuerySchema.safeParse({ q: 'test', limit: '42' })
152 expect(result.success).toBe(true)
153 if (result.success) {
154 expect(result.data.limit).toBe(42)
155 expect(typeof result.data.limit).toBe('number')
156 }
157 })
158
159 it('fails for non-numeric limit', () => {
160 const result = searchQuerySchema.safeParse({ q: 'test', limit: 'abc' })
161 expect(result.success).toBe(false)
162 })
163
164 it('parses optional category filter', () => {
165 const result = searchQuerySchema.safeParse({
166 q: 'test',
167 category: 'support',
168 })
169 expect(result.success).toBe(true)
170 if (result.success) {
171 expect(result.data.category).toBe('support')
172 }
173 })
174
175 it('parses optional author filter', () => {
176 const result = searchQuerySchema.safeParse({
177 q: 'test',
178 author: 'did:plc:abc123',
179 })
180 expect(result.success).toBe(true)
181 if (result.success) {
182 expect(result.data.author).toBe('did:plc:abc123')
183 }
184 })
185
186 it('validates dateFrom as ISO datetime', () => {
187 const result = searchQuerySchema.safeParse({
188 q: 'test',
189 dateFrom: '2026-01-15T10:30:00Z',
190 })
191 expect(result.success).toBe(true)
192 if (result.success) {
193 expect(result.data.dateFrom).toBe('2026-01-15T10:30:00Z')
194 }
195 })
196
197 it('validates dateTo as ISO datetime', () => {
198 const result = searchQuerySchema.safeParse({
199 q: 'test',
200 dateTo: '2026-02-15T23:59:59Z',
201 })
202 expect(result.success).toBe(true)
203 if (result.success) {
204 expect(result.data.dateTo).toBe('2026-02-15T23:59:59Z')
205 }
206 })
207
208 it('fails for invalid dateFrom format', () => {
209 const result = searchQuerySchema.safeParse({
210 q: 'test',
211 dateFrom: 'not-a-date',
212 })
213 expect(result.success).toBe(false)
214 })
215
216 it('fails for invalid dateTo format', () => {
217 const result = searchQuerySchema.safeParse({
218 q: 'test',
219 dateTo: '2026/01/15',
220 })
221 expect(result.success).toBe(false)
222 })
223
224 it('parses optional cursor', () => {
225 const cursor = Buffer.from(
226 JSON.stringify({ rank: 0.5, uri: 'at://did:plc:test/forum.barazo.topic.post/abc' })
227 ).toString('base64')
228 const result = searchQuerySchema.safeParse({ q: 'test', cursor })
229 expect(result.success).toBe(true)
230 if (result.success) {
231 expect(result.data.cursor).toBe(cursor)
232 }
233 })
234 })
235})