forked from
slices.network/quickslice
Auto-indexing service and GraphQL API for AT Protocol Records
1/// Integration tests for Blob type in GraphQL queries
2///
3/// Tests the full flow of querying blob fields:
4/// 1. Create a lexicon with blob fields
5/// 2. Insert records with blob data in AT Protocol format
6/// 3. Execute GraphQL queries with blob field selection
7/// 4. Verify blob fields are resolved correctly with all sub-fields
8import database/repositories/lexicons
9import database/repositories/records
10import gleam/http
11import gleam/json
12import gleam/option
13import gleam/string
14import gleeunit/should
15import handlers/graphql as graphql_handler
16import lib/oauth/did_cache
17import test_helpers
18import wisp
19import wisp/simulate
20
21/// Create a lexicon with a blob field (profile with avatar)
22fn create_profile_lexicon() -> String {
23 json.object([
24 #("lexicon", json.int(1)),
25 #("id", json.string("app.test.profile")),
26 #(
27 "defs",
28 json.object([
29 #(
30 "main",
31 json.object([
32 #("type", json.string("record")),
33 #("key", json.string("self")),
34 #(
35 "record",
36 json.object([
37 #("type", json.string("object")),
38 #(
39 "required",
40 json.array([json.string("displayName")], of: fn(x) { x }),
41 ),
42 #(
43 "properties",
44 json.object([
45 #(
46 "displayName",
47 json.object([#("type", json.string("string"))]),
48 ),
49 #(
50 "description",
51 json.object([#("type", json.string("string"))]),
52 ),
53 #("avatar", json.object([#("type", json.string("blob"))])),
54 #("banner", json.object([#("type", json.string("blob"))])),
55 ]),
56 ),
57 ]),
58 ),
59 ]),
60 ),
61 ]),
62 ),
63 ])
64 |> json.to_string
65}
66
67pub fn blob_field_query_test() {
68 // Create in-memory database
69 let assert Ok(exec) = test_helpers.create_test_db()
70 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
71 let assert Ok(_) = test_helpers.create_record_table(exec)
72
73 // Insert profile lexicon with blob fields
74 let lexicon = create_profile_lexicon()
75 let assert Ok(_) = lexicons.insert(exec, "app.test.profile", lexicon)
76
77 // Insert a profile record with avatar blob
78 // AT Protocol blob format: { ref: { $link: "cid" }, mimeType: "...", size: 123 }
79 let record_json =
80 json.object([
81 #("displayName", json.string("Alice")),
82 #("description", json.string("Software developer")),
83 #(
84 "avatar",
85 json.object([
86 #("ref", json.object([#("$link", json.string("bafyreiabc123"))])),
87 #("mimeType", json.string("image/jpeg")),
88 #("size", json.int(45_678)),
89 ]),
90 ),
91 ])
92 |> json.to_string
93
94 let assert Ok(_) =
95 records.insert(
96 exec,
97 "at://did:plc:alice123/app.test.profile/self",
98 "cidprofile1",
99 "did:plc:alice123",
100 "app.test.profile",
101 record_json,
102 )
103
104 // Query blob fields with all sub-fields
105 let query =
106 json.object([
107 #(
108 "query",
109 json.string(
110 "{ appTestProfile { edges { node { displayName avatar { ref mimeType size url(preset: \"avatar\") } } } } }",
111 ),
112 ),
113 ])
114 |> json.to_string
115
116 let request =
117 simulate.request(http.Post, "/graphql")
118 |> simulate.string_body(query)
119 |> simulate.header("content-type", "application/json")
120
121 let assert Ok(cache) = did_cache.start()
122 let response =
123 graphql_handler.handle_graphql_request(
124 request,
125 exec,
126 cache,
127 option.None,
128 "",
129 "https://plc.directory",
130 )
131
132 // Verify response
133 response.status
134 |> should.equal(200)
135
136 let assert wisp.Text(body) = response.body
137
138 // Verify response contains data
139 string.contains(body, "\"data\"")
140 |> should.be_true
141
142 // Verify displayName is present
143 string.contains(body, "Alice")
144 |> should.be_true
145
146 // Verify blob ref field
147 string.contains(body, "bafyreiabc123")
148 |> should.be_true
149
150 // Verify blob mimeType field
151 string.contains(body, "image/jpeg")
152 |> should.be_true
153
154 // Verify blob size field
155 string.contains(body, "45678")
156 |> should.be_true
157
158 // Verify blob url field contains CDN URL with avatar preset
159 string.contains(
160 body,
161 "https://cdn.bsky.app/img/avatar/plain/did:plc:alice123/bafyreiabc123@jpeg",
162 )
163 |> should.be_true
164}
165
166pub fn blob_field_with_different_presets_test() {
167 // Create in-memory database
168 let assert Ok(exec) = test_helpers.create_test_db()
169 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
170 let assert Ok(_) = test_helpers.create_record_table(exec)
171
172 // Insert profile lexicon
173 let lexicon = create_profile_lexicon()
174 let assert Ok(_) = lexicons.insert(exec, "app.test.profile", lexicon)
175
176 // Insert a profile with banner blob
177 let record_json =
178 json.object([
179 #("displayName", json.string("Bob")),
180 #(
181 "banner",
182 json.object([
183 #("ref", json.object([#("$link", json.string("bafyreibanner789"))])),
184 #("mimeType", json.string("image/png")),
185 #("size", json.int(98_765)),
186 ]),
187 ),
188 ])
189 |> json.to_string
190
191 let assert Ok(_) =
192 records.insert(
193 exec,
194 "at://did:plc:bob456/app.test.profile/self",
195 "cidbanner1",
196 "did:plc:bob456",
197 "app.test.profile",
198 record_json,
199 )
200
201 // Query with banner preset
202 let query =
203 json.object([
204 #(
205 "query",
206 json.string(
207 "{ appTestProfile { edges { node { banner { url(preset: \"banner\") } } } } }",
208 ),
209 ),
210 ])
211 |> json.to_string
212
213 let request =
214 simulate.request(http.Post, "/graphql")
215 |> simulate.string_body(query)
216 |> simulate.header("content-type", "application/json")
217
218 let assert Ok(cache) = did_cache.start()
219 let response =
220 graphql_handler.handle_graphql_request(
221 request,
222 exec,
223 cache,
224 option.None,
225 "",
226 "https://plc.directory",
227 )
228
229 response.status
230 |> should.equal(200)
231
232 let assert wisp.Text(body) = response.body
233
234 // Verify banner URL with banner preset
235 string.contains(
236 body,
237 "https://cdn.bsky.app/img/banner/plain/did:plc:bob456/bafyreibanner789@jpeg",
238 )
239 |> should.be_true
240}
241
242pub fn blob_field_default_preset_test() {
243 // Test that when no preset is specified, feed_fullsize is used
244 let assert Ok(exec) = test_helpers.create_test_db()
245 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
246 let assert Ok(_) = test_helpers.create_record_table(exec)
247
248 let lexicon = create_profile_lexicon()
249 let assert Ok(_) = lexicons.insert(exec, "app.test.profile", lexicon)
250
251 let record_json =
252 json.object([
253 #("displayName", json.string("Charlie")),
254 #(
255 "avatar",
256 json.object([
257 #("ref", json.object([#("$link", json.string("bafyreidefault"))])),
258 #("mimeType", json.string("image/jpeg")),
259 #("size", json.int(12_345)),
260 ]),
261 ),
262 ])
263 |> json.to_string
264
265 let assert Ok(_) =
266 records.insert(
267 exec,
268 "at://did:plc:charlie/app.test.profile/self",
269 "cidcharlie",
270 "did:plc:charlie",
271 "app.test.profile",
272 record_json,
273 )
274
275 // Query without specifying preset (should default to feed_fullsize)
276 let query =
277 json.object([
278 #(
279 "query",
280 json.string("{ appTestProfile { edges { node { avatar { url } } } } }"),
281 ),
282 ])
283 |> json.to_string
284
285 let request =
286 simulate.request(http.Post, "/graphql")
287 |> simulate.string_body(query)
288 |> simulate.header("content-type", "application/json")
289
290 let assert Ok(cache) = did_cache.start()
291 let response =
292 graphql_handler.handle_graphql_request(
293 request,
294 exec,
295 cache,
296 option.None,
297 "",
298 "https://plc.directory",
299 )
300
301 response.status
302 |> should.equal(200)
303
304 let assert wisp.Text(body) = response.body
305
306 // Verify default preset is feed_fullsize
307 string.contains(
308 body,
309 "https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:charlie/bafyreidefault@jpeg",
310 )
311 |> should.be_true
312}
313
314pub fn blob_field_null_when_missing_test() {
315 // Test that blob fields return null when not present in record
316 let assert Ok(exec) = test_helpers.create_test_db()
317 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
318 let assert Ok(_) = test_helpers.create_record_table(exec)
319
320 let lexicon = create_profile_lexicon()
321 let assert Ok(_) = lexicons.insert(exec, "app.test.profile", lexicon)
322
323 // Insert record without avatar field
324 let record_json =
325 json.object([#("displayName", json.string("Dave"))])
326 |> json.to_string
327
328 let assert Ok(_) =
329 records.insert(
330 exec,
331 "at://did:plc:dave/app.test.profile/self",
332 "ciddave",
333 "did:plc:dave",
334 "app.test.profile",
335 record_json,
336 )
337
338 let query =
339 json.object([
340 #(
341 "query",
342 json.string(
343 "{ appTestProfile { edges { node { displayName avatar { ref } } } } }",
344 ),
345 ),
346 ])
347 |> json.to_string
348
349 let request =
350 simulate.request(http.Post, "/graphql")
351 |> simulate.string_body(query)
352 |> simulate.header("content-type", "application/json")
353
354 let assert Ok(cache) = did_cache.start()
355 let response =
356 graphql_handler.handle_graphql_request(
357 request,
358 exec,
359 cache,
360 option.None,
361 "",
362 "https://plc.directory",
363 )
364
365 response.status
366 |> should.equal(200)
367
368 let assert wisp.Text(body) = response.body
369
370 // Verify displayName is present
371 string.contains(body, "Dave")
372 |> should.be_true
373
374 // Verify avatar is null (JSON encoder adds space after colon)
375 string.contains(body, "\"avatar\": null")
376 |> should.be_true
377}