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