this repo has no description
1mod common; 2mod helpers; 3 4use common::*; 5use helpers::*; 6 7use chrono::Utc; 8use reqwest::{StatusCode, header}; 9use serde_json::{Value, json}; 10use std::time::Duration; 11 12#[tokio::test] 13async fn test_post_crud_lifecycle() { 14 let client = client(); 15 let (did, jwt) = setup_new_user("lifecycle-crud").await; 16 let collection = "app.bsky.feed.post"; 17 18 let rkey = format!("e2e_lifecycle_{}", Utc::now().timestamp_millis()); 19 let now = Utc::now().to_rfc3339(); 20 21 let original_text = "Hello from the lifecycle test!"; 22 let create_payload = json!({ 23 "repo": did, 24 "collection": collection, 25 "rkey": rkey, 26 "record": { 27 "$type": collection, 28 "text": original_text, 29 "createdAt": now 30 } 31 }); 32 33 let create_res = client 34 .post(format!( 35 "{}/xrpc/com.atproto.repo.putRecord", 36 base_url().await 37 )) 38 .bearer_auth(&jwt) 39 .json(&create_payload) 40 .send() 41 .await 42 .expect("Failed to send create request"); 43 44 if create_res.status() != reqwest::StatusCode::OK { 45 let status = create_res.status(); 46 let body = create_res 47 .text() 48 .await 49 .unwrap_or_else(|_| "Could not get body".to_string()); 50 panic!( 51 "Failed to create record. Status: {}, Body: {}", 52 status, body 53 ); 54 } 55 56 let create_body: Value = create_res 57 .json() 58 .await 59 .expect("create response was not JSON"); 60 let uri = create_body["uri"].as_str().unwrap(); 61 62 let params = [ 63 ("repo", did.as_str()), 64 ("collection", collection), 65 ("rkey", &rkey), 66 ]; 67 let get_res = client 68 .get(format!( 69 "{}/xrpc/com.atproto.repo.getRecord", 70 base_url().await 71 )) 72 .query(&params) 73 .send() 74 .await 75 .expect("Failed to send get request"); 76 77 assert_eq!( 78 get_res.status(), 79 reqwest::StatusCode::OK, 80 "Failed to get record after create" 81 ); 82 let get_body: Value = get_res.json().await.expect("get response was not JSON"); 83 assert_eq!(get_body["uri"], uri); 84 assert_eq!(get_body["value"]["text"], original_text); 85 86 let updated_text = "This post has been updated."; 87 let update_payload = json!({ 88 "repo": did, 89 "collection": collection, 90 "rkey": rkey, 91 "record": { 92 "$type": collection, 93 "text": updated_text, 94 "createdAt": now 95 } 96 }); 97 98 let update_res = client 99 .post(format!( 100 "{}/xrpc/com.atproto.repo.putRecord", 101 base_url().await 102 )) 103 .bearer_auth(&jwt) 104 .json(&update_payload) 105 .send() 106 .await 107 .expect("Failed to send update request"); 108 109 assert_eq!( 110 update_res.status(), 111 reqwest::StatusCode::OK, 112 "Failed to update record" 113 ); 114 115 let get_updated_res = client 116 .get(format!( 117 "{}/xrpc/com.atproto.repo.getRecord", 118 base_url().await 119 )) 120 .query(&params) 121 .send() 122 .await 123 .expect("Failed to send get-after-update request"); 124 125 assert_eq!( 126 get_updated_res.status(), 127 reqwest::StatusCode::OK, 128 "Failed to get record after update" 129 ); 130 let get_updated_body: Value = get_updated_res 131 .json() 132 .await 133 .expect("get-updated response was not JSON"); 134 assert_eq!( 135 get_updated_body["value"]["text"], updated_text, 136 "Text was not updated" 137 ); 138 139 let delete_payload = json!({ 140 "repo": did, 141 "collection": collection, 142 "rkey": rkey 143 }); 144 145 let delete_res = client 146 .post(format!( 147 "{}/xrpc/com.atproto.repo.deleteRecord", 148 base_url().await 149 )) 150 .bearer_auth(&jwt) 151 .json(&delete_payload) 152 .send() 153 .await 154 .expect("Failed to send delete request"); 155 156 assert_eq!( 157 delete_res.status(), 158 reqwest::StatusCode::OK, 159 "Failed to delete record" 160 ); 161 162 let get_deleted_res = client 163 .get(format!( 164 "{}/xrpc/com.atproto.repo.getRecord", 165 base_url().await 166 )) 167 .query(&params) 168 .send() 169 .await 170 .expect("Failed to send get-after-delete request"); 171 172 assert_eq!( 173 get_deleted_res.status(), 174 reqwest::StatusCode::NOT_FOUND, 175 "Record was found, but it should be deleted" 176 ); 177} 178 179#[tokio::test] 180async fn test_record_update_conflict_lifecycle() { 181 let client = client(); 182 let (user_did, user_jwt) = setup_new_user("user-conflict").await; 183 184 let profile_payload = json!({ 185 "repo": user_did, 186 "collection": "app.bsky.actor.profile", 187 "rkey": "self", 188 "record": { 189 "$type": "app.bsky.actor.profile", 190 "displayName": "Original Name" 191 } 192 }); 193 let create_res = client 194 .post(format!( 195 "{}/xrpc/com.atproto.repo.putRecord", 196 base_url().await 197 )) 198 .bearer_auth(&user_jwt) 199 .json(&profile_payload) 200 .send() 201 .await 202 .expect("create profile failed"); 203 204 if create_res.status() != reqwest::StatusCode::OK { 205 return; 206 } 207 208 let get_res = client 209 .get(format!( 210 "{}/xrpc/com.atproto.repo.getRecord", 211 base_url().await 212 )) 213 .query(&[ 214 ("repo", &user_did), 215 ("collection", &"app.bsky.actor.profile".to_string()), 216 ("rkey", &"self".to_string()), 217 ]) 218 .send() 219 .await 220 .expect("getRecord failed"); 221 let get_body: Value = get_res.json().await.expect("getRecord not json"); 222 let cid_v1 = get_body["cid"] 223 .as_str() 224 .expect("Profile v1 had no CID") 225 .to_string(); 226 227 let update_payload_v2 = json!({ 228 "repo": user_did, 229 "collection": "app.bsky.actor.profile", 230 "rkey": "self", 231 "record": { 232 "$type": "app.bsky.actor.profile", 233 "displayName": "Updated Name (v2)" 234 }, 235 "swapRecord": cid_v1 236 }); 237 let update_res_v2 = client 238 .post(format!( 239 "{}/xrpc/com.atproto.repo.putRecord", 240 base_url().await 241 )) 242 .bearer_auth(&user_jwt) 243 .json(&update_payload_v2) 244 .send() 245 .await 246 .expect("putRecord v2 failed"); 247 assert_eq!( 248 update_res_v2.status(), 249 reqwest::StatusCode::OK, 250 "v2 update failed" 251 ); 252 let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json"); 253 let cid_v2 = update_body_v2["cid"] 254 .as_str() 255 .expect("v2 response had no CID") 256 .to_string(); 257 258 let update_payload_v3_stale = json!({ 259 "repo": user_did, 260 "collection": "app.bsky.actor.profile", 261 "rkey": "self", 262 "record": { 263 "$type": "app.bsky.actor.profile", 264 "displayName": "Stale Update (v3)" 265 }, 266 "swapRecord": cid_v1 267 }); 268 let update_res_v3_stale = client 269 .post(format!( 270 "{}/xrpc/com.atproto.repo.putRecord", 271 base_url().await 272 )) 273 .bearer_auth(&user_jwt) 274 .json(&update_payload_v3_stale) 275 .send() 276 .await 277 .expect("putRecord v3 (stale) failed"); 278 279 assert_eq!( 280 update_res_v3_stale.status(), 281 reqwest::StatusCode::CONFLICT, 282 "Stale update did not cause a 409 Conflict" 283 ); 284 285 let update_payload_v3_good = json!({ 286 "repo": user_did, 287 "collection": "app.bsky.actor.profile", 288 "rkey": "self", 289 "record": { 290 "$type": "app.bsky.actor.profile", 291 "displayName": "Good Update (v3)" 292 }, 293 "swapRecord": cid_v2 294 }); 295 let update_res_v3_good = client 296 .post(format!( 297 "{}/xrpc/com.atproto.repo.putRecord", 298 base_url().await 299 )) 300 .bearer_auth(&user_jwt) 301 .json(&update_payload_v3_good) 302 .send() 303 .await 304 .expect("putRecord v3 (good) failed"); 305 306 assert_eq!( 307 update_res_v3_good.status(), 308 reqwest::StatusCode::OK, 309 "v3 (good) update failed" 310 ); 311} 312 313#[tokio::test] 314async fn test_profile_lifecycle() { 315 let client = client(); 316 let (did, jwt) = setup_new_user("profile-lifecycle").await; 317 318 let profile_payload = json!({ 319 "repo": did, 320 "collection": "app.bsky.actor.profile", 321 "rkey": "self", 322 "record": { 323 "$type": "app.bsky.actor.profile", 324 "displayName": "Test User", 325 "description": "A test profile for lifecycle testing" 326 } 327 }); 328 329 let create_res = client 330 .post(format!( 331 "{}/xrpc/com.atproto.repo.putRecord", 332 base_url().await 333 )) 334 .bearer_auth(&jwt) 335 .json(&profile_payload) 336 .send() 337 .await 338 .expect("Failed to create profile"); 339 340 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile"); 341 let create_body: Value = create_res.json().await.unwrap(); 342 let initial_cid = create_body["cid"].as_str().unwrap().to_string(); 343 344 let get_res = client 345 .get(format!( 346 "{}/xrpc/com.atproto.repo.getRecord", 347 base_url().await 348 )) 349 .query(&[ 350 ("repo", did.as_str()), 351 ("collection", "app.bsky.actor.profile"), 352 ("rkey", "self"), 353 ]) 354 .send() 355 .await 356 .expect("Failed to get profile"); 357 358 assert_eq!(get_res.status(), StatusCode::OK); 359 let get_body: Value = get_res.json().await.unwrap(); 360 assert_eq!(get_body["value"]["displayName"], "Test User"); 361 assert_eq!(get_body["value"]["description"], "A test profile for lifecycle testing"); 362 363 let update_payload = json!({ 364 "repo": did, 365 "collection": "app.bsky.actor.profile", 366 "rkey": "self", 367 "record": { 368 "$type": "app.bsky.actor.profile", 369 "displayName": "Updated User", 370 "description": "Profile has been updated" 371 }, 372 "swapRecord": initial_cid 373 }); 374 375 let update_res = client 376 .post(format!( 377 "{}/xrpc/com.atproto.repo.putRecord", 378 base_url().await 379 )) 380 .bearer_auth(&jwt) 381 .json(&update_payload) 382 .send() 383 .await 384 .expect("Failed to update profile"); 385 386 assert_eq!(update_res.status(), StatusCode::OK, "Failed to update profile"); 387 388 let get_updated_res = client 389 .get(format!( 390 "{}/xrpc/com.atproto.repo.getRecord", 391 base_url().await 392 )) 393 .query(&[ 394 ("repo", did.as_str()), 395 ("collection", "app.bsky.actor.profile"), 396 ("rkey", "self"), 397 ]) 398 .send() 399 .await 400 .expect("Failed to get updated profile"); 401 402 let updated_body: Value = get_updated_res.json().await.unwrap(); 403 assert_eq!(updated_body["value"]["displayName"], "Updated User"); 404} 405 406#[tokio::test] 407async fn test_reply_thread_lifecycle() { 408 let client = client(); 409 410 let (alice_did, alice_jwt) = setup_new_user("alice-thread").await; 411 let (bob_did, bob_jwt) = setup_new_user("bob-thread").await; 412 413 let (root_uri, root_cid) = create_post(&client, &alice_did, &alice_jwt, "This is the root post").await; 414 415 tokio::time::sleep(Duration::from_millis(100)).await; 416 417 let reply_collection = "app.bsky.feed.post"; 418 let reply_rkey = format!("e2e_reply_{}", Utc::now().timestamp_millis()); 419 let now = Utc::now().to_rfc3339(); 420 421 let reply_payload = json!({ 422 "repo": bob_did, 423 "collection": reply_collection, 424 "rkey": reply_rkey, 425 "record": { 426 "$type": reply_collection, 427 "text": "This is Bob's reply to Alice", 428 "createdAt": now, 429 "reply": { 430 "root": { 431 "uri": root_uri, 432 "cid": root_cid 433 }, 434 "parent": { 435 "uri": root_uri, 436 "cid": root_cid 437 } 438 } 439 } 440 }); 441 442 let reply_res = client 443 .post(format!( 444 "{}/xrpc/com.atproto.repo.putRecord", 445 base_url().await 446 )) 447 .bearer_auth(&bob_jwt) 448 .json(&reply_payload) 449 .send() 450 .await 451 .expect("Failed to create reply"); 452 453 assert_eq!(reply_res.status(), StatusCode::OK, "Failed to create reply"); 454 let reply_body: Value = reply_res.json().await.unwrap(); 455 let reply_uri = reply_body["uri"].as_str().unwrap(); 456 let reply_cid = reply_body["cid"].as_str().unwrap(); 457 458 let get_reply_res = client 459 .get(format!( 460 "{}/xrpc/com.atproto.repo.getRecord", 461 base_url().await 462 )) 463 .query(&[ 464 ("repo", bob_did.as_str()), 465 ("collection", reply_collection), 466 ("rkey", reply_rkey.as_str()), 467 ]) 468 .send() 469 .await 470 .expect("Failed to get reply"); 471 472 assert_eq!(get_reply_res.status(), StatusCode::OK); 473 let reply_record: Value = get_reply_res.json().await.unwrap(); 474 assert_eq!(reply_record["value"]["reply"]["root"]["uri"], root_uri); 475 assert_eq!(reply_record["value"]["reply"]["parent"]["uri"], root_uri); 476 477 tokio::time::sleep(Duration::from_millis(100)).await; 478 479 let nested_reply_rkey = format!("e2e_nested_reply_{}", Utc::now().timestamp_millis()); 480 let nested_payload = json!({ 481 "repo": alice_did, 482 "collection": reply_collection, 483 "rkey": nested_reply_rkey, 484 "record": { 485 "$type": reply_collection, 486 "text": "Alice replies to Bob's reply", 487 "createdAt": Utc::now().to_rfc3339(), 488 "reply": { 489 "root": { 490 "uri": root_uri, 491 "cid": root_cid 492 }, 493 "parent": { 494 "uri": reply_uri, 495 "cid": reply_cid 496 } 497 } 498 } 499 }); 500 501 let nested_res = client 502 .post(format!( 503 "{}/xrpc/com.atproto.repo.putRecord", 504 base_url().await 505 )) 506 .bearer_auth(&alice_jwt) 507 .json(&nested_payload) 508 .send() 509 .await 510 .expect("Failed to create nested reply"); 511 512 assert_eq!(nested_res.status(), StatusCode::OK, "Failed to create nested reply"); 513} 514 515#[tokio::test] 516async fn test_blob_in_record_lifecycle() { 517 let client = client(); 518 let (did, jwt) = setup_new_user("blob-record").await; 519 520 let blob_data = b"This is test blob data for a profile avatar"; 521 let upload_res = client 522 .post(format!( 523 "{}/xrpc/com.atproto.repo.uploadBlob", 524 base_url().await 525 )) 526 .header(header::CONTENT_TYPE, "text/plain") 527 .bearer_auth(&jwt) 528 .body(blob_data.to_vec()) 529 .send() 530 .await 531 .expect("Failed to upload blob"); 532 533 assert_eq!(upload_res.status(), StatusCode::OK); 534 let upload_body: Value = upload_res.json().await.unwrap(); 535 let blob_ref = upload_body["blob"].clone(); 536 537 let profile_payload = json!({ 538 "repo": did, 539 "collection": "app.bsky.actor.profile", 540 "rkey": "self", 541 "record": { 542 "$type": "app.bsky.actor.profile", 543 "displayName": "User With Avatar", 544 "avatar": blob_ref 545 } 546 }); 547 548 let create_res = client 549 .post(format!( 550 "{}/xrpc/com.atproto.repo.putRecord", 551 base_url().await 552 )) 553 .bearer_auth(&jwt) 554 .json(&profile_payload) 555 .send() 556 .await 557 .expect("Failed to create profile with blob"); 558 559 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile with blob"); 560 561 let get_res = client 562 .get(format!( 563 "{}/xrpc/com.atproto.repo.getRecord", 564 base_url().await 565 )) 566 .query(&[ 567 ("repo", did.as_str()), 568 ("collection", "app.bsky.actor.profile"), 569 ("rkey", "self"), 570 ]) 571 .send() 572 .await 573 .expect("Failed to get profile"); 574 575 assert_eq!(get_res.status(), StatusCode::OK); 576 let profile: Value = get_res.json().await.unwrap(); 577 assert!(profile["value"]["avatar"]["ref"]["$link"].is_string()); 578} 579 580#[tokio::test] 581async fn test_authorization_cannot_modify_other_repo() { 582 let client = client(); 583 584 let (alice_did, _alice_jwt) = setup_new_user("alice-auth").await; 585 let (_bob_did, bob_jwt) = setup_new_user("bob-auth").await; 586 587 let post_payload = json!({ 588 "repo": alice_did, 589 "collection": "app.bsky.feed.post", 590 "rkey": "unauthorized-post", 591 "record": { 592 "$type": "app.bsky.feed.post", 593 "text": "Bob trying to post as Alice", 594 "createdAt": Utc::now().to_rfc3339() 595 } 596 }); 597 598 let res = client 599 .post(format!( 600 "{}/xrpc/com.atproto.repo.putRecord", 601 base_url().await 602 )) 603 .bearer_auth(&bob_jwt) 604 .json(&post_payload) 605 .send() 606 .await 607 .expect("Failed to send request"); 608 609 assert!( 610 res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED, 611 "Expected 403 or 401 when writing to another user's repo, got {}", 612 res.status() 613 ); 614} 615 616#[tokio::test] 617async fn test_authorization_cannot_delete_other_record() { 618 let client = client(); 619 620 let (alice_did, alice_jwt) = setup_new_user("alice-del-auth").await; 621 let (_bob_did, bob_jwt) = setup_new_user("bob-del-auth").await; 622 623 let (post_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's post").await; 624 let post_rkey = post_uri.split('/').last().unwrap(); 625 626 let delete_payload = json!({ 627 "repo": alice_did, 628 "collection": "app.bsky.feed.post", 629 "rkey": post_rkey 630 }); 631 632 let res = client 633 .post(format!( 634 "{}/xrpc/com.atproto.repo.deleteRecord", 635 base_url().await 636 )) 637 .bearer_auth(&bob_jwt) 638 .json(&delete_payload) 639 .send() 640 .await 641 .expect("Failed to send request"); 642 643 assert!( 644 res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED, 645 "Expected 403 or 401 when deleting another user's record, got {}", 646 res.status() 647 ); 648 649 let get_res = client 650 .get(format!( 651 "{}/xrpc/com.atproto.repo.getRecord", 652 base_url().await 653 )) 654 .query(&[ 655 ("repo", alice_did.as_str()), 656 ("collection", "app.bsky.feed.post"), 657 ("rkey", post_rkey), 658 ]) 659 .send() 660 .await 661 .expect("Failed to verify record exists"); 662 663 assert_eq!(get_res.status(), StatusCode::OK, "Record should still exist"); 664} 665 666#[tokio::test] 667async fn test_apply_writes_batch_lifecycle() { 668 let client = client(); 669 let (did, jwt) = setup_new_user("apply-writes-batch").await; 670 671 let now = Utc::now().to_rfc3339(); 672 let writes_payload = json!({ 673 "repo": did, 674 "writes": [ 675 { 676 "$type": "com.atproto.repo.applyWrites#create", 677 "collection": "app.bsky.feed.post", 678 "rkey": "batch-post-1", 679 "value": { 680 "$type": "app.bsky.feed.post", 681 "text": "First batch post", 682 "createdAt": now 683 } 684 }, 685 { 686 "$type": "com.atproto.repo.applyWrites#create", 687 "collection": "app.bsky.feed.post", 688 "rkey": "batch-post-2", 689 "value": { 690 "$type": "app.bsky.feed.post", 691 "text": "Second batch post", 692 "createdAt": now 693 } 694 }, 695 { 696 "$type": "com.atproto.repo.applyWrites#create", 697 "collection": "app.bsky.actor.profile", 698 "rkey": "self", 699 "value": { 700 "$type": "app.bsky.actor.profile", 701 "displayName": "Batch User" 702 } 703 } 704 ] 705 }); 706 707 let apply_res = client 708 .post(format!( 709 "{}/xrpc/com.atproto.repo.applyWrites", 710 base_url().await 711 )) 712 .bearer_auth(&jwt) 713 .json(&writes_payload) 714 .send() 715 .await 716 .expect("Failed to apply writes"); 717 718 assert_eq!(apply_res.status(), StatusCode::OK); 719 720 let get_post1 = client 721 .get(format!( 722 "{}/xrpc/com.atproto.repo.getRecord", 723 base_url().await 724 )) 725 .query(&[ 726 ("repo", did.as_str()), 727 ("collection", "app.bsky.feed.post"), 728 ("rkey", "batch-post-1"), 729 ]) 730 .send() 731 .await 732 .expect("Failed to get post 1"); 733 assert_eq!(get_post1.status(), StatusCode::OK); 734 let post1_body: Value = get_post1.json().await.unwrap(); 735 assert_eq!(post1_body["value"]["text"], "First batch post"); 736 737 let get_post2 = client 738 .get(format!( 739 "{}/xrpc/com.atproto.repo.getRecord", 740 base_url().await 741 )) 742 .query(&[ 743 ("repo", did.as_str()), 744 ("collection", "app.bsky.feed.post"), 745 ("rkey", "batch-post-2"), 746 ]) 747 .send() 748 .await 749 .expect("Failed to get post 2"); 750 assert_eq!(get_post2.status(), StatusCode::OK); 751 752 let get_profile = client 753 .get(format!( 754 "{}/xrpc/com.atproto.repo.getRecord", 755 base_url().await 756 )) 757 .query(&[ 758 ("repo", did.as_str()), 759 ("collection", "app.bsky.actor.profile"), 760 ("rkey", "self"), 761 ]) 762 .send() 763 .await 764 .expect("Failed to get profile"); 765 assert_eq!(get_profile.status(), StatusCode::OK); 766 let profile_body: Value = get_profile.json().await.unwrap(); 767 assert_eq!(profile_body["value"]["displayName"], "Batch User"); 768 769 let update_writes = json!({ 770 "repo": did, 771 "writes": [ 772 { 773 "$type": "com.atproto.repo.applyWrites#update", 774 "collection": "app.bsky.actor.profile", 775 "rkey": "self", 776 "value": { 777 "$type": "app.bsky.actor.profile", 778 "displayName": "Updated Batch User" 779 } 780 }, 781 { 782 "$type": "com.atproto.repo.applyWrites#delete", 783 "collection": "app.bsky.feed.post", 784 "rkey": "batch-post-1" 785 } 786 ] 787 }); 788 789 let update_res = client 790 .post(format!( 791 "{}/xrpc/com.atproto.repo.applyWrites", 792 base_url().await 793 )) 794 .bearer_auth(&jwt) 795 .json(&update_writes) 796 .send() 797 .await 798 .expect("Failed to apply update writes"); 799 assert_eq!(update_res.status(), StatusCode::OK); 800 801 let get_updated_profile = client 802 .get(format!( 803 "{}/xrpc/com.atproto.repo.getRecord", 804 base_url().await 805 )) 806 .query(&[ 807 ("repo", did.as_str()), 808 ("collection", "app.bsky.actor.profile"), 809 ("rkey", "self"), 810 ]) 811 .send() 812 .await 813 .expect("Failed to get updated profile"); 814 let updated_profile: Value = get_updated_profile.json().await.unwrap(); 815 assert_eq!(updated_profile["value"]["displayName"], "Updated Batch User"); 816 817 let get_deleted_post = client 818 .get(format!( 819 "{}/xrpc/com.atproto.repo.getRecord", 820 base_url().await 821 )) 822 .query(&[ 823 ("repo", did.as_str()), 824 ("collection", "app.bsky.feed.post"), 825 ("rkey", "batch-post-1"), 826 ]) 827 .send() 828 .await 829 .expect("Failed to check deleted post"); 830 assert_eq!( 831 get_deleted_post.status(), 832 StatusCode::NOT_FOUND, 833 "Batch-deleted post should be gone" 834 ); 835} 836 837async fn create_post_with_rkey( 838 client: &reqwest::Client, 839 did: &str, 840 jwt: &str, 841 rkey: &str, 842 text: &str, 843) -> (String, String) { 844 let payload = json!({ 845 "repo": did, 846 "collection": "app.bsky.feed.post", 847 "rkey": rkey, 848 "record": { 849 "$type": "app.bsky.feed.post", 850 "text": text, 851 "createdAt": Utc::now().to_rfc3339() 852 } 853 }); 854 855 let res = client 856 .post(format!( 857 "{}/xrpc/com.atproto.repo.putRecord", 858 base_url().await 859 )) 860 .bearer_auth(jwt) 861 .json(&payload) 862 .send() 863 .await 864 .expect("Failed to create record"); 865 866 assert_eq!(res.status(), StatusCode::OK); 867 let body: Value = res.json().await.unwrap(); 868 ( 869 body["uri"].as_str().unwrap().to_string(), 870 body["cid"].as_str().unwrap().to_string(), 871 ) 872} 873 874#[tokio::test] 875async fn test_list_records_default_order() { 876 let client = client(); 877 let (did, jwt) = setup_new_user("list-default-order").await; 878 879 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First post").await; 880 tokio::time::sleep(Duration::from_millis(50)).await; 881 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second post").await; 882 tokio::time::sleep(Duration::from_millis(50)).await; 883 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third post").await; 884 885 let res = client 886 .get(format!( 887 "{}/xrpc/com.atproto.repo.listRecords", 888 base_url().await 889 )) 890 .query(&[ 891 ("repo", did.as_str()), 892 ("collection", "app.bsky.feed.post"), 893 ]) 894 .send() 895 .await 896 .expect("Failed to list records"); 897 898 assert_eq!(res.status(), StatusCode::OK); 899 let body: Value = res.json().await.unwrap(); 900 let records = body["records"].as_array().unwrap(); 901 902 assert_eq!(records.len(), 3); 903 let rkeys: Vec<&str> = records 904 .iter() 905 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()) 906 .collect(); 907 908 assert_eq!(rkeys, vec!["cccc", "bbbb", "aaaa"], "Default order should be DESC (newest first)"); 909} 910 911#[tokio::test] 912async fn test_list_records_reverse_true() { 913 let client = client(); 914 let (did, jwt) = setup_new_user("list-reverse").await; 915 916 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First post").await; 917 tokio::time::sleep(Duration::from_millis(50)).await; 918 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second post").await; 919 tokio::time::sleep(Duration::from_millis(50)).await; 920 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third post").await; 921 922 let res = client 923 .get(format!( 924 "{}/xrpc/com.atproto.repo.listRecords", 925 base_url().await 926 )) 927 .query(&[ 928 ("repo", did.as_str()), 929 ("collection", "app.bsky.feed.post"), 930 ("reverse", "true"), 931 ]) 932 .send() 933 .await 934 .expect("Failed to list records"); 935 936 assert_eq!(res.status(), StatusCode::OK); 937 let body: Value = res.json().await.unwrap(); 938 let records = body["records"].as_array().unwrap(); 939 940 let rkeys: Vec<&str> = records 941 .iter() 942 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()) 943 .collect(); 944 945 assert_eq!(rkeys, vec!["aaaa", "bbbb", "cccc"], "reverse=true should give ASC order (oldest first)"); 946} 947 948#[tokio::test] 949async fn test_list_records_cursor_pagination() { 950 let client = client(); 951 let (did, jwt) = setup_new_user("list-cursor").await; 952 953 for i in 0..5 { 954 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await; 955 tokio::time::sleep(Duration::from_millis(50)).await; 956 } 957 958 let res = client 959 .get(format!( 960 "{}/xrpc/com.atproto.repo.listRecords", 961 base_url().await 962 )) 963 .query(&[ 964 ("repo", did.as_str()), 965 ("collection", "app.bsky.feed.post"), 966 ("limit", "2"), 967 ]) 968 .send() 969 .await 970 .expect("Failed to list records"); 971 972 assert_eq!(res.status(), StatusCode::OK); 973 let body: Value = res.json().await.unwrap(); 974 let records = body["records"].as_array().unwrap(); 975 assert_eq!(records.len(), 2); 976 977 let cursor = body["cursor"].as_str().expect("Should have cursor with more records"); 978 979 let res2 = client 980 .get(format!( 981 "{}/xrpc/com.atproto.repo.listRecords", 982 base_url().await 983 )) 984 .query(&[ 985 ("repo", did.as_str()), 986 ("collection", "app.bsky.feed.post"), 987 ("limit", "2"), 988 ("cursor", cursor), 989 ]) 990 .send() 991 .await 992 .expect("Failed to list records with cursor"); 993 994 assert_eq!(res2.status(), StatusCode::OK); 995 let body2: Value = res2.json().await.unwrap(); 996 let records2 = body2["records"].as_array().unwrap(); 997 assert_eq!(records2.len(), 2); 998 999 let all_uris: Vec<&str> = records 1000 .iter() 1001 .chain(records2.iter()) 1002 .map(|r| r["uri"].as_str().unwrap()) 1003 .collect(); 1004 let unique_uris: std::collections::HashSet<&str> = all_uris.iter().copied().collect(); 1005 assert_eq!(all_uris.len(), unique_uris.len(), "Cursor pagination should not repeat records"); 1006} 1007 1008#[tokio::test] 1009async fn test_list_records_rkey_start() { 1010 let client = client(); 1011 let (did, jwt) = setup_new_user("list-rkey-start").await; 1012 1013 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First").await; 1014 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second").await; 1015 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third").await; 1016 create_post_with_rkey(&client, &did, &jwt, "dddd", "Fourth").await; 1017 1018 let res = client 1019 .get(format!( 1020 "{}/xrpc/com.atproto.repo.listRecords", 1021 base_url().await 1022 )) 1023 .query(&[ 1024 ("repo", did.as_str()), 1025 ("collection", "app.bsky.feed.post"), 1026 ("rkeyStart", "bbbb"), 1027 ("reverse", "true"), 1028 ]) 1029 .send() 1030 .await 1031 .expect("Failed to list records"); 1032 1033 assert_eq!(res.status(), StatusCode::OK); 1034 let body: Value = res.json().await.unwrap(); 1035 let records = body["records"].as_array().unwrap(); 1036 1037 let rkeys: Vec<&str> = records 1038 .iter() 1039 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()) 1040 .collect(); 1041 1042 for rkey in &rkeys { 1043 assert!(*rkey >= "bbbb", "rkeyStart should filter records >= start"); 1044 } 1045} 1046 1047#[tokio::test] 1048async fn test_list_records_rkey_end() { 1049 let client = client(); 1050 let (did, jwt) = setup_new_user("list-rkey-end").await; 1051 1052 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First").await; 1053 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second").await; 1054 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third").await; 1055 create_post_with_rkey(&client, &did, &jwt, "dddd", "Fourth").await; 1056 1057 let res = client 1058 .get(format!( 1059 "{}/xrpc/com.atproto.repo.listRecords", 1060 base_url().await 1061 )) 1062 .query(&[ 1063 ("repo", did.as_str()), 1064 ("collection", "app.bsky.feed.post"), 1065 ("rkeyEnd", "cccc"), 1066 ("reverse", "true"), 1067 ]) 1068 .send() 1069 .await 1070 .expect("Failed to list records"); 1071 1072 assert_eq!(res.status(), StatusCode::OK); 1073 let body: Value = res.json().await.unwrap(); 1074 let records = body["records"].as_array().unwrap(); 1075 1076 let rkeys: Vec<&str> = records 1077 .iter() 1078 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()) 1079 .collect(); 1080 1081 for rkey in &rkeys { 1082 assert!(*rkey <= "cccc", "rkeyEnd should filter records <= end"); 1083 } 1084} 1085 1086#[tokio::test] 1087async fn test_list_records_rkey_range() { 1088 let client = client(); 1089 let (did, jwt) = setup_new_user("list-rkey-range").await; 1090 1091 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First").await; 1092 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second").await; 1093 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third").await; 1094 create_post_with_rkey(&client, &did, &jwt, "dddd", "Fourth").await; 1095 create_post_with_rkey(&client, &did, &jwt, "eeee", "Fifth").await; 1096 1097 let res = client 1098 .get(format!( 1099 "{}/xrpc/com.atproto.repo.listRecords", 1100 base_url().await 1101 )) 1102 .query(&[ 1103 ("repo", did.as_str()), 1104 ("collection", "app.bsky.feed.post"), 1105 ("rkeyStart", "bbbb"), 1106 ("rkeyEnd", "dddd"), 1107 ("reverse", "true"), 1108 ]) 1109 .send() 1110 .await 1111 .expect("Failed to list records"); 1112 1113 assert_eq!(res.status(), StatusCode::OK); 1114 let body: Value = res.json().await.unwrap(); 1115 let records = body["records"].as_array().unwrap(); 1116 1117 let rkeys: Vec<&str> = records 1118 .iter() 1119 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()) 1120 .collect(); 1121 1122 for rkey in &rkeys { 1123 assert!(*rkey >= "bbbb" && *rkey <= "dddd", "Range should be inclusive, got {}", rkey); 1124 } 1125 assert!(!rkeys.is_empty(), "Should have at least some records in range"); 1126} 1127 1128#[tokio::test] 1129async fn test_list_records_limit_clamping_max() { 1130 let client = client(); 1131 let (did, jwt) = setup_new_user("list-limit-max").await; 1132 1133 for i in 0..5 { 1134 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await; 1135 } 1136 1137 let res = client 1138 .get(format!( 1139 "{}/xrpc/com.atproto.repo.listRecords", 1140 base_url().await 1141 )) 1142 .query(&[ 1143 ("repo", did.as_str()), 1144 ("collection", "app.bsky.feed.post"), 1145 ("limit", "1000"), 1146 ]) 1147 .send() 1148 .await 1149 .expect("Failed to list records"); 1150 1151 assert_eq!(res.status(), StatusCode::OK); 1152 let body: Value = res.json().await.unwrap(); 1153 let records = body["records"].as_array().unwrap(); 1154 assert!(records.len() <= 100, "Limit should be clamped to max 100"); 1155} 1156 1157#[tokio::test] 1158async fn test_list_records_limit_clamping_min() { 1159 let client = client(); 1160 let (did, jwt) = setup_new_user("list-limit-min").await; 1161 1162 create_post_with_rkey(&client, &did, &jwt, "aaaa", "Post").await; 1163 1164 let res = client 1165 .get(format!( 1166 "{}/xrpc/com.atproto.repo.listRecords", 1167 base_url().await 1168 )) 1169 .query(&[ 1170 ("repo", did.as_str()), 1171 ("collection", "app.bsky.feed.post"), 1172 ("limit", "0"), 1173 ]) 1174 .send() 1175 .await 1176 .expect("Failed to list records"); 1177 1178 assert_eq!(res.status(), StatusCode::OK); 1179 let body: Value = res.json().await.unwrap(); 1180 let records = body["records"].as_array().unwrap(); 1181 assert!(records.len() >= 1, "Limit should be clamped to min 1"); 1182} 1183 1184#[tokio::test] 1185async fn test_list_records_empty_collection() { 1186 let client = client(); 1187 let (did, _jwt) = setup_new_user("list-empty").await; 1188 1189 let res = client 1190 .get(format!( 1191 "{}/xrpc/com.atproto.repo.listRecords", 1192 base_url().await 1193 )) 1194 .query(&[ 1195 ("repo", did.as_str()), 1196 ("collection", "app.bsky.feed.post"), 1197 ]) 1198 .send() 1199 .await 1200 .expect("Failed to list records"); 1201 1202 assert_eq!(res.status(), StatusCode::OK); 1203 let body: Value = res.json().await.unwrap(); 1204 let records = body["records"].as_array().unwrap(); 1205 assert!(records.is_empty(), "Empty collection should return empty array"); 1206 assert!(body["cursor"].is_null(), "Empty collection should have no cursor"); 1207} 1208 1209#[tokio::test] 1210async fn test_list_records_exact_limit() { 1211 let client = client(); 1212 let (did, jwt) = setup_new_user("list-exact-limit").await; 1213 1214 for i in 0..10 { 1215 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await; 1216 } 1217 1218 let res = client 1219 .get(format!( 1220 "{}/xrpc/com.atproto.repo.listRecords", 1221 base_url().await 1222 )) 1223 .query(&[ 1224 ("repo", did.as_str()), 1225 ("collection", "app.bsky.feed.post"), 1226 ("limit", "5"), 1227 ]) 1228 .send() 1229 .await 1230 .expect("Failed to list records"); 1231 1232 assert_eq!(res.status(), StatusCode::OK); 1233 let body: Value = res.json().await.unwrap(); 1234 let records = body["records"].as_array().unwrap(); 1235 assert_eq!(records.len(), 5, "Should return exactly 5 records when limit=5"); 1236} 1237 1238#[tokio::test] 1239async fn test_list_records_cursor_exhaustion() { 1240 let client = client(); 1241 let (did, jwt) = setup_new_user("list-cursor-exhaust").await; 1242 1243 for i in 0..3 { 1244 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await; 1245 } 1246 1247 let res = client 1248 .get(format!( 1249 "{}/xrpc/com.atproto.repo.listRecords", 1250 base_url().await 1251 )) 1252 .query(&[ 1253 ("repo", did.as_str()), 1254 ("collection", "app.bsky.feed.post"), 1255 ("limit", "10"), 1256 ]) 1257 .send() 1258 .await 1259 .expect("Failed to list records"); 1260 1261 assert_eq!(res.status(), StatusCode::OK); 1262 let body: Value = res.json().await.unwrap(); 1263 let records = body["records"].as_array().unwrap(); 1264 assert_eq!(records.len(), 3); 1265} 1266 1267#[tokio::test] 1268async fn test_list_records_repo_not_found() { 1269 let client = client(); 1270 1271 let res = client 1272 .get(format!( 1273 "{}/xrpc/com.atproto.repo.listRecords", 1274 base_url().await 1275 )) 1276 .query(&[ 1277 ("repo", "did:plc:nonexistent12345"), 1278 ("collection", "app.bsky.feed.post"), 1279 ]) 1280 .send() 1281 .await 1282 .expect("Failed to list records"); 1283 1284 assert_eq!(res.status(), StatusCode::NOT_FOUND); 1285} 1286 1287#[tokio::test] 1288async fn test_list_records_includes_cid() { 1289 let client = client(); 1290 let (did, jwt) = setup_new_user("list-includes-cid").await; 1291 1292 create_post_with_rkey(&client, &did, &jwt, "test", "Test post").await; 1293 1294 let res = client 1295 .get(format!( 1296 "{}/xrpc/com.atproto.repo.listRecords", 1297 base_url().await 1298 )) 1299 .query(&[ 1300 ("repo", did.as_str()), 1301 ("collection", "app.bsky.feed.post"), 1302 ]) 1303 .send() 1304 .await 1305 .expect("Failed to list records"); 1306 1307 assert_eq!(res.status(), StatusCode::OK); 1308 let body: Value = res.json().await.unwrap(); 1309 let records = body["records"].as_array().unwrap(); 1310 1311 for record in records { 1312 assert!(record["uri"].is_string(), "Record should have uri"); 1313 assert!(record["cid"].is_string(), "Record should have cid"); 1314 assert!(record["value"].is_object(), "Record should have value"); 1315 let cid = record["cid"].as_str().unwrap(); 1316 assert!(cid.starts_with("bafy"), "CID should be valid"); 1317 } 1318} 1319 1320#[tokio::test] 1321async fn test_list_records_cursor_with_reverse() { 1322 let client = client(); 1323 let (did, jwt) = setup_new_user("list-cursor-reverse").await; 1324 1325 for i in 0..5 { 1326 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await; 1327 } 1328 1329 let res = client 1330 .get(format!( 1331 "{}/xrpc/com.atproto.repo.listRecords", 1332 base_url().await 1333 )) 1334 .query(&[ 1335 ("repo", did.as_str()), 1336 ("collection", "app.bsky.feed.post"), 1337 ("limit", "2"), 1338 ("reverse", "true"), 1339 ]) 1340 .send() 1341 .await 1342 .expect("Failed to list records"); 1343 1344 assert_eq!(res.status(), StatusCode::OK); 1345 let body: Value = res.json().await.unwrap(); 1346 let records = body["records"].as_array().unwrap(); 1347 let first_rkeys: Vec<&str> = records 1348 .iter() 1349 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()) 1350 .collect(); 1351 1352 assert_eq!(first_rkeys, vec!["post00", "post01"], "First page with reverse should start from oldest"); 1353 1354 if let Some(cursor) = body["cursor"].as_str() { 1355 let res2 = client 1356 .get(format!( 1357 "{}/xrpc/com.atproto.repo.listRecords", 1358 base_url().await 1359 )) 1360 .query(&[ 1361 ("repo", did.as_str()), 1362 ("collection", "app.bsky.feed.post"), 1363 ("limit", "2"), 1364 ("reverse", "true"), 1365 ("cursor", cursor), 1366 ]) 1367 .send() 1368 .await 1369 .expect("Failed to list records with cursor"); 1370 1371 let body2: Value = res2.json().await.unwrap(); 1372 let records2 = body2["records"].as_array().unwrap(); 1373 let second_rkeys: Vec<&str> = records2 1374 .iter() 1375 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()) 1376 .collect(); 1377 1378 assert_eq!(second_rkeys, vec!["post02", "post03"], "Second page should continue in ASC order"); 1379 } 1380}