Auto-indexing service and GraphQL API for AT Protocol Records
quickslice.slices.network/
atproto
gleam
graphql
1/// Integration tests for viewer state fields
2///
3/// Verifies that viewer fields show the authenticated viewer's relationship
4/// to records (e.g., viewer's like on a gallery)
5import database/repositories/lexicons
6import database/repositories/records
7import gleam/http
8import gleam/json
9import gleam/option.{None}
10import gleam/string
11import gleeunit/should
12import handlers/graphql as graphql_handler
13import lib/oauth/did_cache
14import test_helpers
15import wisp
16import wisp/simulate
17
18// Gallery lexicon with subject field for favorites
19fn create_gallery_lexicon() -> String {
20 json.object([
21 #("lexicon", json.int(1)),
22 #("id", json.string("social.grain.gallery")),
23 #(
24 "defs",
25 json.object([
26 #(
27 "main",
28 json.object([
29 #("type", json.string("record")),
30 #("key", json.string("tid")),
31 #(
32 "record",
33 json.object([
34 #("type", json.string("object")),
35 #(
36 "required",
37 json.array([json.string("title")], of: fn(x) { x }),
38 ),
39 #(
40 "properties",
41 json.object([
42 #("title", json.object([#("type", json.string("string"))])),
43 ]),
44 ),
45 ]),
46 ),
47 ]),
48 ),
49 ]),
50 ),
51 ])
52 |> json.to_string
53}
54
55// Favorite lexicon with AT-URI subject field
56fn create_favorite_lexicon() -> String {
57 json.object([
58 #("lexicon", json.int(1)),
59 #("id", json.string("social.grain.favorite")),
60 #(
61 "defs",
62 json.object([
63 #(
64 "main",
65 json.object([
66 #("type", json.string("record")),
67 #("key", json.string("tid")),
68 #(
69 "record",
70 json.object([
71 #("type", json.string("object")),
72 #(
73 "required",
74 json.array([json.string("subject")], of: fn(x) { x }),
75 ),
76 #(
77 "properties",
78 json.object([
79 #(
80 "subject",
81 json.object([
82 #("type", json.string("string")),
83 #("format", json.string("at-uri")),
84 ]),
85 ),
86 ]),
87 ),
88 ]),
89 ),
90 ]),
91 ),
92 ]),
93 ),
94 ])
95 |> json.to_string
96}
97
98// Follow lexicon with DID subject field
99fn create_follow_lexicon() -> String {
100 json.object([
101 #("lexicon", json.int(1)),
102 #("id", json.string("social.grain.graph.follow")),
103 #(
104 "defs",
105 json.object([
106 #(
107 "main",
108 json.object([
109 #("type", json.string("record")),
110 #("key", json.string("tid")),
111 #(
112 "record",
113 json.object([
114 #("type", json.string("object")),
115 #(
116 "required",
117 json.array([json.string("subject")], of: fn(x) { x }),
118 ),
119 #(
120 "properties",
121 json.object([
122 #(
123 "subject",
124 json.object([
125 #("type", json.string("string")),
126 #("format", json.string("did")),
127 ]),
128 ),
129 ]),
130 ),
131 ]),
132 ),
133 ]),
134 ),
135 ]),
136 ),
137 ])
138 |> json.to_string
139}
140
141/// Test: Viewer favorite field returns null when not favorited
142pub fn viewer_favorite_null_when_not_favorited_test() {
143 // Setup database
144 let assert Ok(exec) = test_helpers.create_test_db()
145 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
146 let assert Ok(_) = test_helpers.create_record_table(exec)
147 let assert Ok(_) = test_helpers.create_config_table(exec)
148 let assert Ok(_) = test_helpers.create_actor_table(exec)
149 let assert Ok(_) = test_helpers.create_oauth_tables(exec)
150 let assert Ok(_) =
151 test_helpers.insert_test_token(exec, "test-viewer-token", "did:plc:viewer")
152
153 // Insert lexicons
154 let assert Ok(_) =
155 lexicons.insert(exec, "social.grain.gallery", create_gallery_lexicon())
156 let assert Ok(_) =
157 lexicons.insert(exec, "social.grain.favorite", create_favorite_lexicon())
158
159 // Create a gallery record
160 let gallery_uri = "at://did:plc:author/social.grain.gallery/gallery1"
161 let gallery_json =
162 json.object([#("title", json.string("Test Gallery"))])
163 |> json.to_string
164
165 let assert Ok(_) =
166 records.insert(
167 exec,
168 gallery_uri,
169 "cid1",
170 "did:plc:author",
171 "social.grain.gallery",
172 gallery_json,
173 )
174
175 // Query with auth token - should show null for viewerSocialGrainFavoriteViaSubject
176 let query =
177 json.object([
178 #(
179 "query",
180 json.string(
181 "{ socialGrainGallery { edges { node { uri viewerSocialGrainFavoriteViaSubject { uri } } } } }",
182 ),
183 ),
184 ])
185 |> json.to_string
186
187 let request =
188 simulate.request(http.Post, "/graphql")
189 |> simulate.string_body(query)
190 |> simulate.header("content-type", "application/json")
191 |> simulate.header("authorization", "Bearer test-viewer-token")
192
193 let assert Ok(cache) = did_cache.start()
194 let response =
195 graphql_handler.handle_graphql_request(request, exec, cache, None, "", "")
196
197 let assert wisp.Text(body) = response.body
198
199 // Debug: print the response if not 200
200 case response.status {
201 200 -> Nil
202 _ -> {
203 // Print error for debugging
204 should.fail()
205 }
206 }
207
208 // Should contain the gallery URI
209 string.contains(body, gallery_uri) |> should.be_true
210
211 // The viewer field should be null since there's no favorite
212 // JSON formatting may have spaces, so check for the field and null value
213 string.contains(body, "viewerSocialGrainFavoriteViaSubject")
214 |> should.be_true
215 string.contains(body, "null")
216 |> should.be_true
217}
218
219/// Test: Schema includes viewer favorite field and query succeeds
220pub fn viewer_favorite_schema_test() {
221 // Setup database
222 let assert Ok(exec) = test_helpers.create_test_db()
223 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
224 let assert Ok(_) = test_helpers.create_record_table(exec)
225 let assert Ok(_) = test_helpers.create_config_table(exec)
226 let assert Ok(_) = test_helpers.create_actor_table(exec)
227 let assert Ok(_) = test_helpers.create_oauth_tables(exec)
228 let assert Ok(_) =
229 test_helpers.insert_test_token(exec, "test-viewer-token", "did:plc:viewer")
230
231 // Insert lexicons
232 let assert Ok(_) =
233 lexicons.insert(exec, "social.grain.gallery", create_gallery_lexicon())
234 let assert Ok(_) =
235 lexicons.insert(exec, "social.grain.favorite", create_favorite_lexicon())
236
237 // Create a gallery record
238 let gallery_uri = "at://did:plc:author/social.grain.gallery/gallery1"
239 let gallery_json =
240 json.object([#("title", json.string("Test Gallery"))])
241 |> json.to_string
242
243 let assert Ok(_) =
244 records.insert(
245 exec,
246 gallery_uri,
247 "cid1",
248 "did:plc:author",
249 "social.grain.gallery",
250 gallery_json,
251 )
252
253 // Query WITH viewer field (verifies schema includes it)
254 let query =
255 json.object([
256 #(
257 "query",
258 json.string(
259 "{ socialGrainGallery { edges { node { uri viewerSocialGrainFavoriteViaSubject { uri subject } } } } }",
260 ),
261 ),
262 ])
263 |> json.to_string
264
265 let request =
266 simulate.request(http.Post, "/graphql")
267 |> simulate.string_body(query)
268 |> simulate.header("content-type", "application/json")
269 |> simulate.header("authorization", "Bearer test-viewer-token")
270
271 let assert Ok(cache) = did_cache.start()
272 let response =
273 graphql_handler.handle_graphql_request(request, exec, cache, None, "", "")
274
275 let assert wisp.Text(body) = response.body
276
277 // Query should succeed (schema generation works)
278 response.status |> should.equal(200)
279
280 // Should contain the gallery URI
281 string.contains(body, gallery_uri) |> should.be_true
282
283 // Viewer field should exist in response (currently null, data lookup needs work)
284 string.contains(body, "viewerSocialGrainFavoriteViaSubject")
285 |> should.be_true
286}
287
288/// Test: Viewer follow field returns null when not following
289pub fn viewer_follow_null_when_not_following_test() {
290 // Setup database
291 let assert Ok(exec) = test_helpers.create_test_db()
292 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
293 let assert Ok(_) = test_helpers.create_record_table(exec)
294 let assert Ok(_) = test_helpers.create_config_table(exec)
295 let assert Ok(_) = test_helpers.create_actor_table(exec)
296 let assert Ok(_) = test_helpers.create_oauth_tables(exec)
297 let assert Ok(_) =
298 test_helpers.insert_test_token(exec, "test-viewer-token", "did:plc:viewer")
299
300 // Insert lexicons
301 let assert Ok(_) =
302 lexicons.insert(exec, "social.grain.gallery", create_gallery_lexicon())
303 let assert Ok(_) =
304 lexicons.insert(exec, "social.grain.graph.follow", create_follow_lexicon())
305
306 // Create a gallery record by a different author
307 let gallery_uri = "at://did:plc:author/social.grain.gallery/gallery1"
308 let gallery_json =
309 json.object([#("title", json.string("Author's Gallery"))])
310 |> json.to_string
311
312 let assert Ok(_) =
313 records.insert(
314 exec,
315 gallery_uri,
316 "cid1",
317 "did:plc:author",
318 "social.grain.gallery",
319 gallery_json,
320 )
321
322 // Query with auth token - should show null for viewerSocialGrainGraphFollowViaSubject
323 let query =
324 json.object([
325 #(
326 "query",
327 json.string(
328 "{ socialGrainGallery { edges { node { uri did viewerSocialGrainGraphFollowViaSubject { uri } } } } }",
329 ),
330 ),
331 ])
332 |> json.to_string
333
334 let request =
335 simulate.request(http.Post, "/graphql")
336 |> simulate.string_body(query)
337 |> simulate.header("content-type", "application/json")
338 |> simulate.header("authorization", "Bearer test-viewer-token")
339
340 let assert Ok(cache) = did_cache.start()
341 let response =
342 graphql_handler.handle_graphql_request(request, exec, cache, None, "", "")
343
344 let assert wisp.Text(body) = response.body
345
346 response.status |> should.equal(200)
347
348 // Should contain the gallery URI
349 string.contains(body, gallery_uri) |> should.be_true
350
351 // The viewer follow field should be null since viewer doesn't follow the author
352 string.contains(body, "viewerSocialGrainGraphFollowViaSubject")
353 |> should.be_true
354 string.contains(body, "null")
355 |> should.be_true
356}
357
358/// Test: Viewer follow field returns follow when viewer follows the author
359pub fn viewer_follow_returns_follow_when_following_test() {
360 // Setup database
361 let assert Ok(exec) = test_helpers.create_test_db()
362 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
363 let assert Ok(_) = test_helpers.create_record_table(exec)
364 let assert Ok(_) = test_helpers.create_config_table(exec)
365 let assert Ok(_) = test_helpers.create_actor_table(exec)
366 let assert Ok(_) = test_helpers.create_oauth_tables(exec)
367 let assert Ok(_) =
368 test_helpers.insert_test_token(exec, "test-viewer-token", "did:plc:viewer")
369
370 // Insert lexicons
371 let assert Ok(_) =
372 lexicons.insert(exec, "social.grain.gallery", create_gallery_lexicon())
373 let assert Ok(_) =
374 lexicons.insert(exec, "social.grain.graph.follow", create_follow_lexicon())
375
376 // Create a gallery record by a different author
377 let gallery_uri = "at://did:plc:author/social.grain.gallery/gallery1"
378 let gallery_json =
379 json.object([#("title", json.string("Author's Gallery"))])
380 |> json.to_string
381
382 let assert Ok(_) =
383 records.insert(
384 exec,
385 gallery_uri,
386 "cid1",
387 "did:plc:author",
388 "social.grain.gallery",
389 gallery_json,
390 )
391
392 // Create a follow record from the viewer to the author
393 let follow_uri = "at://did:plc:viewer/social.grain.graph.follow/follow1"
394 let follow_json =
395 json.object([#("subject", json.string("did:plc:author"))])
396 |> json.to_string
397
398 let assert Ok(_) =
399 records.insert(
400 exec,
401 follow_uri,
402 "cid2",
403 "did:plc:viewer",
404 "social.grain.graph.follow",
405 follow_json,
406 )
407
408 // Query with auth token only (no variables)
409 let query =
410 json.object([
411 #(
412 "query",
413 json.string(
414 "{ socialGrainGallery { edges { node { uri did viewerSocialGrainGraphFollowViaSubject { uri } } } } }",
415 ),
416 ),
417 ])
418 |> json.to_string
419
420 let request =
421 simulate.request(http.Post, "/graphql")
422 |> simulate.string_body(query)
423 |> simulate.header("content-type", "application/json")
424 |> simulate.header("authorization", "Bearer test-viewer-token")
425
426 let assert Ok(cache) = did_cache.start()
427 let response =
428 graphql_handler.handle_graphql_request(request, exec, cache, None, "", "")
429
430 let assert wisp.Text(body) = response.body
431
432 response.status |> should.equal(200)
433
434 // Should contain the gallery URI
435 string.contains(body, gallery_uri) |> should.be_true
436
437 // The viewer follow field should contain the follow URI
438 string.contains(body, follow_uri) |> should.be_true
439}
440
441/// Test: Viewer favorite field returns favorite when viewer has favorited (AT-URI subject)
442pub fn viewer_favorite_returns_favorite_when_favorited_test() {
443 // Setup database
444 let assert Ok(exec) = test_helpers.create_test_db()
445 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
446 let assert Ok(_) = test_helpers.create_record_table(exec)
447 let assert Ok(_) = test_helpers.create_config_table(exec)
448 let assert Ok(_) = test_helpers.create_actor_table(exec)
449 let assert Ok(_) = test_helpers.create_oauth_tables(exec)
450 let assert Ok(_) =
451 test_helpers.insert_test_token(exec, "test-viewer-token", "did:plc:viewer")
452
453 // Insert lexicons
454 let assert Ok(_) =
455 lexicons.insert(exec, "social.grain.gallery", create_gallery_lexicon())
456 let assert Ok(_) =
457 lexicons.insert(exec, "social.grain.favorite", create_favorite_lexicon())
458
459 // Create a gallery record
460 let gallery_uri = "at://did:plc:author/social.grain.gallery/gallery1"
461 let gallery_json =
462 json.object([#("title", json.string("Test Gallery"))])
463 |> json.to_string
464
465 let assert Ok(_) =
466 records.insert(
467 exec,
468 gallery_uri,
469 "cid1",
470 "did:plc:author",
471 "social.grain.gallery",
472 gallery_json,
473 )
474
475 // Create a favorite record from the viewer for the gallery
476 let favorite_uri = "at://did:plc:viewer/social.grain.favorite/fav1"
477 let favorite_json =
478 json.object([#("subject", json.string(gallery_uri))])
479 |> json.to_string
480
481 let assert Ok(_) =
482 records.insert(
483 exec,
484 favorite_uri,
485 "cid2",
486 "did:plc:viewer",
487 "social.grain.favorite",
488 favorite_json,
489 )
490
491 // Query with auth token
492 let query =
493 json.object([
494 #(
495 "query",
496 json.string(
497 "{ socialGrainGallery { edges { node { uri viewerSocialGrainFavoriteViaSubject { uri subject } } } } }",
498 ),
499 ),
500 ])
501 |> json.to_string
502
503 let request =
504 simulate.request(http.Post, "/graphql")
505 |> simulate.string_body(query)
506 |> simulate.header("content-type", "application/json")
507 |> simulate.header("authorization", "Bearer test-viewer-token")
508
509 let assert Ok(cache) = did_cache.start()
510 let response =
511 graphql_handler.handle_graphql_request(request, exec, cache, None, "", "")
512
513 let assert wisp.Text(body) = response.body
514
515 response.status |> should.equal(200)
516
517 // Should contain the gallery URI
518 string.contains(body, gallery_uri) |> should.be_true
519
520 // The viewer favorite field should contain the favorite URI
521 string.contains(body, favorite_uri) |> should.be_true
522}