forked from
slices.network/quickslice
Auto-indexing service and GraphQL API for AT Protocol Records
1/// Integration tests for paginated joins (connections)
2///
3/// Tests verify that:
4/// - DID joins return paginated connections with first/after/last/before
5/// - Reverse joins return paginated connections
6/// - PageInfo is correctly populated
7/// - Cursors work for pagination
8import database/repositories/lexicons
9import database/repositories/records
10import gleam/int
11import gleam/json
12import gleam/list
13import gleam/option
14import gleam/string
15import gleeunit/should
16import graphql/lexicon/schema as lexicon_schema
17import lib/oauth/did_cache
18import test_helpers
19
20// Helper to create a post lexicon JSON
21fn create_post_lexicon() -> String {
22 json.object([
23 #("lexicon", json.int(1)),
24 #("id", json.string("app.bsky.feed.post")),
25 #(
26 "defs",
27 json.object([
28 #(
29 "main",
30 json.object([
31 #("type", json.string("record")),
32 #("key", json.string("tid")),
33 #(
34 "record",
35 json.object([
36 #("type", json.string("object")),
37 #(
38 "required",
39 json.array([json.string("text")], of: fn(x) { x }),
40 ),
41 #(
42 "properties",
43 json.object([
44 #(
45 "text",
46 json.object([
47 #("type", json.string("string")),
48 #("maxLength", json.int(300)),
49 ]),
50 ),
51 ]),
52 ),
53 ]),
54 ),
55 ]),
56 ),
57 ]),
58 ),
59 ])
60 |> json.to_string
61}
62
63// Helper to create a like lexicon JSON with subject field
64fn create_like_lexicon() -> String {
65 json.object([
66 #("lexicon", json.int(1)),
67 #("id", json.string("app.bsky.feed.like")),
68 #(
69 "defs",
70 json.object([
71 #(
72 "main",
73 json.object([
74 #("type", json.string("record")),
75 #("key", json.string("tid")),
76 #(
77 "record",
78 json.object([
79 #("type", json.string("object")),
80 #(
81 "required",
82 json.array([json.string("subject")], of: fn(x) { x }),
83 ),
84 #(
85 "properties",
86 json.object([
87 #(
88 "subject",
89 json.object([
90 #("type", json.string("string")),
91 #("format", json.string("at-uri")),
92 ]),
93 ),
94 #(
95 "createdAt",
96 json.object([
97 #("type", json.string("string")),
98 #("format", json.string("datetime")),
99 ]),
100 ),
101 ]),
102 ),
103 ]),
104 ),
105 ]),
106 ),
107 ]),
108 ),
109 ])
110 |> json.to_string
111}
112
113// Helper to create a profile lexicon with literal:self key
114fn create_profile_lexicon() -> String {
115 json.object([
116 #("lexicon", json.int(1)),
117 #("id", json.string("app.bsky.actor.profile")),
118 #(
119 "defs",
120 json.object([
121 #(
122 "main",
123 json.object([
124 #("type", json.string("record")),
125 #("key", json.string("literal:self")),
126 #(
127 "record",
128 json.object([
129 #("type", json.string("object")),
130 #(
131 "properties",
132 json.object([
133 #(
134 "displayName",
135 json.object([#("type", json.string("string"))]),
136 ),
137 ]),
138 ),
139 ]),
140 ),
141 ]),
142 ),
143 ]),
144 ),
145 ])
146 |> json.to_string
147}
148
149// Test: DID join with first:1 returns only 1 result
150pub fn did_join_first_one_test() {
151 // Setup database
152 let assert Ok(exec) = test_helpers.create_test_db()
153 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
154 let assert Ok(_) = test_helpers.create_record_table(exec)
155 let assert Ok(_) = test_helpers.create_actor_table(exec)
156
157 // Insert lexicons
158 let post_lexicon = create_post_lexicon()
159 let profile_lexicon = create_profile_lexicon()
160 let assert Ok(_) = lexicons.insert(exec, "app.bsky.feed.post", post_lexicon)
161 let assert Ok(_) =
162 lexicons.insert(exec, "app.bsky.actor.profile", profile_lexicon)
163
164 // Insert a profile
165 let profile_uri = "at://did:plc:author/app.bsky.actor.profile/self"
166 let profile_json =
167 json.object([#("displayName", json.string("Author"))])
168 |> json.to_string
169
170 let assert Ok(_) =
171 records.insert(
172 exec,
173 profile_uri,
174 "cid_profile",
175 "did:plc:author",
176 "app.bsky.actor.profile",
177 profile_json,
178 )
179
180 // Insert 5 posts by the same DID
181 list.range(1, 5)
182 |> list.each(fn(i) {
183 let post_uri =
184 "at://did:plc:author/app.bsky.feed.post/post" <> int.to_string(i)
185 let post_json =
186 json.object([
187 #("text", json.string("Post number " <> int.to_string(i))),
188 ])
189 |> json.to_string
190
191 let assert Ok(_) =
192 records.insert(
193 exec,
194 post_uri,
195 "cid_post" <> int.to_string(i),
196 "did:plc:author",
197 "app.bsky.feed.post",
198 post_json,
199 )
200 Nil
201 })
202
203 // Execute GraphQL query with DID join and first:1
204 let query =
205 "
206 {
207 appBskyActorProfile {
208 edges {
209 node {
210 uri
211 appBskyFeedPostByDid(first: 1) {
212 edges {
213 node {
214 uri
215 text
216 }
217 }
218 pageInfo {
219 hasNextPage
220 hasPreviousPage
221 }
222 }
223 }
224 }
225 }
226 }
227 "
228
229 let assert Ok(cache) = did_cache.start()
230 let assert Ok(response_json) =
231 lexicon_schema.execute_query_with_db(
232 exec,
233 query,
234 "{}",
235 Error(Nil),
236 cache,
237 option.None,
238 "",
239 "https://plc.directory",
240 )
241
242 // Verify only 1 post is returned
243 string.contains(response_json, "\"edges\"")
244 |> should.be_true
245
246 // Count how many post URIs appear (should be 1)
247 let post_count =
248 list.range(1, 5)
249 |> list.filter(fn(i) {
250 string.contains(
251 response_json,
252 "at://did:plc:author/app.bsky.feed.post/post" <> int.to_string(i),
253 )
254 })
255 |> list.length
256
257 post_count
258 |> should.equal(1)
259
260 // Verify hasNextPage is true (more posts available)
261 {
262 string.contains(response_json, "\"hasNextPage\":true")
263 || string.contains(response_json, "\"hasNextPage\": true")
264 }
265 |> should.be_true
266}
267
268// Test: DID join with first:2 returns only 2 results
269pub fn did_join_first_two_test() {
270 // Setup database
271 let assert Ok(exec) = test_helpers.create_test_db()
272 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
273 let assert Ok(_) = test_helpers.create_record_table(exec)
274 let assert Ok(_) = test_helpers.create_actor_table(exec)
275
276 // Insert lexicons
277 let post_lexicon = create_post_lexicon()
278 let profile_lexicon = create_profile_lexicon()
279 let assert Ok(_) = lexicons.insert(exec, "app.bsky.feed.post", post_lexicon)
280 let assert Ok(_) =
281 lexicons.insert(exec, "app.bsky.actor.profile", profile_lexicon)
282
283 // Insert a profile
284 let profile_uri = "at://did:plc:author/app.bsky.actor.profile/self"
285 let profile_json =
286 json.object([#("displayName", json.string("Author"))])
287 |> json.to_string
288
289 let assert Ok(_) =
290 records.insert(
291 exec,
292 profile_uri,
293 "cid_profile",
294 "did:plc:author",
295 "app.bsky.actor.profile",
296 profile_json,
297 )
298
299 // Insert 5 posts by the same DID
300 list.range(1, 5)
301 |> list.each(fn(i) {
302 let post_uri =
303 "at://did:plc:author/app.bsky.feed.post/post" <> int.to_string(i)
304 let post_json =
305 json.object([
306 #("text", json.string("Post number " <> int.to_string(i))),
307 ])
308 |> json.to_string
309
310 let assert Ok(_) =
311 records.insert(
312 exec,
313 post_uri,
314 "cid_post" <> int.to_string(i),
315 "did:plc:author",
316 "app.bsky.feed.post",
317 post_json,
318 )
319 Nil
320 })
321
322 // Execute GraphQL query with DID join and first:2
323 let query =
324 "
325 {
326 appBskyActorProfile {
327 edges {
328 node {
329 uri
330 appBskyFeedPostByDid(first: 2) {
331 edges {
332 node {
333 uri
334 text
335 }
336 }
337 pageInfo {
338 hasNextPage
339 hasPreviousPage
340 }
341 }
342 }
343 }
344 }
345 }
346 "
347
348 let assert Ok(cache) = did_cache.start()
349 let assert Ok(response_json) =
350 lexicon_schema.execute_query_with_db(
351 exec,
352 query,
353 "{}",
354 Error(Nil),
355 cache,
356 option.None,
357 "",
358 "https://plc.directory",
359 )
360
361 // Count how many post URIs appear (should be 2)
362 let post_count =
363 list.range(1, 5)
364 |> list.filter(fn(i) {
365 string.contains(
366 response_json,
367 "at://did:plc:author/app.bsky.feed.post/post" <> int.to_string(i),
368 )
369 })
370 |> list.length
371
372 post_count
373 |> should.equal(2)
374
375 // Verify hasNextPage is true (more posts available)
376 {
377 string.contains(response_json, "\"hasNextPage\":true")
378 || string.contains(response_json, "\"hasNextPage\": true")
379 }
380 |> should.be_true
381}
382
383// Test: Reverse join with first:1 returns only 1 result
384pub fn reverse_join_first_one_test() {
385 // Setup database
386 let assert Ok(exec) = test_helpers.create_test_db()
387 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
388 let assert Ok(_) = test_helpers.create_record_table(exec)
389 let assert Ok(_) = test_helpers.create_actor_table(exec)
390
391 // Insert lexicons
392 let post_lexicon = create_post_lexicon()
393 let like_lexicon = create_like_lexicon()
394 let assert Ok(_) = lexicons.insert(exec, "app.bsky.feed.post", post_lexicon)
395 let assert Ok(_) = lexicons.insert(exec, "app.bsky.feed.like", like_lexicon)
396
397 // Insert a post
398 let post_uri = "at://did:plc:author/app.bsky.feed.post/post1"
399 let post_json =
400 json.object([#("text", json.string("Great post!"))])
401 |> json.to_string
402
403 let assert Ok(_) =
404 records.insert(
405 exec,
406 post_uri,
407 "cid_post",
408 "did:plc:author",
409 "app.bsky.feed.post",
410 post_json,
411 )
412
413 // Insert 5 likes that reference the post
414 list.range(1, 5)
415 |> list.each(fn(i) {
416 let like_uri =
417 "at://did:plc:liker"
418 <> int.to_string(i)
419 <> "/app.bsky.feed.like/like"
420 <> int.to_string(i)
421 let like_json =
422 json.object([
423 #("subject", json.string(post_uri)),
424 #("createdAt", json.string("2024-01-01T12:00:00Z")),
425 ])
426 |> json.to_string
427
428 let assert Ok(_) =
429 records.insert(
430 exec,
431 like_uri,
432 "cid_like" <> int.to_string(i),
433 "did:plc:liker" <> int.to_string(i),
434 "app.bsky.feed.like",
435 like_json,
436 )
437 Nil
438 })
439
440 // Execute GraphQL query with reverse join and first:1
441 let query =
442 "
443 {
444 appBskyFeedPost {
445 edges {
446 node {
447 uri
448 appBskyFeedLikeViaSubject(first: 1) {
449 edges {
450 node {
451 uri
452 }
453 }
454 pageInfo {
455 hasNextPage
456 hasPreviousPage
457 }
458 }
459 }
460 }
461 }
462 }
463 "
464
465 let assert Ok(cache) = did_cache.start()
466 let assert Ok(response_json) =
467 lexicon_schema.execute_query_with_db(
468 exec,
469 query,
470 "{}",
471 Error(Nil),
472 cache,
473 option.None,
474 "",
475 "https://plc.directory",
476 )
477
478 // Count how many like URIs appear (should be 1)
479 let like_count =
480 list.range(1, 5)
481 |> list.filter(fn(i) {
482 string.contains(
483 response_json,
484 "at://did:plc:liker"
485 <> int.to_string(i)
486 <> "/app.bsky.feed.like/like"
487 <> int.to_string(i),
488 )
489 })
490 |> list.length
491
492 like_count
493 |> should.equal(1)
494
495 // Verify hasNextPage is true (more likes available)
496 {
497 string.contains(response_json, "\"hasNextPage\":true")
498 || string.contains(response_json, "\"hasNextPage\": true")
499 }
500 |> should.be_true
501}
502
503// Test: DID join with no pagination args defaults to first:50
504pub fn did_join_default_pagination_test() {
505 // Setup database
506 let assert Ok(exec) = test_helpers.create_test_db()
507 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
508 let assert Ok(_) = test_helpers.create_record_table(exec)
509 let assert Ok(_) = test_helpers.create_actor_table(exec)
510
511 // Insert lexicons
512 let post_lexicon = create_post_lexicon()
513 let profile_lexicon = create_profile_lexicon()
514 let assert Ok(_) = lexicons.insert(exec, "app.bsky.feed.post", post_lexicon)
515 let assert Ok(_) =
516 lexicons.insert(exec, "app.bsky.actor.profile", profile_lexicon)
517
518 // Insert a profile
519 let profile_uri = "at://did:plc:author/app.bsky.actor.profile/self"
520 let profile_json =
521 json.object([#("displayName", json.string("Author"))])
522 |> json.to_string
523
524 let assert Ok(_) =
525 records.insert(
526 exec,
527 profile_uri,
528 "cid_profile",
529 "did:plc:author",
530 "app.bsky.actor.profile",
531 profile_json,
532 )
533
534 // Insert 3 posts by the same DID
535 list.range(1, 3)
536 |> list.each(fn(i) {
537 let post_uri =
538 "at://did:plc:author/app.bsky.feed.post/post" <> int.to_string(i)
539 let post_json =
540 json.object([
541 #("text", json.string("Post number " <> int.to_string(i))),
542 ])
543 |> json.to_string
544
545 let assert Ok(_) =
546 records.insert(
547 exec,
548 post_uri,
549 "cid_post" <> int.to_string(i),
550 "did:plc:author",
551 "app.bsky.feed.post",
552 post_json,
553 )
554 Nil
555 })
556
557 // Execute GraphQL query with DID join and NO pagination args (should default to first:50)
558 let query =
559 "
560 {
561 appBskyActorProfile {
562 edges {
563 node {
564 uri
565 appBskyFeedPostByDid {
566 edges {
567 node {
568 uri
569 text
570 }
571 }
572 pageInfo {
573 hasNextPage
574 hasPreviousPage
575 }
576 }
577 }
578 }
579 }
580 }
581 "
582
583 let assert Ok(cache) = did_cache.start()
584 let assert Ok(response_json) =
585 lexicon_schema.execute_query_with_db(
586 exec,
587 query,
588 "{}",
589 Error(Nil),
590 cache,
591 option.None,
592 "",
593 "https://plc.directory",
594 )
595
596 // All 3 posts should be returned (within default limit of 50)
597 let post_count =
598 list.range(1, 3)
599 |> list.filter(fn(i) {
600 string.contains(
601 response_json,
602 "at://did:plc:author/app.bsky.feed.post/post" <> int.to_string(i),
603 )
604 })
605 |> list.length
606
607 post_count
608 |> should.equal(3)
609
610 // Verify hasNextPage is false (no more posts)
611 {
612 string.contains(response_json, "\"hasNextPage\":false")
613 || string.contains(response_json, "\"hasNextPage\": false")
614 }
615 |> should.be_true
616}