Auto-indexing service and GraphQL API for AT Protocol Records
at main 616 lines 15 kB view raw
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}