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