Auto-indexing service and GraphQL API for AT Protocol Records
quickslice.slices.network/
atproto
gleam
graphql
1/// Integration tests for GraphQL totalCount field
2///
3/// These tests verify that totalCount is correctly returned in connection queries
4import database/repositories/actors
5import database/repositories/lexicons
6import database/repositories/records
7import gleam/http
8import gleam/int
9import gleam/json
10import gleam/list
11import gleam/option
12import gleam/string
13import gleeunit/should
14import handlers/graphql as graphql_handler
15import lib/oauth/did_cache
16import test_helpers
17import wisp
18import wisp/simulate
19
20// Helper to create a status lexicon
21fn create_status_lexicon() -> String {
22 json.object([
23 #("lexicon", json.int(1)),
24 #("id", json.string("xyz.statusphere.status")),
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(
40 [json.string("status"), json.string("createdAt")],
41 of: fn(x) { x },
42 ),
43 ),
44 #(
45 "properties",
46 json.object([
47 #(
48 "status",
49 json.object([
50 #("type", json.string("string")),
51 #("minLength", json.int(1)),
52 #("maxGraphemes", json.int(1)),
53 #("maxLength", json.int(32)),
54 ]),
55 ),
56 #(
57 "createdAt",
58 json.object([
59 #("type", json.string("string")),
60 #("format", json.string("datetime")),
61 ]),
62 ),
63 ]),
64 ),
65 ]),
66 ),
67 ]),
68 ),
69 ]),
70 ),
71 ])
72 |> json.to_string
73}
74
75// Helper function to create a range of integers
76fn list_range(from: Int, to: Int) -> List(Int) {
77 list_range_helper(from, to, [])
78 |> list.reverse
79}
80
81fn list_range_helper(current: Int, to: Int, acc: List(Int)) -> List(Int) {
82 case current > to {
83 True -> acc
84 False -> list_range_helper(current + 1, to, [current, ..acc])
85 }
86}
87
88pub fn graphql_total_count_basic_test() {
89 // Create in-memory database
90 let assert Ok(exec) = test_helpers.create_test_db()
91 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
92 let assert Ok(_) = test_helpers.create_record_table(exec)
93 let assert Ok(_) = test_helpers.create_actor_table(exec)
94
95 // Insert a lexicon
96 let lexicon = create_status_lexicon()
97 let assert Ok(_) = lexicons.insert(exec, "xyz.statusphere.status", lexicon)
98
99 // Insert 5 test records
100 let _ =
101 list_range(1, 5)
102 |> list.each(fn(i) {
103 let uri = "at://did:plc:test/xyz.statusphere.status/" <> int.to_string(i)
104 let cid = "cid" <> int.to_string(i)
105 let json_data =
106 json.object([
107 #("status", json.string("✨")),
108 #("createdAt", json.string("2024-01-01T00:00:00Z")),
109 ])
110 |> json.to_string
111 let assert Ok(_) =
112 records.insert(
113 exec,
114 uri,
115 cid,
116 "did:plc:test",
117 "xyz.statusphere.status",
118 json_data,
119 )
120 Nil
121 })
122
123 // Query with totalCount field
124 let query =
125 json.object([
126 #(
127 "query",
128 json.string(
129 "{ xyzStatusphereStatus { totalCount edges { node { uri } } pageInfo { hasNextPage } } }",
130 ),
131 ),
132 ])
133 |> json.to_string
134
135 let request =
136 simulate.request(http.Post, "/graphql")
137 |> simulate.string_body(query)
138 |> simulate.header("content-type", "application/json")
139
140 let assert Ok(cache) = did_cache.start()
141 let response =
142 graphql_handler.handle_graphql_request(
143 request,
144 exec,
145 cache,
146 option.None,
147 "",
148 "https://plc.directory",
149 )
150
151 // Verify response
152 response.status
153 |> should.equal(200)
154
155 let assert wisp.Text(body) = response.body
156
157 // Should contain totalCount field
158 string.contains(body, "totalCount")
159 |> should.be_true
160
161 // Should contain the count value (5 records)
162 string.contains(body, "\"totalCount\": 5")
163 |> should.be_true
164 // Clean up
165}
166
167pub fn graphql_total_count_with_filter_test() {
168 // Create in-memory database
169 let assert Ok(exec) = test_helpers.create_test_db()
170 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
171 let assert Ok(_) = test_helpers.create_record_table(exec)
172 let assert Ok(_) = test_helpers.create_actor_table(exec)
173
174 // Insert a lexicon
175 let lexicon = create_status_lexicon()
176 let assert Ok(_) = lexicons.insert(exec, "xyz.statusphere.status", lexicon)
177
178 // Insert test actors
179 let assert Ok(_) = actors.upsert(exec, "did:plc:alice", "alice.bsky.social")
180 let assert Ok(_) = actors.upsert(exec, "did:plc:bob", "bob.bsky.social")
181
182 // Insert 3 records for alice
183 let _ =
184 list_range(1, 3)
185 |> list.each(fn(i) {
186 let uri = "at://did:plc:alice/xyz.statusphere.status/" <> int.to_string(i)
187 let cid = "alice_cid" <> int.to_string(i)
188 let json_data =
189 json.object([
190 #("status", json.string("👍")),
191 #("createdAt", json.string("2024-01-01T00:00:00Z")),
192 ])
193 |> json.to_string
194 let assert Ok(_) =
195 records.insert(
196 exec,
197 uri,
198 cid,
199 "did:plc:alice",
200 "xyz.statusphere.status",
201 json_data,
202 )
203 Nil
204 })
205
206 // Insert 2 records for bob
207 let _ =
208 list_range(1, 2)
209 |> list.each(fn(i) {
210 let uri = "at://did:plc:bob/xyz.statusphere.status/" <> int.to_string(i)
211 let cid = "bob_cid" <> int.to_string(i)
212 let json_data =
213 json.object([
214 #("status", json.string("🔥")),
215 #("createdAt", json.string("2024-01-02T00:00:00Z")),
216 ])
217 |> json.to_string
218 let assert Ok(_) =
219 records.insert(
220 exec,
221 uri,
222 cid,
223 "did:plc:bob",
224 "xyz.statusphere.status",
225 json_data,
226 )
227 Nil
228 })
229
230 // Query with totalCount and filter by actorHandle
231 let query =
232 json.object([
233 #(
234 "query",
235 json.string(
236 "{ xyzStatusphereStatus(where: {actorHandle: {eq: \"alice.bsky.social\"}}) { totalCount edges { node { uri actorHandle } } } }",
237 ),
238 ),
239 ])
240 |> json.to_string
241
242 let request =
243 simulate.request(http.Post, "/graphql")
244 |> simulate.string_body(query)
245 |> simulate.header("content-type", "application/json")
246
247 let assert Ok(cache) = did_cache.start()
248 let response =
249 graphql_handler.handle_graphql_request(
250 request,
251 exec,
252 cache,
253 option.None,
254 "",
255 "https://plc.directory",
256 )
257
258 // Verify response
259 response.status
260 |> should.equal(200)
261
262 let assert wisp.Text(body) = response.body
263
264 // Should contain totalCount field
265 string.contains(body, "totalCount")
266 |> should.be_true
267
268 // Should contain count of 3 (only alice's records)
269 string.contains(body, "\"totalCount\": 3")
270 |> should.be_true
271
272 // Should only contain alice's records
273 string.contains(body, "alice.bsky.social")
274 |> should.be_true
275
276 string.contains(body, "bob.bsky.social")
277 |> should.be_false
278 // Clean up
279}
280
281pub fn graphql_total_count_empty_result_test() {
282 // Create in-memory database
283 let assert Ok(exec) = test_helpers.create_test_db()
284 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
285 let assert Ok(_) = test_helpers.create_record_table(exec)
286
287 // Insert a lexicon
288 let lexicon = create_status_lexicon()
289 let assert Ok(_) = lexicons.insert(exec, "xyz.statusphere.status", lexicon)
290
291 // Query with totalCount field (no records inserted)
292 let query =
293 json.object([
294 #(
295 "query",
296 json.string(
297 "{ xyzStatusphereStatus { totalCount edges { node { uri } } pageInfo { hasNextPage } } }",
298 ),
299 ),
300 ])
301 |> json.to_string
302
303 let request =
304 simulate.request(http.Post, "/graphql")
305 |> simulate.string_body(query)
306 |> simulate.header("content-type", "application/json")
307
308 let assert Ok(cache) = did_cache.start()
309 let response =
310 graphql_handler.handle_graphql_request(
311 request,
312 exec,
313 cache,
314 option.None,
315 "",
316 "https://plc.directory",
317 )
318
319 // Verify response
320 response.status
321 |> should.equal(200)
322
323 let assert wisp.Text(body) = response.body
324
325 // Should contain totalCount of 0
326 string.contains(body, "\"totalCount\": 0")
327 |> should.be_true
328 // Clean up
329}
330
331pub fn graphql_total_count_with_pagination_test() {
332 // Create in-memory database
333 let assert Ok(exec) = test_helpers.create_test_db()
334 let assert Ok(_) = test_helpers.create_lexicon_table(exec)
335 let assert Ok(_) = test_helpers.create_record_table(exec)
336
337 // Insert a lexicon
338 let lexicon = create_status_lexicon()
339 let assert Ok(_) = lexicons.insert(exec, "xyz.statusphere.status", lexicon)
340
341 // Insert 10 test records
342 let _ =
343 list_range(1, 10)
344 |> list.each(fn(i) {
345 let uri = "at://did:plc:test/xyz.statusphere.status/" <> int.to_string(i)
346 let cid = "cid" <> int.to_string(i)
347 let json_data =
348 json.object([
349 #("status", json.string("✨")),
350 #("createdAt", json.string("2024-01-01T00:00:00Z")),
351 ])
352 |> json.to_string
353 let assert Ok(_) =
354 records.insert(
355 exec,
356 uri,
357 cid,
358 "did:plc:test",
359 "xyz.statusphere.status",
360 json_data,
361 )
362 Nil
363 })
364
365 // Query with totalCount and pagination (first: 3)
366 let query =
367 json.object([
368 #(
369 "query",
370 json.string(
371 "{ xyzStatusphereStatus(first: 3) { totalCount edges { node { uri } } pageInfo { hasNextPage } } }",
372 ),
373 ),
374 ])
375 |> json.to_string
376
377 let request =
378 simulate.request(http.Post, "/graphql")
379 |> simulate.string_body(query)
380 |> simulate.header("content-type", "application/json")
381
382 let assert Ok(cache) = did_cache.start()
383 let response =
384 graphql_handler.handle_graphql_request(
385 request,
386 exec,
387 cache,
388 option.None,
389 "",
390 "https://plc.directory",
391 )
392
393 // Verify response
394 response.status
395 |> should.equal(200)
396
397 let assert wisp.Text(body) = response.body
398
399 // totalCount should still be 10 (total records, not just the page)
400 string.contains(body, "\"totalCount\": 10")
401 |> should.be_true
402
403 // hasNextPage should be true (more records available)
404 string.contains(body, "\"hasNextPage\": true")
405 |> should.be_true
406 // Clean up
407}