Auto-indexing service and GraphQL API for AT Protocol Records quickslice.slices.network/
atproto gleam graphql
at main 407 lines 11 kB view raw
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}