this repo has no description
1mod common; 2mod helpers; 3use common::*; 4use helpers::*; 5use chrono::Utc; 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!(create_res.status(), StatusCode::OK, "Failed to create profile"); 311 let create_body: Value = create_res.json().await.unwrap(); 312 let initial_cid = create_body["cid"].as_str().unwrap().to_string(); 313 let get_res = client 314 .get(format!( 315 "{}/xrpc/com.atproto.repo.getRecord", 316 base_url().await 317 )) 318 .query(&[ 319 ("repo", did.as_str()), 320 ("collection", "app.bsky.actor.profile"), 321 ("rkey", "self"), 322 ]) 323 .send() 324 .await 325 .expect("Failed to get profile"); 326 assert_eq!(get_res.status(), StatusCode::OK); 327 let get_body: Value = get_res.json().await.unwrap(); 328 assert_eq!(get_body["value"]["displayName"], "Test User"); 329 assert_eq!(get_body["value"]["description"], "A test profile for lifecycle testing"); 330 let update_payload = json!({ 331 "repo": did, 332 "collection": "app.bsky.actor.profile", 333 "rkey": "self", 334 "record": { 335 "$type": "app.bsky.actor.profile", 336 "displayName": "Updated User", 337 "description": "Profile has been updated" 338 }, 339 "swapRecord": initial_cid 340 }); 341 let update_res = client 342 .post(format!( 343 "{}/xrpc/com.atproto.repo.putRecord", 344 base_url().await 345 )) 346 .bearer_auth(&jwt) 347 .json(&update_payload) 348 .send() 349 .await 350 .expect("Failed to update profile"); 351 assert_eq!(update_res.status(), StatusCode::OK, "Failed to update profile"); 352 let get_updated_res = client 353 .get(format!( 354 "{}/xrpc/com.atproto.repo.getRecord", 355 base_url().await 356 )) 357 .query(&[ 358 ("repo", did.as_str()), 359 ("collection", "app.bsky.actor.profile"), 360 ("rkey", "self"), 361 ]) 362 .send() 363 .await 364 .expect("Failed to get updated profile"); 365 let updated_body: Value = get_updated_res.json().await.unwrap(); 366 assert_eq!(updated_body["value"]["displayName"], "Updated User"); 367} 368 369#[tokio::test] 370async fn test_reply_thread_lifecycle() { 371 let client = client(); 372 let (alice_did, alice_jwt) = setup_new_user("alice-thread").await; 373 let (bob_did, bob_jwt) = setup_new_user("bob-thread").await; 374 let (root_uri, root_cid) = create_post(&client, &alice_did, &alice_jwt, "This is the root post").await; 375 tokio::time::sleep(Duration::from_millis(100)).await; 376 let reply_collection = "app.bsky.feed.post"; 377 let reply_rkey = format!("e2e_reply_{}", Utc::now().timestamp_millis()); 378 let now = Utc::now().to_rfc3339(); 379 let reply_payload = json!({ 380 "repo": bob_did, 381 "collection": reply_collection, 382 "rkey": reply_rkey, 383 "record": { 384 "$type": reply_collection, 385 "text": "This is Bob's reply to Alice", 386 "createdAt": now, 387 "reply": { 388 "root": { 389 "uri": root_uri, 390 "cid": root_cid 391 }, 392 "parent": { 393 "uri": root_uri, 394 "cid": root_cid 395 } 396 } 397 } 398 }); 399 let reply_res = client 400 .post(format!( 401 "{}/xrpc/com.atproto.repo.putRecord", 402 base_url().await 403 )) 404 .bearer_auth(&bob_jwt) 405 .json(&reply_payload) 406 .send() 407 .await 408 .expect("Failed to create reply"); 409 assert_eq!(reply_res.status(), StatusCode::OK, "Failed to create reply"); 410 let reply_body: Value = reply_res.json().await.unwrap(); 411 let reply_uri = reply_body["uri"].as_str().unwrap(); 412 let reply_cid = reply_body["cid"].as_str().unwrap(); 413 let get_reply_res = client 414 .get(format!( 415 "{}/xrpc/com.atproto.repo.getRecord", 416 base_url().await 417 )) 418 .query(&[ 419 ("repo", bob_did.as_str()), 420 ("collection", reply_collection), 421 ("rkey", reply_rkey.as_str()), 422 ]) 423 .send() 424 .await 425 .expect("Failed to get reply"); 426 assert_eq!(get_reply_res.status(), StatusCode::OK); 427 let reply_record: Value = get_reply_res.json().await.unwrap(); 428 assert_eq!(reply_record["value"]["reply"]["root"]["uri"], root_uri); 429 assert_eq!(reply_record["value"]["reply"]["parent"]["uri"], root_uri); 430 tokio::time::sleep(Duration::from_millis(100)).await; 431 let nested_reply_rkey = format!("e2e_nested_reply_{}", Utc::now().timestamp_millis()); 432 let nested_payload = json!({ 433 "repo": alice_did, 434 "collection": reply_collection, 435 "rkey": nested_reply_rkey, 436 "record": { 437 "$type": reply_collection, 438 "text": "Alice replies to Bob's reply", 439 "createdAt": Utc::now().to_rfc3339(), 440 "reply": { 441 "root": { 442 "uri": root_uri, 443 "cid": root_cid 444 }, 445 "parent": { 446 "uri": reply_uri, 447 "cid": reply_cid 448 } 449 } 450 } 451 }); 452 let nested_res = client 453 .post(format!( 454 "{}/xrpc/com.atproto.repo.putRecord", 455 base_url().await 456 )) 457 .bearer_auth(&alice_jwt) 458 .json(&nested_payload) 459 .send() 460 .await 461 .expect("Failed to create nested reply"); 462 assert_eq!(nested_res.status(), StatusCode::OK, "Failed to create nested reply"); 463} 464 465#[tokio::test] 466async fn test_blob_in_record_lifecycle() { 467 let client = client(); 468 let (did, jwt) = setup_new_user("blob-record").await; 469 let blob_data = b"This is test blob data for a profile avatar"; 470 let upload_res = client 471 .post(format!( 472 "{}/xrpc/com.atproto.repo.uploadBlob", 473 base_url().await 474 )) 475 .header(header::CONTENT_TYPE, "text/plain") 476 .bearer_auth(&jwt) 477 .body(blob_data.to_vec()) 478 .send() 479 .await 480 .expect("Failed to upload blob"); 481 assert_eq!(upload_res.status(), StatusCode::OK); 482 let upload_body: Value = upload_res.json().await.unwrap(); 483 let blob_ref = upload_body["blob"].clone(); 484 let profile_payload = json!({ 485 "repo": did, 486 "collection": "app.bsky.actor.profile", 487 "rkey": "self", 488 "record": { 489 "$type": "app.bsky.actor.profile", 490 "displayName": "User With Avatar", 491 "avatar": blob_ref 492 } 493 }); 494 let create_res = client 495 .post(format!( 496 "{}/xrpc/com.atproto.repo.putRecord", 497 base_url().await 498 )) 499 .bearer_auth(&jwt) 500 .json(&profile_payload) 501 .send() 502 .await 503 .expect("Failed to create profile with blob"); 504 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile with blob"); 505 let get_res = client 506 .get(format!( 507 "{}/xrpc/com.atproto.repo.getRecord", 508 base_url().await 509 )) 510 .query(&[ 511 ("repo", did.as_str()), 512 ("collection", "app.bsky.actor.profile"), 513 ("rkey", "self"), 514 ]) 515 .send() 516 .await 517 .expect("Failed to get profile"); 518 assert_eq!(get_res.status(), StatusCode::OK); 519 let profile: Value = get_res.json().await.unwrap(); 520 assert!(profile["value"]["avatar"]["ref"]["$link"].is_string()); 521} 522 523#[tokio::test] 524async fn test_authorization_cannot_modify_other_repo() { 525 let client = client(); 526 let (alice_did, _alice_jwt) = setup_new_user("alice-auth").await; 527 let (_bob_did, bob_jwt) = setup_new_user("bob-auth").await; 528 let post_payload = json!({ 529 "repo": alice_did, 530 "collection": "app.bsky.feed.post", 531 "rkey": "unauthorized-post", 532 "record": { 533 "$type": "app.bsky.feed.post", 534 "text": "Bob trying to post as Alice", 535 "createdAt": Utc::now().to_rfc3339() 536 } 537 }); 538 let res = client 539 .post(format!( 540 "{}/xrpc/com.atproto.repo.putRecord", 541 base_url().await 542 )) 543 .bearer_auth(&bob_jwt) 544 .json(&post_payload) 545 .send() 546 .await 547 .expect("Failed to send request"); 548 assert!( 549 res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED, 550 "Expected 403 or 401 when writing to another user's repo, got {}", 551 res.status() 552 ); 553} 554 555#[tokio::test] 556async fn test_authorization_cannot_delete_other_record() { 557 let client = client(); 558 let (alice_did, alice_jwt) = setup_new_user("alice-del-auth").await; 559 let (_bob_did, bob_jwt) = setup_new_user("bob-del-auth").await; 560 let (post_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's post").await; 561 let post_rkey = post_uri.split('/').last().unwrap(); 562 let delete_payload = json!({ 563 "repo": alice_did, 564 "collection": "app.bsky.feed.post", 565 "rkey": post_rkey 566 }); 567 let res = client 568 .post(format!( 569 "{}/xrpc/com.atproto.repo.deleteRecord", 570 base_url().await 571 )) 572 .bearer_auth(&bob_jwt) 573 .json(&delete_payload) 574 .send() 575 .await 576 .expect("Failed to send request"); 577 assert!( 578 res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED, 579 "Expected 403 or 401 when deleting another user's record, got {}", 580 res.status() 581 ); 582 let get_res = client 583 .get(format!( 584 "{}/xrpc/com.atproto.repo.getRecord", 585 base_url().await 586 )) 587 .query(&[ 588 ("repo", alice_did.as_str()), 589 ("collection", "app.bsky.feed.post"), 590 ("rkey", post_rkey), 591 ]) 592 .send() 593 .await 594 .expect("Failed to verify record exists"); 595 assert_eq!(get_res.status(), StatusCode::OK, "Record should still exist"); 596} 597 598#[tokio::test] 599async fn test_apply_writes_batch_lifecycle() { 600 let client = client(); 601 let (did, jwt) = setup_new_user("apply-writes-batch").await; 602 let now = Utc::now().to_rfc3339(); 603 let writes_payload = json!({ 604 "repo": did, 605 "writes": [ 606 { 607 "$type": "com.atproto.repo.applyWrites#create", 608 "collection": "app.bsky.feed.post", 609 "rkey": "batch-post-1", 610 "value": { 611 "$type": "app.bsky.feed.post", 612 "text": "First batch post", 613 "createdAt": now 614 } 615 }, 616 { 617 "$type": "com.atproto.repo.applyWrites#create", 618 "collection": "app.bsky.feed.post", 619 "rkey": "batch-post-2", 620 "value": { 621 "$type": "app.bsky.feed.post", 622 "text": "Second batch post", 623 "createdAt": now 624 } 625 }, 626 { 627 "$type": "com.atproto.repo.applyWrites#create", 628 "collection": "app.bsky.actor.profile", 629 "rkey": "self", 630 "value": { 631 "$type": "app.bsky.actor.profile", 632 "displayName": "Batch User" 633 } 634 } 635 ] 636 }); 637 let apply_res = client 638 .post(format!( 639 "{}/xrpc/com.atproto.repo.applyWrites", 640 base_url().await 641 )) 642 .bearer_auth(&jwt) 643 .json(&writes_payload) 644 .send() 645 .await 646 .expect("Failed to apply writes"); 647 assert_eq!(apply_res.status(), StatusCode::OK); 648 let get_post1 = client 649 .get(format!( 650 "{}/xrpc/com.atproto.repo.getRecord", 651 base_url().await 652 )) 653 .query(&[ 654 ("repo", did.as_str()), 655 ("collection", "app.bsky.feed.post"), 656 ("rkey", "batch-post-1"), 657 ]) 658 .send() 659 .await 660 .expect("Failed to get post 1"); 661 assert_eq!(get_post1.status(), StatusCode::OK); 662 let post1_body: Value = get_post1.json().await.unwrap(); 663 assert_eq!(post1_body["value"]["text"], "First batch post"); 664 let get_post2 = client 665 .get(format!( 666 "{}/xrpc/com.atproto.repo.getRecord", 667 base_url().await 668 )) 669 .query(&[ 670 ("repo", did.as_str()), 671 ("collection", "app.bsky.feed.post"), 672 ("rkey", "batch-post-2"), 673 ]) 674 .send() 675 .await 676 .expect("Failed to get post 2"); 677 assert_eq!(get_post2.status(), StatusCode::OK); 678 let get_profile = client 679 .get(format!( 680 "{}/xrpc/com.atproto.repo.getRecord", 681 base_url().await 682 )) 683 .query(&[ 684 ("repo", did.as_str()), 685 ("collection", "app.bsky.actor.profile"), 686 ("rkey", "self"), 687 ]) 688 .send() 689 .await 690 .expect("Failed to get profile"); 691 assert_eq!(get_profile.status(), StatusCode::OK); 692 let profile_body: Value = get_profile.json().await.unwrap(); 693 assert_eq!(profile_body["value"]["displayName"], "Batch User"); 694 let update_writes = json!({ 695 "repo": did, 696 "writes": [ 697 { 698 "$type": "com.atproto.repo.applyWrites#update", 699 "collection": "app.bsky.actor.profile", 700 "rkey": "self", 701 "value": { 702 "$type": "app.bsky.actor.profile", 703 "displayName": "Updated Batch User" 704 } 705 }, 706 { 707 "$type": "com.atproto.repo.applyWrites#delete", 708 "collection": "app.bsky.feed.post", 709 "rkey": "batch-post-1" 710 } 711 ] 712 }); 713 let update_res = client 714 .post(format!( 715 "{}/xrpc/com.atproto.repo.applyWrites", 716 base_url().await 717 )) 718 .bearer_auth(&jwt) 719 .json(&update_writes) 720 .send() 721 .await 722 .expect("Failed to apply update writes"); 723 assert_eq!(update_res.status(), StatusCode::OK); 724 let get_updated_profile = client 725 .get(format!( 726 "{}/xrpc/com.atproto.repo.getRecord", 727 base_url().await 728 )) 729 .query(&[ 730 ("repo", did.as_str()), 731 ("collection", "app.bsky.actor.profile"), 732 ("rkey", "self"), 733 ]) 734 .send() 735 .await 736 .expect("Failed to get updated profile"); 737 let updated_profile: Value = get_updated_profile.json().await.unwrap(); 738 assert_eq!(updated_profile["value"]["displayName"], "Updated Batch User"); 739 let get_deleted_post = client 740 .get(format!( 741 "{}/xrpc/com.atproto.repo.getRecord", 742 base_url().await 743 )) 744 .query(&[ 745 ("repo", did.as_str()), 746 ("collection", "app.bsky.feed.post"), 747 ("rkey", "batch-post-1"), 748 ]) 749 .send() 750 .await 751 .expect("Failed to check deleted post"); 752 assert_eq!( 753 get_deleted_post.status(), 754 StatusCode::NOT_FOUND, 755 "Batch-deleted post should be gone" 756 ); 757} 758 759async fn create_post_with_rkey( 760 client: &reqwest::Client, 761 did: &str, 762 jwt: &str, 763 rkey: &str, 764 text: &str, 765) -> (String, String) { 766 let payload = json!({ 767 "repo": did, 768 "collection": "app.bsky.feed.post", 769 "rkey": rkey, 770 "record": { 771 "$type": "app.bsky.feed.post", 772 "text": text, 773 "createdAt": Utc::now().to_rfc3339() 774 } 775 }); 776 let res = client 777 .post(format!( 778 "{}/xrpc/com.atproto.repo.putRecord", 779 base_url().await 780 )) 781 .bearer_auth(jwt) 782 .json(&payload) 783 .send() 784 .await 785 .expect("Failed to create record"); 786 assert_eq!(res.status(), StatusCode::OK); 787 let body: Value = res.json().await.unwrap(); 788 ( 789 body["uri"].as_str().unwrap().to_string(), 790 body["cid"].as_str().unwrap().to_string(), 791 ) 792} 793 794#[tokio::test] 795async fn test_list_records_default_order() { 796 let client = client(); 797 let (did, jwt) = setup_new_user("list-default-order").await; 798 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First post").await; 799 tokio::time::sleep(Duration::from_millis(50)).await; 800 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second post").await; 801 tokio::time::sleep(Duration::from_millis(50)).await; 802 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third post").await; 803 let res = client 804 .get(format!( 805 "{}/xrpc/com.atproto.repo.listRecords", 806 base_url().await 807 )) 808 .query(&[ 809 ("repo", did.as_str()), 810 ("collection", "app.bsky.feed.post"), 811 ]) 812 .send() 813 .await 814 .expect("Failed to list records"); 815 assert_eq!(res.status(), StatusCode::OK); 816 let body: Value = res.json().await.unwrap(); 817 let records = body["records"].as_array().unwrap(); 818 assert_eq!(records.len(), 3); 819 let rkeys: Vec<&str> = records 820 .iter() 821 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()) 822 .collect(); 823 assert_eq!(rkeys, vec!["cccc", "bbbb", "aaaa"], "Default order should be DESC (newest first)"); 824} 825 826#[tokio::test] 827async fn test_list_records_reverse_true() { 828 let client = client(); 829 let (did, jwt) = setup_new_user("list-reverse").await; 830 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First post").await; 831 tokio::time::sleep(Duration::from_millis(50)).await; 832 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second post").await; 833 tokio::time::sleep(Duration::from_millis(50)).await; 834 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third post").await; 835 let res = client 836 .get(format!( 837 "{}/xrpc/com.atproto.repo.listRecords", 838 base_url().await 839 )) 840 .query(&[ 841 ("repo", did.as_str()), 842 ("collection", "app.bsky.feed.post"), 843 ("reverse", "true"), 844 ]) 845 .send() 846 .await 847 .expect("Failed to list records"); 848 assert_eq!(res.status(), StatusCode::OK); 849 let body: Value = res.json().await.unwrap(); 850 let records = body["records"].as_array().unwrap(); 851 let rkeys: Vec<&str> = records 852 .iter() 853 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()) 854 .collect(); 855 assert_eq!(rkeys, vec!["aaaa", "bbbb", "cccc"], "reverse=true should give ASC order (oldest first)"); 856} 857 858#[tokio::test] 859async fn test_list_records_cursor_pagination() { 860 let client = client(); 861 let (did, jwt) = setup_new_user("list-cursor").await; 862 for i in 0..5 { 863 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await; 864 tokio::time::sleep(Duration::from_millis(50)).await; 865 } 866 let res = client 867 .get(format!( 868 "{}/xrpc/com.atproto.repo.listRecords", 869 base_url().await 870 )) 871 .query(&[ 872 ("repo", did.as_str()), 873 ("collection", "app.bsky.feed.post"), 874 ("limit", "2"), 875 ]) 876 .send() 877 .await 878 .expect("Failed to list records"); 879 assert_eq!(res.status(), StatusCode::OK); 880 let body: Value = res.json().await.unwrap(); 881 let records = body["records"].as_array().unwrap(); 882 assert_eq!(records.len(), 2); 883 let cursor = body["cursor"].as_str().expect("Should have cursor with more records"); 884 let res2 = client 885 .get(format!( 886 "{}/xrpc/com.atproto.repo.listRecords", 887 base_url().await 888 )) 889 .query(&[ 890 ("repo", did.as_str()), 891 ("collection", "app.bsky.feed.post"), 892 ("limit", "2"), 893 ("cursor", cursor), 894 ]) 895 .send() 896 .await 897 .expect("Failed to list records with cursor"); 898 assert_eq!(res2.status(), StatusCode::OK); 899 let body2: Value = res2.json().await.unwrap(); 900 let records2 = body2["records"].as_array().unwrap(); 901 assert_eq!(records2.len(), 2); 902 let all_uris: Vec<&str> = records 903 .iter() 904 .chain(records2.iter()) 905 .map(|r| r["uri"].as_str().unwrap()) 906 .collect(); 907 let unique_uris: std::collections::HashSet<&str> = all_uris.iter().copied().collect(); 908 assert_eq!(all_uris.len(), unique_uris.len(), "Cursor pagination should not repeat records"); 909} 910 911#[tokio::test] 912async fn test_list_records_rkey_start() { 913 let client = client(); 914 let (did, jwt) = setup_new_user("list-rkey-start").await; 915 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First").await; 916 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second").await; 917 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third").await; 918 create_post_with_rkey(&client, &did, &jwt, "dddd", "Fourth").await; 919 let res = client 920 .get(format!( 921 "{}/xrpc/com.atproto.repo.listRecords", 922 base_url().await 923 )) 924 .query(&[ 925 ("repo", did.as_str()), 926 ("collection", "app.bsky.feed.post"), 927 ("rkeyStart", "bbbb"), 928 ("reverse", "true"), 929 ]) 930 .send() 931 .await 932 .expect("Failed to list records"); 933 assert_eq!(res.status(), StatusCode::OK); 934 let body: Value = res.json().await.unwrap(); 935 let records = body["records"].as_array().unwrap(); 936 let rkeys: Vec<&str> = records 937 .iter() 938 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()) 939 .collect(); 940 for rkey in &rkeys { 941 assert!(*rkey >= "bbbb", "rkeyStart should filter records >= start"); 942 } 943} 944 945#[tokio::test] 946async fn test_list_records_rkey_end() { 947 let client = client(); 948 let (did, jwt) = setup_new_user("list-rkey-end").await; 949 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First").await; 950 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second").await; 951 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third").await; 952 create_post_with_rkey(&client, &did, &jwt, "dddd", "Fourth").await; 953 let res = client 954 .get(format!( 955 "{}/xrpc/com.atproto.repo.listRecords", 956 base_url().await 957 )) 958 .query(&[ 959 ("repo", did.as_str()), 960 ("collection", "app.bsky.feed.post"), 961 ("rkeyEnd", "cccc"), 962 ("reverse", "true"), 963 ]) 964 .send() 965 .await 966 .expect("Failed to list records"); 967 assert_eq!(res.status(), StatusCode::OK); 968 let body: Value = res.json().await.unwrap(); 969 let records = body["records"].as_array().unwrap(); 970 let rkeys: Vec<&str> = records 971 .iter() 972 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()) 973 .collect(); 974 for rkey in &rkeys { 975 assert!(*rkey <= "cccc", "rkeyEnd should filter records <= end"); 976 } 977} 978 979#[tokio::test] 980async fn test_list_records_rkey_range() { 981 let client = client(); 982 let (did, jwt) = setup_new_user("list-rkey-range").await; 983 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First").await; 984 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second").await; 985 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third").await; 986 create_post_with_rkey(&client, &did, &jwt, "dddd", "Fourth").await; 987 create_post_with_rkey(&client, &did, &jwt, "eeee", "Fifth").await; 988 let res = client 989 .get(format!( 990 "{}/xrpc/com.atproto.repo.listRecords", 991 base_url().await 992 )) 993 .query(&[ 994 ("repo", did.as_str()), 995 ("collection", "app.bsky.feed.post"), 996 ("rkeyStart", "bbbb"), 997 ("rkeyEnd", "dddd"), 998 ("reverse", "true"), 999 ]) 1000 .send() 1001 .await 1002 .expect("Failed to list records"); 1003 assert_eq!(res.status(), StatusCode::OK); 1004 let body: Value = res.json().await.unwrap(); 1005 let records = body["records"].as_array().unwrap(); 1006 let rkeys: Vec<&str> = records 1007 .iter() 1008 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()) 1009 .collect(); 1010 for rkey in &rkeys { 1011 assert!(*rkey >= "bbbb" && *rkey <= "dddd", "Range should be inclusive, got {}", rkey); 1012 } 1013 assert!(!rkeys.is_empty(), "Should have at least some records in range"); 1014} 1015 1016#[tokio::test] 1017async fn test_list_records_limit_clamping_max() { 1018 let client = client(); 1019 let (did, jwt) = setup_new_user("list-limit-max").await; 1020 for i in 0..5 { 1021 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await; 1022 } 1023 let res = client 1024 .get(format!( 1025 "{}/xrpc/com.atproto.repo.listRecords", 1026 base_url().await 1027 )) 1028 .query(&[ 1029 ("repo", did.as_str()), 1030 ("collection", "app.bsky.feed.post"), 1031 ("limit", "1000"), 1032 ]) 1033 .send() 1034 .await 1035 .expect("Failed to list records"); 1036 assert_eq!(res.status(), StatusCode::OK); 1037 let body: Value = res.json().await.unwrap(); 1038 let records = body["records"].as_array().unwrap(); 1039 assert!(records.len() <= 100, "Limit should be clamped to max 100"); 1040} 1041 1042#[tokio::test] 1043async fn test_list_records_limit_clamping_min() { 1044 let client = client(); 1045 let (did, jwt) = setup_new_user("list-limit-min").await; 1046 create_post_with_rkey(&client, &did, &jwt, "aaaa", "Post").await; 1047 let res = client 1048 .get(format!( 1049 "{}/xrpc/com.atproto.repo.listRecords", 1050 base_url().await 1051 )) 1052 .query(&[ 1053 ("repo", did.as_str()), 1054 ("collection", "app.bsky.feed.post"), 1055 ("limit", "0"), 1056 ]) 1057 .send() 1058 .await 1059 .expect("Failed to list records"); 1060 assert_eq!(res.status(), StatusCode::OK); 1061 let body: Value = res.json().await.unwrap(); 1062 let records = body["records"].as_array().unwrap(); 1063 assert!(records.len() >= 1, "Limit should be clamped to min 1"); 1064} 1065 1066#[tokio::test] 1067async fn test_list_records_empty_collection() { 1068 let client = client(); 1069 let (did, _jwt) = setup_new_user("list-empty").await; 1070 let res = client 1071 .get(format!( 1072 "{}/xrpc/com.atproto.repo.listRecords", 1073 base_url().await 1074 )) 1075 .query(&[ 1076 ("repo", did.as_str()), 1077 ("collection", "app.bsky.feed.post"), 1078 ]) 1079 .send() 1080 .await 1081 .expect("Failed to list records"); 1082 assert_eq!(res.status(), StatusCode::OK); 1083 let body: Value = res.json().await.unwrap(); 1084 let records = body["records"].as_array().unwrap(); 1085 assert!(records.is_empty(), "Empty collection should return empty array"); 1086 assert!(body["cursor"].is_null(), "Empty collection should have no cursor"); 1087} 1088 1089#[tokio::test] 1090async fn test_list_records_exact_limit() { 1091 let client = client(); 1092 let (did, jwt) = setup_new_user("list-exact-limit").await; 1093 for i in 0..10 { 1094 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await; 1095 } 1096 let res = client 1097 .get(format!( 1098 "{}/xrpc/com.atproto.repo.listRecords", 1099 base_url().await 1100 )) 1101 .query(&[ 1102 ("repo", did.as_str()), 1103 ("collection", "app.bsky.feed.post"), 1104 ("limit", "5"), 1105 ]) 1106 .send() 1107 .await 1108 .expect("Failed to list records"); 1109 assert_eq!(res.status(), StatusCode::OK); 1110 let body: Value = res.json().await.unwrap(); 1111 let records = body["records"].as_array().unwrap(); 1112 assert_eq!(records.len(), 5, "Should return exactly 5 records when limit=5"); 1113} 1114 1115#[tokio::test] 1116async fn test_list_records_cursor_exhaustion() { 1117 let client = client(); 1118 let (did, jwt) = setup_new_user("list-cursor-exhaust").await; 1119 for i in 0..3 { 1120 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await; 1121 } 1122 let res = client 1123 .get(format!( 1124 "{}/xrpc/com.atproto.repo.listRecords", 1125 base_url().await 1126 )) 1127 .query(&[ 1128 ("repo", did.as_str()), 1129 ("collection", "app.bsky.feed.post"), 1130 ("limit", "10"), 1131 ]) 1132 .send() 1133 .await 1134 .expect("Failed to list records"); 1135 assert_eq!(res.status(), StatusCode::OK); 1136 let body: Value = res.json().await.unwrap(); 1137 let records = body["records"].as_array().unwrap(); 1138 assert_eq!(records.len(), 3); 1139} 1140 1141#[tokio::test] 1142async fn test_list_records_repo_not_found() { 1143 let client = client(); 1144 let res = client 1145 .get(format!( 1146 "{}/xrpc/com.atproto.repo.listRecords", 1147 base_url().await 1148 )) 1149 .query(&[ 1150 ("repo", "did:plc:nonexistent12345"), 1151 ("collection", "app.bsky.feed.post"), 1152 ]) 1153 .send() 1154 .await 1155 .expect("Failed to list records"); 1156 assert_eq!(res.status(), StatusCode::NOT_FOUND); 1157} 1158 1159#[tokio::test] 1160async fn test_list_records_includes_cid() { 1161 let client = client(); 1162 let (did, jwt) = setup_new_user("list-includes-cid").await; 1163 create_post_with_rkey(&client, &did, &jwt, "test", "Test post").await; 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 ]) 1173 .send() 1174 .await 1175 .expect("Failed to list records"); 1176 assert_eq!(res.status(), StatusCode::OK); 1177 let body: Value = res.json().await.unwrap(); 1178 let records = body["records"].as_array().unwrap(); 1179 for record in records { 1180 assert!(record["uri"].is_string(), "Record should have uri"); 1181 assert!(record["cid"].is_string(), "Record should have cid"); 1182 assert!(record["value"].is_object(), "Record should have value"); 1183 let cid = record["cid"].as_str().unwrap(); 1184 assert!(cid.starts_with("bafy"), "CID should be valid"); 1185 } 1186} 1187 1188#[tokio::test] 1189async fn test_list_records_cursor_with_reverse() { 1190 let client = client(); 1191 let (did, jwt) = setup_new_user("list-cursor-reverse").await; 1192 for i in 0..5 { 1193 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await; 1194 } 1195 let res = client 1196 .get(format!( 1197 "{}/xrpc/com.atproto.repo.listRecords", 1198 base_url().await 1199 )) 1200 .query(&[ 1201 ("repo", did.as_str()), 1202 ("collection", "app.bsky.feed.post"), 1203 ("limit", "2"), 1204 ("reverse", "true"), 1205 ]) 1206 .send() 1207 .await 1208 .expect("Failed to list records"); 1209 assert_eq!(res.status(), StatusCode::OK); 1210 let body: Value = res.json().await.unwrap(); 1211 let records = body["records"].as_array().unwrap(); 1212 let first_rkeys: Vec<&str> = records 1213 .iter() 1214 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()) 1215 .collect(); 1216 assert_eq!(first_rkeys, vec!["post00", "post01"], "First page with reverse should start from oldest"); 1217 if let Some(cursor) = body["cursor"].as_str() { 1218 let res2 = 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", "2"), 1227 ("reverse", "true"), 1228 ("cursor", cursor), 1229 ]) 1230 .send() 1231 .await 1232 .expect("Failed to list records with cursor"); 1233 let body2: Value = res2.json().await.unwrap(); 1234 let records2 = body2["records"].as_array().unwrap(); 1235 let second_rkeys: Vec<&str> = records2 1236 .iter() 1237 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()) 1238 .collect(); 1239 assert_eq!(second_rkeys, vec!["post02", "post03"], "Second page should continue in ASC order"); 1240 } 1241}