I've been saying "PDSes seem easy enough, they're what, some CRUD to a db? I can do that in my sleep". well i'm sleeping rn so let's go
at main 25 kB view raw
1mod common; 2mod helpers; 3use chrono::Utc; 4use common::*; 5use helpers::*; 6use reqwest::{StatusCode, header}; 7use serde_json::{Value, json}; 8use std::time::Duration; 9 10#[tokio::test] 11async fn test_record_crud_lifecycle() { 12 let client = client(); 13 let (did, jwt) = setup_new_user("lifecycle-crud").await; 14 let collection = "app.bsky.feed.post"; 15 let rkey = format!("e2e_lifecycle_{}", Utc::now().timestamp_millis()); 16 let now = Utc::now().to_rfc3339(); 17 let original_text = "Hello from the lifecycle test!"; 18 let create_payload = json!({ 19 "repo": did, 20 "collection": collection, 21 "rkey": rkey, 22 "record": { 23 "$type": collection, 24 "text": original_text, 25 "createdAt": now 26 } 27 }); 28 let create_res = client 29 .post(format!( 30 "{}/xrpc/com.atproto.repo.putRecord", 31 base_url().await 32 )) 33 .bearer_auth(&jwt) 34 .json(&create_payload) 35 .send() 36 .await 37 .expect("Failed to send create request"); 38 assert_eq!( 39 create_res.status(), 40 StatusCode::OK, 41 "Failed to create record" 42 ); 43 let create_body: Value = create_res 44 .json() 45 .await 46 .expect("create response was not JSON"); 47 let uri = create_body["uri"].as_str().unwrap(); 48 let initial_cid = create_body["cid"].as_str().unwrap().to_string(); 49 let params = [ 50 ("repo", did.as_str()), 51 ("collection", collection), 52 ("rkey", &rkey), 53 ]; 54 let get_res = client 55 .get(format!( 56 "{}/xrpc/com.atproto.repo.getRecord", 57 base_url().await 58 )) 59 .query(&params) 60 .send() 61 .await 62 .expect("Failed to send get request"); 63 assert_eq!( 64 get_res.status(), 65 StatusCode::OK, 66 "Failed to get record after create" 67 ); 68 let get_body: Value = get_res.json().await.expect("get response was not JSON"); 69 assert_eq!(get_body["uri"], uri); 70 assert_eq!(get_body["value"]["text"], original_text); 71 let updated_text = "This post has been updated."; 72 let update_payload = json!({ 73 "repo": did, 74 "collection": collection, 75 "rkey": rkey, 76 "record": { "$type": collection, "text": updated_text, "createdAt": now }, 77 "swapRecord": initial_cid 78 }); 79 let update_res = client 80 .post(format!( 81 "{}/xrpc/com.atproto.repo.putRecord", 82 base_url().await 83 )) 84 .bearer_auth(&jwt) 85 .json(&update_payload) 86 .send() 87 .await 88 .expect("Failed to send update request"); 89 assert_eq!( 90 update_res.status(), 91 StatusCode::OK, 92 "Failed to update record" 93 ); 94 let update_body: Value = update_res 95 .json() 96 .await 97 .expect("update response was not JSON"); 98 let updated_cid = update_body["cid"].as_str().unwrap().to_string(); 99 let get_updated_res = client 100 .get(format!( 101 "{}/xrpc/com.atproto.repo.getRecord", 102 base_url().await 103 )) 104 .query(&params) 105 .send() 106 .await 107 .expect("Failed to send get-after-update request"); 108 let get_updated_body: Value = get_updated_res 109 .json() 110 .await 111 .expect("get-updated response was not JSON"); 112 assert_eq!( 113 get_updated_body["value"]["text"], updated_text, 114 "Text was not updated" 115 ); 116 let stale_update_payload = json!({ 117 "repo": did, 118 "collection": collection, 119 "rkey": rkey, 120 "record": { "$type": collection, "text": "Stale update", "createdAt": now }, 121 "swapRecord": initial_cid 122 }); 123 let stale_res = client 124 .post(format!( 125 "{}/xrpc/com.atproto.repo.putRecord", 126 base_url().await 127 )) 128 .bearer_auth(&jwt) 129 .json(&stale_update_payload) 130 .send() 131 .await 132 .expect("Failed to send stale update"); 133 assert_eq!( 134 stale_res.status(), 135 StatusCode::CONFLICT, 136 "Stale update should cause 409" 137 ); 138 let good_update_payload = json!({ 139 "repo": did, 140 "collection": collection, 141 "rkey": rkey, 142 "record": { "$type": collection, "text": "Good update", "createdAt": now }, 143 "swapRecord": updated_cid 144 }); 145 let good_res = client 146 .post(format!( 147 "{}/xrpc/com.atproto.repo.putRecord", 148 base_url().await 149 )) 150 .bearer_auth(&jwt) 151 .json(&good_update_payload) 152 .send() 153 .await 154 .expect("Failed to send good update"); 155 assert_eq!( 156 good_res.status(), 157 StatusCode::OK, 158 "Good update should succeed" 159 ); 160 let delete_payload = json!({ "repo": did, "collection": collection, "rkey": rkey }); 161 let delete_res = client 162 .post(format!( 163 "{}/xrpc/com.atproto.repo.deleteRecord", 164 base_url().await 165 )) 166 .bearer_auth(&jwt) 167 .json(&delete_payload) 168 .send() 169 .await 170 .expect("Failed to send delete request"); 171 assert_eq!( 172 delete_res.status(), 173 StatusCode::OK, 174 "Failed to delete record" 175 ); 176 let get_deleted_res = client 177 .get(format!( 178 "{}/xrpc/com.atproto.repo.getRecord", 179 base_url().await 180 )) 181 .query(&params) 182 .send() 183 .await 184 .expect("Failed to send get-after-delete request"); 185 assert_eq!( 186 get_deleted_res.status(), 187 StatusCode::NOT_FOUND, 188 "Record should be deleted" 189 ); 190} 191 192#[tokio::test] 193async fn test_profile_with_blob_lifecycle() { 194 let client = client(); 195 let (did, jwt) = setup_new_user("profile-blob").await; 196 let blob_data = b"This is test blob data for a profile avatar"; 197 let upload_res = client 198 .post(format!( 199 "{}/xrpc/com.atproto.repo.uploadBlob", 200 base_url().await 201 )) 202 .header(header::CONTENT_TYPE, "text/plain") 203 .bearer_auth(&jwt) 204 .body(blob_data.to_vec()) 205 .send() 206 .await 207 .expect("Failed to upload blob"); 208 assert_eq!(upload_res.status(), StatusCode::OK); 209 let upload_body: Value = upload_res.json().await.unwrap(); 210 let blob_ref = upload_body["blob"].clone(); 211 let profile_payload = json!({ 212 "repo": did, 213 "collection": "app.bsky.actor.profile", 214 "rkey": "self", 215 "record": { 216 "$type": "app.bsky.actor.profile", 217 "displayName": "Test User", 218 "description": "A test profile for lifecycle testing", 219 "avatar": blob_ref 220 } 221 }); 222 let create_res = client 223 .post(format!( 224 "{}/xrpc/com.atproto.repo.putRecord", 225 base_url().await 226 )) 227 .bearer_auth(&jwt) 228 .json(&profile_payload) 229 .send() 230 .await 231 .expect("Failed to create profile"); 232 assert_eq!( 233 create_res.status(), 234 StatusCode::OK, 235 "Failed to create profile" 236 ); 237 let create_body: Value = create_res.json().await.unwrap(); 238 let initial_cid = create_body["cid"].as_str().unwrap().to_string(); 239 let get_res = client 240 .get(format!( 241 "{}/xrpc/com.atproto.repo.getRecord", 242 base_url().await 243 )) 244 .query(&[ 245 ("repo", did.as_str()), 246 ("collection", "app.bsky.actor.profile"), 247 ("rkey", "self"), 248 ]) 249 .send() 250 .await 251 .expect("Failed to get profile"); 252 assert_eq!(get_res.status(), StatusCode::OK); 253 let get_body: Value = get_res.json().await.unwrap(); 254 assert_eq!(get_body["value"]["displayName"], "Test User"); 255 assert!(get_body["value"]["avatar"]["ref"]["$link"].is_string()); 256 let update_payload = json!({ 257 "repo": did, 258 "collection": "app.bsky.actor.profile", 259 "rkey": "self", 260 "record": { "$type": "app.bsky.actor.profile", "displayName": "Updated User", "description": "Profile updated" }, 261 "swapRecord": initial_cid 262 }); 263 let update_res = client 264 .post(format!( 265 "{}/xrpc/com.atproto.repo.putRecord", 266 base_url().await 267 )) 268 .bearer_auth(&jwt) 269 .json(&update_payload) 270 .send() 271 .await 272 .expect("Failed to update profile"); 273 assert_eq!( 274 update_res.status(), 275 StatusCode::OK, 276 "Failed to update profile" 277 ); 278 let get_updated_res = client 279 .get(format!( 280 "{}/xrpc/com.atproto.repo.getRecord", 281 base_url().await 282 )) 283 .query(&[ 284 ("repo", did.as_str()), 285 ("collection", "app.bsky.actor.profile"), 286 ("rkey", "self"), 287 ]) 288 .send() 289 .await 290 .expect("Failed to get updated profile"); 291 let updated_body: Value = get_updated_res.json().await.unwrap(); 292 assert_eq!(updated_body["value"]["displayName"], "Updated User"); 293} 294 295#[tokio::test] 296async fn test_reply_thread_lifecycle() { 297 let client = client(); 298 let (alice_did, alice_jwt) = setup_new_user("alice-thread").await; 299 let (bob_did, bob_jwt) = setup_new_user("bob-thread").await; 300 let (root_uri, root_cid) = 301 create_post(&client, &alice_did, &alice_jwt, "This is the root post").await; 302 tokio::time::sleep(Duration::from_millis(100)).await; 303 let reply_collection = "app.bsky.feed.post"; 304 let reply_rkey = format!("e2e_reply_{}", Utc::now().timestamp_millis()); 305 let reply_payload = json!({ 306 "repo": bob_did, 307 "collection": reply_collection, 308 "rkey": reply_rkey, 309 "record": { 310 "$type": reply_collection, 311 "text": "This is Bob's reply to Alice", 312 "createdAt": Utc::now().to_rfc3339(), 313 "reply": { 314 "root": { "uri": root_uri, "cid": root_cid }, 315 "parent": { "uri": root_uri, "cid": root_cid } 316 } 317 } 318 }); 319 let reply_res = client 320 .post(format!( 321 "{}/xrpc/com.atproto.repo.putRecord", 322 base_url().await 323 )) 324 .bearer_auth(&bob_jwt) 325 .json(&reply_payload) 326 .send() 327 .await 328 .expect("Failed to create reply"); 329 assert_eq!(reply_res.status(), StatusCode::OK, "Failed to create reply"); 330 let reply_body: Value = reply_res.json().await.unwrap(); 331 let reply_uri = reply_body["uri"].as_str().unwrap(); 332 let reply_cid = reply_body["cid"].as_str().unwrap(); 333 let get_reply_res = client 334 .get(format!( 335 "{}/xrpc/com.atproto.repo.getRecord", 336 base_url().await 337 )) 338 .query(&[ 339 ("repo", bob_did.as_str()), 340 ("collection", reply_collection), 341 ("rkey", reply_rkey.as_str()), 342 ]) 343 .send() 344 .await 345 .expect("Failed to get reply"); 346 assert_eq!(get_reply_res.status(), StatusCode::OK); 347 let reply_record: Value = get_reply_res.json().await.unwrap(); 348 assert_eq!(reply_record["value"]["reply"]["root"]["uri"], root_uri); 349 tokio::time::sleep(Duration::from_millis(100)).await; 350 let nested_reply_rkey = format!("e2e_nested_reply_{}", Utc::now().timestamp_millis()); 351 let nested_payload = json!({ 352 "repo": alice_did, 353 "collection": reply_collection, 354 "rkey": nested_reply_rkey, 355 "record": { 356 "$type": reply_collection, 357 "text": "Alice replies to Bob's reply", 358 "createdAt": Utc::now().to_rfc3339(), 359 "reply": { 360 "root": { "uri": root_uri, "cid": root_cid }, 361 "parent": { "uri": reply_uri, "cid": reply_cid } 362 } 363 } 364 }); 365 let nested_res = client 366 .post(format!( 367 "{}/xrpc/com.atproto.repo.putRecord", 368 base_url().await 369 )) 370 .bearer_auth(&alice_jwt) 371 .json(&nested_payload) 372 .send() 373 .await 374 .expect("Failed to create nested reply"); 375 assert_eq!( 376 nested_res.status(), 377 StatusCode::OK, 378 "Failed to create nested reply" 379 ); 380} 381 382#[tokio::test] 383async fn test_authorization_protects_repos() { 384 let client = client(); 385 let (alice_did, alice_jwt) = setup_new_user("alice-auth").await; 386 let (_bob_did, bob_jwt) = setup_new_user("bob-auth").await; 387 let (post_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's post").await; 388 let post_rkey = post_uri.split('/').next_back().unwrap(); 389 let post_payload = json!({ 390 "repo": alice_did, 391 "collection": "app.bsky.feed.post", 392 "rkey": "unauthorized-post", 393 "record": { "$type": "app.bsky.feed.post", "text": "Bob trying to post as Alice", "createdAt": Utc::now().to_rfc3339() } 394 }); 395 let write_res = client 396 .post(format!( 397 "{}/xrpc/com.atproto.repo.putRecord", 398 base_url().await 399 )) 400 .bearer_auth(&bob_jwt) 401 .json(&post_payload) 402 .send() 403 .await 404 .expect("Failed to send request"); 405 assert!( 406 write_res.status() == StatusCode::FORBIDDEN 407 || write_res.status() == StatusCode::UNAUTHORIZED, 408 "Expected 403/401 for writing to another user's repo, got {}", 409 write_res.status() 410 ); 411 let delete_payload = 412 json!({ "repo": alice_did, "collection": "app.bsky.feed.post", "rkey": post_rkey }); 413 let delete_res = client 414 .post(format!( 415 "{}/xrpc/com.atproto.repo.deleteRecord", 416 base_url().await 417 )) 418 .bearer_auth(&bob_jwt) 419 .json(&delete_payload) 420 .send() 421 .await 422 .expect("Failed to send request"); 423 assert!( 424 delete_res.status() == StatusCode::FORBIDDEN 425 || delete_res.status() == StatusCode::UNAUTHORIZED, 426 "Expected 403/401 for deleting another user's record, got {}", 427 delete_res.status() 428 ); 429 let get_res = client 430 .get(format!( 431 "{}/xrpc/com.atproto.repo.getRecord", 432 base_url().await 433 )) 434 .query(&[ 435 ("repo", alice_did.as_str()), 436 ("collection", "app.bsky.feed.post"), 437 ("rkey", post_rkey), 438 ]) 439 .send() 440 .await 441 .expect("Failed to verify record exists"); 442 assert_eq!( 443 get_res.status(), 444 StatusCode::OK, 445 "Record should still exist" 446 ); 447} 448 449#[tokio::test] 450async fn test_apply_writes_batch() { 451 let client = client(); 452 let (did, jwt) = setup_new_user("apply-writes-batch").await; 453 let now = Utc::now().to_rfc3339(); 454 let writes_payload = json!({ 455 "repo": did, 456 "writes": [ 457 { "$type": "com.atproto.repo.applyWrites#create", "collection": "app.bsky.feed.post", "rkey": "batch-post-1", "value": { "$type": "app.bsky.feed.post", "text": "First batch post", "createdAt": now } }, 458 { "$type": "com.atproto.repo.applyWrites#create", "collection": "app.bsky.feed.post", "rkey": "batch-post-2", "value": { "$type": "app.bsky.feed.post", "text": "Second batch post", "createdAt": now } }, 459 { "$type": "com.atproto.repo.applyWrites#create", "collection": "app.bsky.actor.profile", "rkey": "self", "value": { "$type": "app.bsky.actor.profile", "displayName": "Batch User" } } 460 ] 461 }); 462 let apply_res = client 463 .post(format!( 464 "{}/xrpc/com.atproto.repo.applyWrites", 465 base_url().await 466 )) 467 .bearer_auth(&jwt) 468 .json(&writes_payload) 469 .send() 470 .await 471 .expect("Failed to apply writes"); 472 assert_eq!(apply_res.status(), StatusCode::OK); 473 let get_post1 = client 474 .get(format!( 475 "{}/xrpc/com.atproto.repo.getRecord", 476 base_url().await 477 )) 478 .query(&[ 479 ("repo", did.as_str()), 480 ("collection", "app.bsky.feed.post"), 481 ("rkey", "batch-post-1"), 482 ]) 483 .send() 484 .await 485 .expect("Failed to get post 1"); 486 assert_eq!(get_post1.status(), StatusCode::OK); 487 let post1_body: Value = get_post1.json().await.unwrap(); 488 assert_eq!(post1_body["value"]["text"], "First batch post"); 489 let get_post2 = client 490 .get(format!( 491 "{}/xrpc/com.atproto.repo.getRecord", 492 base_url().await 493 )) 494 .query(&[ 495 ("repo", did.as_str()), 496 ("collection", "app.bsky.feed.post"), 497 ("rkey", "batch-post-2"), 498 ]) 499 .send() 500 .await 501 .expect("Failed to get post 2"); 502 assert_eq!(get_post2.status(), StatusCode::OK); 503 let get_profile = client 504 .get(format!( 505 "{}/xrpc/com.atproto.repo.getRecord", 506 base_url().await 507 )) 508 .query(&[ 509 ("repo", did.as_str()), 510 ("collection", "app.bsky.actor.profile"), 511 ("rkey", "self"), 512 ]) 513 .send() 514 .await 515 .expect("Failed to get profile"); 516 let profile_body: Value = get_profile.json().await.unwrap(); 517 assert_eq!(profile_body["value"]["displayName"], "Batch User"); 518 let update_writes = json!({ 519 "repo": did, 520 "writes": [ 521 { "$type": "com.atproto.repo.applyWrites#update", "collection": "app.bsky.actor.profile", "rkey": "self", "value": { "$type": "app.bsky.actor.profile", "displayName": "Updated Batch User" } }, 522 { "$type": "com.atproto.repo.applyWrites#delete", "collection": "app.bsky.feed.post", "rkey": "batch-post-1" } 523 ] 524 }); 525 let update_res = client 526 .post(format!( 527 "{}/xrpc/com.atproto.repo.applyWrites", 528 base_url().await 529 )) 530 .bearer_auth(&jwt) 531 .json(&update_writes) 532 .send() 533 .await 534 .expect("Failed to apply update writes"); 535 assert_eq!(update_res.status(), StatusCode::OK); 536 let get_updated_profile = client 537 .get(format!( 538 "{}/xrpc/com.atproto.repo.getRecord", 539 base_url().await 540 )) 541 .query(&[ 542 ("repo", did.as_str()), 543 ("collection", "app.bsky.actor.profile"), 544 ("rkey", "self"), 545 ]) 546 .send() 547 .await 548 .expect("Failed to get updated profile"); 549 let updated_profile: Value = get_updated_profile.json().await.unwrap(); 550 assert_eq!( 551 updated_profile["value"]["displayName"], 552 "Updated Batch User" 553 ); 554 let get_deleted_post = client 555 .get(format!( 556 "{}/xrpc/com.atproto.repo.getRecord", 557 base_url().await 558 )) 559 .query(&[ 560 ("repo", did.as_str()), 561 ("collection", "app.bsky.feed.post"), 562 ("rkey", "batch-post-1"), 563 ]) 564 .send() 565 .await 566 .expect("Failed to check deleted post"); 567 assert_eq!( 568 get_deleted_post.status(), 569 StatusCode::NOT_FOUND, 570 "Batch-deleted post should be gone" 571 ); 572} 573 574async fn create_post_with_rkey( 575 client: &reqwest::Client, 576 did: &str, 577 jwt: &str, 578 rkey: &str, 579 text: &str, 580) -> (String, String) { 581 let payload = json!({ 582 "repo": did, "collection": "app.bsky.feed.post", "rkey": rkey, 583 "record": { "$type": "app.bsky.feed.post", "text": text, "createdAt": Utc::now().to_rfc3339() } 584 }); 585 let res = client 586 .post(format!( 587 "{}/xrpc/com.atproto.repo.putRecord", 588 base_url().await 589 )) 590 .bearer_auth(jwt) 591 .json(&payload) 592 .send() 593 .await 594 .expect("Failed to create record"); 595 assert_eq!(res.status(), StatusCode::OK); 596 let body: Value = res.json().await.unwrap(); 597 ( 598 body["uri"].as_str().unwrap().to_string(), 599 body["cid"].as_str().unwrap().to_string(), 600 ) 601} 602 603#[tokio::test] 604async fn test_list_records_comprehensive() { 605 let client = client(); 606 let (did, jwt) = setup_new_user("list-records-test").await; 607 for i in 0..5 { 608 create_post_with_rkey( 609 &client, 610 &did, 611 &jwt, 612 &format!("post{:02}", i), 613 &format!("Post {}", i), 614 ) 615 .await; 616 tokio::time::sleep(Duration::from_millis(50)).await; 617 } 618 let res = client 619 .get(format!( 620 "{}/xrpc/com.atproto.repo.listRecords", 621 base_url().await 622 )) 623 .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post")]) 624 .send() 625 .await 626 .expect("Failed to list records"); 627 assert_eq!(res.status(), StatusCode::OK); 628 let body: Value = res.json().await.unwrap(); 629 let records = body["records"].as_array().unwrap(); 630 assert_eq!(records.len(), 5); 631 let rkeys: Vec<&str> = records 632 .iter() 633 .map(|r| r["uri"].as_str().unwrap().split('/').next_back().unwrap()) 634 .collect(); 635 assert_eq!( 636 rkeys, 637 vec!["post04", "post03", "post02", "post01", "post00"], 638 "Default order should be DESC" 639 ); 640 for record in records { 641 assert!(record["uri"].is_string()); 642 assert!(record["cid"].is_string()); 643 assert!(record["cid"].as_str().unwrap().starts_with("bafy")); 644 assert!(record["value"].is_object()); 645 } 646 let rev_res = client 647 .get(format!( 648 "{}/xrpc/com.atproto.repo.listRecords", 649 base_url().await 650 )) 651 .query(&[ 652 ("repo", did.as_str()), 653 ("collection", "app.bsky.feed.post"), 654 ("reverse", "true"), 655 ]) 656 .send() 657 .await 658 .expect("Failed to list records reverse"); 659 let rev_body: Value = rev_res.json().await.unwrap(); 660 let rev_rkeys: Vec<&str> = rev_body["records"] 661 .as_array() 662 .unwrap() 663 .iter() 664 .map(|r| r["uri"].as_str().unwrap().split('/').next_back().unwrap()) 665 .collect(); 666 assert_eq!( 667 rev_rkeys, 668 vec!["post00", "post01", "post02", "post03", "post04"], 669 "reverse=true should give ASC" 670 ); 671 let page1 = client 672 .get(format!( 673 "{}/xrpc/com.atproto.repo.listRecords", 674 base_url().await 675 )) 676 .query(&[ 677 ("repo", did.as_str()), 678 ("collection", "app.bsky.feed.post"), 679 ("limit", "2"), 680 ]) 681 .send() 682 .await 683 .expect("Failed to list page 1"); 684 let page1_body: Value = page1.json().await.unwrap(); 685 let page1_records = page1_body["records"].as_array().unwrap(); 686 assert_eq!(page1_records.len(), 2); 687 let cursor = page1_body["cursor"].as_str().expect("Should have cursor"); 688 let page2 = client 689 .get(format!( 690 "{}/xrpc/com.atproto.repo.listRecords", 691 base_url().await 692 )) 693 .query(&[ 694 ("repo", did.as_str()), 695 ("collection", "app.bsky.feed.post"), 696 ("limit", "2"), 697 ("cursor", cursor), 698 ]) 699 .send() 700 .await 701 .expect("Failed to list page 2"); 702 let page2_body: Value = page2.json().await.unwrap(); 703 let page2_records = page2_body["records"].as_array().unwrap(); 704 assert_eq!(page2_records.len(), 2); 705 let all_uris: Vec<&str> = page1_records 706 .iter() 707 .chain(page2_records.iter()) 708 .map(|r| r["uri"].as_str().unwrap()) 709 .collect(); 710 let unique_uris: std::collections::HashSet<&str> = all_uris.iter().copied().collect(); 711 assert_eq!( 712 all_uris.len(), 713 unique_uris.len(), 714 "Cursor pagination should not repeat records" 715 ); 716 let range_res = client 717 .get(format!( 718 "{}/xrpc/com.atproto.repo.listRecords", 719 base_url().await 720 )) 721 .query(&[ 722 ("repo", did.as_str()), 723 ("collection", "app.bsky.feed.post"), 724 ("rkeyStart", "post01"), 725 ("rkeyEnd", "post03"), 726 ("reverse", "true"), 727 ]) 728 .send() 729 .await 730 .expect("Failed to list range"); 731 let range_body: Value = range_res.json().await.unwrap(); 732 let range_rkeys: Vec<&str> = range_body["records"] 733 .as_array() 734 .unwrap() 735 .iter() 736 .map(|r| r["uri"].as_str().unwrap().split('/').next_back().unwrap()) 737 .collect(); 738 for rkey in &range_rkeys { 739 assert!( 740 *rkey >= "post01" && *rkey <= "post03", 741 "Range should be inclusive" 742 ); 743 } 744 let limit_res = client 745 .get(format!( 746 "{}/xrpc/com.atproto.repo.listRecords", 747 base_url().await 748 )) 749 .query(&[ 750 ("repo", did.as_str()), 751 ("collection", "app.bsky.feed.post"), 752 ("limit", "1000"), 753 ]) 754 .send() 755 .await 756 .expect("Failed with high limit"); 757 let limit_body: Value = limit_res.json().await.unwrap(); 758 assert!( 759 limit_body["records"].as_array().unwrap().len() <= 100, 760 "Limit should be clamped to max 100" 761 ); 762 let not_found_res = client 763 .get(format!( 764 "{}/xrpc/com.atproto.repo.listRecords", 765 base_url().await 766 )) 767 .query(&[ 768 ("repo", "did:plc:nonexistent12345"), 769 ("collection", "app.bsky.feed.post"), 770 ]) 771 .send() 772 .await 773 .expect("Failed with nonexistent repo"); 774 assert_eq!(not_found_res.status(), StatusCode::BAD_REQUEST); 775}