this repo has no description
1mod common; 2use common::*; 3 4use base64::Engine; 5use chrono::Utc; 6use reqwest::{self, StatusCode, header}; 7use serde_json::{Value, json}; 8use std::time::Duration; 9 10async fn setup_new_user(handle_prefix: &str) -> (String, String) { 11 let client = client(); 12 let ts = Utc::now().timestamp_millis(); 13 let handle = format!("{}-{}.test", handle_prefix, ts); 14 let email = format!("{}-{}@test.com", handle_prefix, ts); 15 let password = "e2e-password-123"; 16 17 let create_account_payload = json!({ 18 "handle": handle, 19 "email": email, 20 "password": password 21 }); 22 let create_res = client 23 .post(format!( 24 "{}/xrpc/com.atproto.server.createAccount", 25 base_url().await 26 )) 27 .json(&create_account_payload) 28 .send() 29 .await 30 .expect("setup_new_user: Failed to send createAccount"); 31 32 if create_res.status() != reqwest::StatusCode::OK { 33 panic!( 34 "setup_new_user: Failed to create account: {:?}", 35 create_res.text().await 36 ); 37 } 38 39 let create_body: Value = create_res 40 .json() 41 .await 42 .expect("setup_new_user: createAccount response was not JSON"); 43 44 let new_did = create_body["did"] 45 .as_str() 46 .expect("setup_new_user: Response had no DID") 47 .to_string(); 48 let new_jwt = create_body["accessJwt"] 49 .as_str() 50 .expect("setup_new_user: Response had no accessJwt") 51 .to_string(); 52 53 (new_did, new_jwt) 54} 55 56#[tokio::test] 57async fn test_post_crud_lifecycle() { 58 let client = client(); 59 let (did, jwt) = setup_new_user("lifecycle-crud").await; 60 let collection = "app.bsky.feed.post"; 61 62 let rkey = format!("e2e_lifecycle_{}", Utc::now().timestamp_millis()); 63 let now = Utc::now().to_rfc3339(); 64 65 let original_text = "Hello from the lifecycle test!"; 66 let create_payload = json!({ 67 "repo": did, 68 "collection": collection, 69 "rkey": rkey, 70 "record": { 71 "$type": collection, 72 "text": original_text, 73 "createdAt": now 74 } 75 }); 76 77 let create_res = client 78 .post(format!( 79 "{}/xrpc/com.atproto.repo.putRecord", 80 base_url().await 81 )) 82 .bearer_auth(&jwt) 83 .json(&create_payload) 84 .send() 85 .await 86 .expect("Failed to send create request"); 87 88 if create_res.status() != reqwest::StatusCode::OK { 89 let status = create_res.status(); 90 let body = create_res 91 .text() 92 .await 93 .unwrap_or_else(|_| "Could not get body".to_string()); 94 panic!( 95 "Failed to create record. Status: {}, Body: {}", 96 status, body 97 ); 98 } 99 100 let create_body: Value = create_res 101 .json() 102 .await 103 .expect("create response was not JSON"); 104 let uri = create_body["uri"].as_str().unwrap(); 105 106 let params = [ 107 ("repo", did.as_str()), 108 ("collection", collection), 109 ("rkey", &rkey), 110 ]; 111 let get_res = client 112 .get(format!( 113 "{}/xrpc/com.atproto.repo.getRecord", 114 base_url().await 115 )) 116 .query(&params) 117 .send() 118 .await 119 .expect("Failed to send get request"); 120 121 assert_eq!( 122 get_res.status(), 123 reqwest::StatusCode::OK, 124 "Failed to get record after create" 125 ); 126 let get_body: Value = get_res.json().await.expect("get response was not JSON"); 127 assert_eq!(get_body["uri"], uri); 128 assert_eq!(get_body["value"]["text"], original_text); 129 130 let updated_text = "This post has been updated."; 131 let update_payload = json!({ 132 "repo": did, 133 "collection": collection, 134 "rkey": rkey, 135 "record": { 136 "$type": collection, 137 "text": updated_text, 138 "createdAt": now 139 } 140 }); 141 142 let update_res = client 143 .post(format!( 144 "{}/xrpc/com.atproto.repo.putRecord", 145 base_url().await 146 )) 147 .bearer_auth(&jwt) 148 .json(&update_payload) 149 .send() 150 .await 151 .expect("Failed to send update request"); 152 153 assert_eq!( 154 update_res.status(), 155 reqwest::StatusCode::OK, 156 "Failed to update record" 157 ); 158 159 let get_updated_res = client 160 .get(format!( 161 "{}/xrpc/com.atproto.repo.getRecord", 162 base_url().await 163 )) 164 .query(&params) 165 .send() 166 .await 167 .expect("Failed to send get-after-update request"); 168 169 assert_eq!( 170 get_updated_res.status(), 171 reqwest::StatusCode::OK, 172 "Failed to get record after update" 173 ); 174 let get_updated_body: Value = get_updated_res 175 .json() 176 .await 177 .expect("get-updated response was not JSON"); 178 assert_eq!( 179 get_updated_body["value"]["text"], updated_text, 180 "Text was not updated" 181 ); 182 183 let delete_payload = json!({ 184 "repo": did, 185 "collection": collection, 186 "rkey": rkey 187 }); 188 189 let delete_res = client 190 .post(format!( 191 "{}/xrpc/com.atproto.repo.deleteRecord", 192 base_url().await 193 )) 194 .bearer_auth(&jwt) 195 .json(&delete_payload) 196 .send() 197 .await 198 .expect("Failed to send delete request"); 199 200 assert_eq!( 201 delete_res.status(), 202 reqwest::StatusCode::OK, 203 "Failed to delete record" 204 ); 205 206 let get_deleted_res = client 207 .get(format!( 208 "{}/xrpc/com.atproto.repo.getRecord", 209 base_url().await 210 )) 211 .query(&params) 212 .send() 213 .await 214 .expect("Failed to send get-after-delete request"); 215 216 assert_eq!( 217 get_deleted_res.status(), 218 reqwest::StatusCode::NOT_FOUND, 219 "Record was found, but it should be deleted" 220 ); 221} 222 223#[tokio::test] 224async fn test_record_update_conflict_lifecycle() { 225 let client = client(); 226 let (user_did, user_jwt) = setup_new_user("user-conflict").await; 227 228 let profile_payload = json!({ 229 "repo": user_did, 230 "collection": "app.bsky.actor.profile", 231 "rkey": "self", 232 "record": { 233 "$type": "app.bsky.actor.profile", 234 "displayName": "Original Name" 235 } 236 }); 237 let create_res = client 238 .post(format!( 239 "{}/xrpc/com.atproto.repo.putRecord", 240 base_url().await 241 )) 242 .bearer_auth(&user_jwt) 243 .json(&profile_payload) 244 .send() 245 .await 246 .expect("create profile failed"); 247 248 if create_res.status() != reqwest::StatusCode::OK { 249 return; 250 } 251 252 let get_res = client 253 .get(format!( 254 "{}/xrpc/com.atproto.repo.getRecord", 255 base_url().await 256 )) 257 .query(&[ 258 ("repo", &user_did), 259 ("collection", &"app.bsky.actor.profile".to_string()), 260 ("rkey", &"self".to_string()), 261 ]) 262 .send() 263 .await 264 .expect("getRecord failed"); 265 let get_body: Value = get_res.json().await.expect("getRecord not json"); 266 let cid_v1 = get_body["cid"] 267 .as_str() 268 .expect("Profile v1 had no CID") 269 .to_string(); 270 271 let update_payload_v2 = json!({ 272 "repo": user_did, 273 "collection": "app.bsky.actor.profile", 274 "rkey": "self", 275 "record": { 276 "$type": "app.bsky.actor.profile", 277 "displayName": "Updated Name (v2)" 278 }, 279 "swapRecord": cid_v1 280 }); 281 let update_res_v2 = client 282 .post(format!( 283 "{}/xrpc/com.atproto.repo.putRecord", 284 base_url().await 285 )) 286 .bearer_auth(&user_jwt) 287 .json(&update_payload_v2) 288 .send() 289 .await 290 .expect("putRecord v2 failed"); 291 assert_eq!( 292 update_res_v2.status(), 293 reqwest::StatusCode::OK, 294 "v2 update failed" 295 ); 296 let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json"); 297 let cid_v2 = update_body_v2["cid"] 298 .as_str() 299 .expect("v2 response had no CID") 300 .to_string(); 301 302 let update_payload_v3_stale = json!({ 303 "repo": user_did, 304 "collection": "app.bsky.actor.profile", 305 "rkey": "self", 306 "record": { 307 "$type": "app.bsky.actor.profile", 308 "displayName": "Stale Update (v3)" 309 }, 310 "swapRecord": cid_v1 311 }); 312 let update_res_v3_stale = client 313 .post(format!( 314 "{}/xrpc/com.atproto.repo.putRecord", 315 base_url().await 316 )) 317 .bearer_auth(&user_jwt) 318 .json(&update_payload_v3_stale) 319 .send() 320 .await 321 .expect("putRecord v3 (stale) failed"); 322 323 assert_eq!( 324 update_res_v3_stale.status(), 325 reqwest::StatusCode::CONFLICT, 326 "Stale update did not cause a 409 Conflict" 327 ); 328 329 let update_payload_v3_good = json!({ 330 "repo": user_did, 331 "collection": "app.bsky.actor.profile", 332 "rkey": "self", 333 "record": { 334 "$type": "app.bsky.actor.profile", 335 "displayName": "Good Update (v3)" 336 }, 337 "swapRecord": cid_v2 338 }); 339 let update_res_v3_good = client 340 .post(format!( 341 "{}/xrpc/com.atproto.repo.putRecord", 342 base_url().await 343 )) 344 .bearer_auth(&user_jwt) 345 .json(&update_payload_v3_good) 346 .send() 347 .await 348 .expect("putRecord v3 (good) failed"); 349 350 assert_eq!( 351 update_res_v3_good.status(), 352 reqwest::StatusCode::OK, 353 "v3 (good) update failed" 354 ); 355} 356 357async fn create_post( 358 client: &reqwest::Client, 359 did: &str, 360 jwt: &str, 361 text: &str, 362) -> (String, String) { 363 let collection = "app.bsky.feed.post"; 364 let rkey = format!("e2e_social_{}", Utc::now().timestamp_millis()); 365 let now = Utc::now().to_rfc3339(); 366 367 let create_payload = json!({ 368 "repo": did, 369 "collection": collection, 370 "rkey": rkey, 371 "record": { 372 "$type": collection, 373 "text": text, 374 "createdAt": now 375 } 376 }); 377 378 let create_res = client 379 .post(format!( 380 "{}/xrpc/com.atproto.repo.putRecord", 381 base_url().await 382 )) 383 .bearer_auth(jwt) 384 .json(&create_payload) 385 .send() 386 .await 387 .expect("Failed to send create post request"); 388 389 assert_eq!( 390 create_res.status(), 391 reqwest::StatusCode::OK, 392 "Failed to create post record" 393 ); 394 let create_body: Value = create_res 395 .json() 396 .await 397 .expect("create post response was not JSON"); 398 let uri = create_body["uri"].as_str().unwrap().to_string(); 399 let cid = create_body["cid"].as_str().unwrap().to_string(); 400 (uri, cid) 401} 402 403async fn create_follow( 404 client: &reqwest::Client, 405 follower_did: &str, 406 follower_jwt: &str, 407 followee_did: &str, 408) -> (String, String) { 409 let collection = "app.bsky.graph.follow"; 410 let rkey = format!("e2e_follow_{}", Utc::now().timestamp_millis()); 411 let now = Utc::now().to_rfc3339(); 412 413 let create_payload = json!({ 414 "repo": follower_did, 415 "collection": collection, 416 "rkey": rkey, 417 "record": { 418 "$type": collection, 419 "subject": followee_did, 420 "createdAt": now 421 } 422 }); 423 424 let create_res = client 425 .post(format!( 426 "{}/xrpc/com.atproto.repo.putRecord", 427 base_url().await 428 )) 429 .bearer_auth(follower_jwt) 430 .json(&create_payload) 431 .send() 432 .await 433 .expect("Failed to send create follow request"); 434 435 assert_eq!( 436 create_res.status(), 437 reqwest::StatusCode::OK, 438 "Failed to create follow record" 439 ); 440 let create_body: Value = create_res 441 .json() 442 .await 443 .expect("create follow response was not JSON"); 444 let uri = create_body["uri"].as_str().unwrap().to_string(); 445 let cid = create_body["cid"].as_str().unwrap().to_string(); 446 (uri, cid) 447} 448 449#[tokio::test] 450async fn test_social_flow_lifecycle() { 451 let client = client(); 452 453 let (alice_did, alice_jwt) = setup_new_user("alice-social").await; 454 let (bob_did, bob_jwt) = setup_new_user("bob-social").await; 455 456 let (post1_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's first post!").await; 457 458 create_follow(&client, &bob_did, &bob_jwt, &alice_did).await; 459 460 tokio::time::sleep(Duration::from_secs(1)).await; 461 462 let timeline_res_1 = client 463 .get(format!( 464 "{}/xrpc/app.bsky.feed.getTimeline", 465 base_url().await 466 )) 467 .bearer_auth(&bob_jwt) 468 .send() 469 .await 470 .expect("Failed to get timeline (1)"); 471 472 assert_eq!( 473 timeline_res_1.status(), 474 reqwest::StatusCode::OK, 475 "Failed to get timeline (1)" 476 ); 477 let timeline_body_1: Value = timeline_res_1.json().await.expect("Timeline (1) not JSON"); 478 let feed_1 = timeline_body_1["feed"].as_array().unwrap(); 479 assert_eq!(feed_1.len(), 1, "Timeline should have 1 post"); 480 assert_eq!( 481 feed_1[0]["post"]["uri"], post1_uri, 482 "Post URI mismatch in timeline (1)" 483 ); 484 485 let (post2_uri, _) = create_post( 486 &client, 487 &alice_did, 488 &alice_jwt, 489 "Alice's second post, so exciting!", 490 ) 491 .await; 492 493 tokio::time::sleep(Duration::from_secs(1)).await; 494 495 let timeline_res_2 = client 496 .get(format!( 497 "{}/xrpc/app.bsky.feed.getTimeline", 498 base_url().await 499 )) 500 .bearer_auth(&bob_jwt) 501 .send() 502 .await 503 .expect("Failed to get timeline (2)"); 504 505 assert_eq!( 506 timeline_res_2.status(), 507 reqwest::StatusCode::OK, 508 "Failed to get timeline (2)" 509 ); 510 let timeline_body_2: Value = timeline_res_2.json().await.expect("Timeline (2) not JSON"); 511 let feed_2 = timeline_body_2["feed"].as_array().unwrap(); 512 assert_eq!(feed_2.len(), 2, "Timeline should have 2 posts"); 513 assert_eq!( 514 feed_2[0]["post"]["uri"], post2_uri, 515 "Post 2 should be first" 516 ); 517 assert_eq!( 518 feed_2[1]["post"]["uri"], post1_uri, 519 "Post 1 should be second" 520 ); 521 522 let delete_payload = json!({ 523 "repo": alice_did, 524 "collection": "app.bsky.feed.post", 525 "rkey": post1_uri.split('/').last().unwrap() 526 }); 527 let delete_res = client 528 .post(format!( 529 "{}/xrpc/com.atproto.repo.deleteRecord", 530 base_url().await 531 )) 532 .bearer_auth(&alice_jwt) 533 .json(&delete_payload) 534 .send() 535 .await 536 .expect("Failed to send delete request"); 537 assert_eq!( 538 delete_res.status(), 539 reqwest::StatusCode::OK, 540 "Failed to delete record" 541 ); 542 543 tokio::time::sleep(Duration::from_secs(1)).await; 544 545 let timeline_res_3 = client 546 .get(format!( 547 "{}/xrpc/app.bsky.feed.getTimeline", 548 base_url().await 549 )) 550 .bearer_auth(&bob_jwt) 551 .send() 552 .await 553 .expect("Failed to get timeline (3)"); 554 555 assert_eq!( 556 timeline_res_3.status(), 557 reqwest::StatusCode::OK, 558 "Failed to get timeline (3)" 559 ); 560 let timeline_body_3: Value = timeline_res_3.json().await.expect("Timeline (3) not JSON"); 561 let feed_3 = timeline_body_3["feed"].as_array().unwrap(); 562 assert_eq!(feed_3.len(), 1, "Timeline should have 1 post after delete"); 563 assert_eq!( 564 feed_3[0]["post"]["uri"], post2_uri, 565 "Only post 2 should remain" 566 ); 567} 568 569#[tokio::test] 570async fn test_session_lifecycle_wrong_password() { 571 let client = client(); 572 let (_, _) = setup_new_user("session-wrong-pw").await; 573 574 let login_payload = json!({ 575 "identifier": format!("session-wrong-pw-{}.test", Utc::now().timestamp_millis()), 576 "password": "wrong-password" 577 }); 578 579 let res = client 580 .post(format!( 581 "{}/xrpc/com.atproto.server.createSession", 582 base_url().await 583 )) 584 .json(&login_payload) 585 .send() 586 .await 587 .expect("Failed to send request"); 588 589 assert!( 590 res.status() == StatusCode::UNAUTHORIZED || res.status() == StatusCode::BAD_REQUEST, 591 "Expected 401 or 400 for wrong password, got {}", 592 res.status() 593 ); 594} 595 596#[tokio::test] 597async fn test_session_lifecycle_multiple_sessions() { 598 let client = client(); 599 let ts = Utc::now().timestamp_millis(); 600 let handle = format!("multi-session-{}.test", ts); 601 let email = format!("multi-session-{}@test.com", ts); 602 let password = "multi-session-pw"; 603 604 let create_payload = json!({ 605 "handle": handle, 606 "email": email, 607 "password": password 608 }); 609 let create_res = client 610 .post(format!( 611 "{}/xrpc/com.atproto.server.createAccount", 612 base_url().await 613 )) 614 .json(&create_payload) 615 .send() 616 .await 617 .expect("Failed to create account"); 618 assert_eq!(create_res.status(), StatusCode::OK); 619 620 let login_payload = json!({ 621 "identifier": handle, 622 "password": password 623 }); 624 625 let session1_res = client 626 .post(format!( 627 "{}/xrpc/com.atproto.server.createSession", 628 base_url().await 629 )) 630 .json(&login_payload) 631 .send() 632 .await 633 .expect("Failed session 1"); 634 assert_eq!(session1_res.status(), StatusCode::OK); 635 let session1: Value = session1_res.json().await.unwrap(); 636 let jwt1 = session1["accessJwt"].as_str().unwrap(); 637 638 let session2_res = client 639 .post(format!( 640 "{}/xrpc/com.atproto.server.createSession", 641 base_url().await 642 )) 643 .json(&login_payload) 644 .send() 645 .await 646 .expect("Failed session 2"); 647 assert_eq!(session2_res.status(), StatusCode::OK); 648 let session2: Value = session2_res.json().await.unwrap(); 649 let jwt2 = session2["accessJwt"].as_str().unwrap(); 650 651 assert_ne!(jwt1, jwt2, "Sessions should have different tokens"); 652 653 let get1 = client 654 .get(format!( 655 "{}/xrpc/com.atproto.server.getSession", 656 base_url().await 657 )) 658 .bearer_auth(jwt1) 659 .send() 660 .await 661 .expect("Failed getSession 1"); 662 assert_eq!(get1.status(), StatusCode::OK); 663 664 let get2 = client 665 .get(format!( 666 "{}/xrpc/com.atproto.server.getSession", 667 base_url().await 668 )) 669 .bearer_auth(jwt2) 670 .send() 671 .await 672 .expect("Failed getSession 2"); 673 assert_eq!(get2.status(), StatusCode::OK); 674} 675 676#[tokio::test] 677async fn test_session_lifecycle_refresh_invalidates_old() { 678 let client = client(); 679 let ts = Utc::now().timestamp_millis(); 680 let handle = format!("refresh-inv-{}.test", ts); 681 let email = format!("refresh-inv-{}@test.com", ts); 682 let password = "refresh-inv-pw"; 683 684 let create_payload = json!({ 685 "handle": handle, 686 "email": email, 687 "password": password 688 }); 689 client 690 .post(format!( 691 "{}/xrpc/com.atproto.server.createAccount", 692 base_url().await 693 )) 694 .json(&create_payload) 695 .send() 696 .await 697 .expect("Failed to create account"); 698 699 let login_payload = json!({ 700 "identifier": handle, 701 "password": password 702 }); 703 let login_res = client 704 .post(format!( 705 "{}/xrpc/com.atproto.server.createSession", 706 base_url().await 707 )) 708 .json(&login_payload) 709 .send() 710 .await 711 .expect("Failed login"); 712 let login_body: Value = login_res.json().await.unwrap(); 713 let refresh_jwt = login_body["refreshJwt"].as_str().unwrap().to_string(); 714 715 let refresh_res = client 716 .post(format!( 717 "{}/xrpc/com.atproto.server.refreshSession", 718 base_url().await 719 )) 720 .bearer_auth(&refresh_jwt) 721 .send() 722 .await 723 .expect("Failed first refresh"); 724 assert_eq!(refresh_res.status(), StatusCode::OK); 725 let refresh_body: Value = refresh_res.json().await.unwrap(); 726 let new_refresh_jwt = refresh_body["refreshJwt"].as_str().unwrap(); 727 728 assert_ne!(refresh_jwt, new_refresh_jwt, "Refresh tokens should differ"); 729 730 let reuse_res = client 731 .post(format!( 732 "{}/xrpc/com.atproto.server.refreshSession", 733 base_url().await 734 )) 735 .bearer_auth(&refresh_jwt) 736 .send() 737 .await 738 .expect("Failed reuse attempt"); 739 740 assert!( 741 reuse_res.status() == StatusCode::UNAUTHORIZED || reuse_res.status() == StatusCode::BAD_REQUEST, 742 "Old refresh token should be invalid after use" 743 ); 744} 745 746async fn create_like( 747 client: &reqwest::Client, 748 liker_did: &str, 749 liker_jwt: &str, 750 subject_uri: &str, 751 subject_cid: &str, 752) -> (String, String) { 753 let collection = "app.bsky.feed.like"; 754 let rkey = format!("e2e_like_{}", Utc::now().timestamp_millis()); 755 let now = Utc::now().to_rfc3339(); 756 757 let payload = json!({ 758 "repo": liker_did, 759 "collection": collection, 760 "rkey": rkey, 761 "record": { 762 "$type": collection, 763 "subject": { 764 "uri": subject_uri, 765 "cid": subject_cid 766 }, 767 "createdAt": now 768 } 769 }); 770 771 let res = client 772 .post(format!( 773 "{}/xrpc/com.atproto.repo.putRecord", 774 base_url().await 775 )) 776 .bearer_auth(liker_jwt) 777 .json(&payload) 778 .send() 779 .await 780 .expect("Failed to create like"); 781 782 assert_eq!(res.status(), StatusCode::OK, "Failed to create like"); 783 let body: Value = res.json().await.expect("Like response not JSON"); 784 ( 785 body["uri"].as_str().unwrap().to_string(), 786 body["cid"].as_str().unwrap().to_string(), 787 ) 788} 789 790async fn create_repost( 791 client: &reqwest::Client, 792 reposter_did: &str, 793 reposter_jwt: &str, 794 subject_uri: &str, 795 subject_cid: &str, 796) -> (String, String) { 797 let collection = "app.bsky.feed.repost"; 798 let rkey = format!("e2e_repost_{}", Utc::now().timestamp_millis()); 799 let now = Utc::now().to_rfc3339(); 800 801 let payload = json!({ 802 "repo": reposter_did, 803 "collection": collection, 804 "rkey": rkey, 805 "record": { 806 "$type": collection, 807 "subject": { 808 "uri": subject_uri, 809 "cid": subject_cid 810 }, 811 "createdAt": now 812 } 813 }); 814 815 let res = client 816 .post(format!( 817 "{}/xrpc/com.atproto.repo.putRecord", 818 base_url().await 819 )) 820 .bearer_auth(reposter_jwt) 821 .json(&payload) 822 .send() 823 .await 824 .expect("Failed to create repost"); 825 826 assert_eq!(res.status(), StatusCode::OK, "Failed to create repost"); 827 let body: Value = res.json().await.expect("Repost response not JSON"); 828 ( 829 body["uri"].as_str().unwrap().to_string(), 830 body["cid"].as_str().unwrap().to_string(), 831 ) 832} 833 834#[tokio::test] 835async fn test_profile_lifecycle() { 836 let client = client(); 837 let (did, jwt) = setup_new_user("profile-lifecycle").await; 838 839 let profile_payload = json!({ 840 "repo": did, 841 "collection": "app.bsky.actor.profile", 842 "rkey": "self", 843 "record": { 844 "$type": "app.bsky.actor.profile", 845 "displayName": "Test User", 846 "description": "A test profile for lifecycle testing" 847 } 848 }); 849 850 let create_res = client 851 .post(format!( 852 "{}/xrpc/com.atproto.repo.putRecord", 853 base_url().await 854 )) 855 .bearer_auth(&jwt) 856 .json(&profile_payload) 857 .send() 858 .await 859 .expect("Failed to create profile"); 860 861 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile"); 862 let create_body: Value = create_res.json().await.unwrap(); 863 let initial_cid = create_body["cid"].as_str().unwrap().to_string(); 864 865 let get_res = client 866 .get(format!( 867 "{}/xrpc/com.atproto.repo.getRecord", 868 base_url().await 869 )) 870 .query(&[ 871 ("repo", did.as_str()), 872 ("collection", "app.bsky.actor.profile"), 873 ("rkey", "self"), 874 ]) 875 .send() 876 .await 877 .expect("Failed to get profile"); 878 879 assert_eq!(get_res.status(), StatusCode::OK); 880 let get_body: Value = get_res.json().await.unwrap(); 881 assert_eq!(get_body["value"]["displayName"], "Test User"); 882 assert_eq!(get_body["value"]["description"], "A test profile for lifecycle testing"); 883 884 let update_payload = json!({ 885 "repo": did, 886 "collection": "app.bsky.actor.profile", 887 "rkey": "self", 888 "record": { 889 "$type": "app.bsky.actor.profile", 890 "displayName": "Updated User", 891 "description": "Profile has been updated" 892 }, 893 "swapRecord": initial_cid 894 }); 895 896 let update_res = client 897 .post(format!( 898 "{}/xrpc/com.atproto.repo.putRecord", 899 base_url().await 900 )) 901 .bearer_auth(&jwt) 902 .json(&update_payload) 903 .send() 904 .await 905 .expect("Failed to update profile"); 906 907 assert_eq!(update_res.status(), StatusCode::OK, "Failed to update profile"); 908 909 let get_updated_res = client 910 .get(format!( 911 "{}/xrpc/com.atproto.repo.getRecord", 912 base_url().await 913 )) 914 .query(&[ 915 ("repo", did.as_str()), 916 ("collection", "app.bsky.actor.profile"), 917 ("rkey", "self"), 918 ]) 919 .send() 920 .await 921 .expect("Failed to get updated profile"); 922 923 let updated_body: Value = get_updated_res.json().await.unwrap(); 924 assert_eq!(updated_body["value"]["displayName"], "Updated User"); 925} 926 927#[tokio::test] 928async fn test_reply_thread_lifecycle() { 929 let client = client(); 930 931 let (alice_did, alice_jwt) = setup_new_user("alice-thread").await; 932 let (bob_did, bob_jwt) = setup_new_user("bob-thread").await; 933 934 let (root_uri, root_cid) = create_post(&client, &alice_did, &alice_jwt, "This is the root post").await; 935 936 tokio::time::sleep(Duration::from_millis(100)).await; 937 938 let reply_collection = "app.bsky.feed.post"; 939 let reply_rkey = format!("e2e_reply_{}", Utc::now().timestamp_millis()); 940 let now = Utc::now().to_rfc3339(); 941 942 let reply_payload = json!({ 943 "repo": bob_did, 944 "collection": reply_collection, 945 "rkey": reply_rkey, 946 "record": { 947 "$type": reply_collection, 948 "text": "This is Bob's reply to Alice", 949 "createdAt": now, 950 "reply": { 951 "root": { 952 "uri": root_uri, 953 "cid": root_cid 954 }, 955 "parent": { 956 "uri": root_uri, 957 "cid": root_cid 958 } 959 } 960 } 961 }); 962 963 let reply_res = client 964 .post(format!( 965 "{}/xrpc/com.atproto.repo.putRecord", 966 base_url().await 967 )) 968 .bearer_auth(&bob_jwt) 969 .json(&reply_payload) 970 .send() 971 .await 972 .expect("Failed to create reply"); 973 974 assert_eq!(reply_res.status(), StatusCode::OK, "Failed to create reply"); 975 let reply_body: Value = reply_res.json().await.unwrap(); 976 let reply_uri = reply_body["uri"].as_str().unwrap(); 977 let reply_cid = reply_body["cid"].as_str().unwrap(); 978 979 let get_reply_res = client 980 .get(format!( 981 "{}/xrpc/com.atproto.repo.getRecord", 982 base_url().await 983 )) 984 .query(&[ 985 ("repo", bob_did.as_str()), 986 ("collection", reply_collection), 987 ("rkey", reply_rkey.as_str()), 988 ]) 989 .send() 990 .await 991 .expect("Failed to get reply"); 992 993 assert_eq!(get_reply_res.status(), StatusCode::OK); 994 let reply_record: Value = get_reply_res.json().await.unwrap(); 995 assert_eq!(reply_record["value"]["reply"]["root"]["uri"], root_uri); 996 assert_eq!(reply_record["value"]["reply"]["parent"]["uri"], root_uri); 997 998 tokio::time::sleep(Duration::from_millis(100)).await; 999 1000 let nested_reply_rkey = format!("e2e_nested_reply_{}", Utc::now().timestamp_millis()); 1001 let nested_payload = json!({ 1002 "repo": alice_did, 1003 "collection": reply_collection, 1004 "rkey": nested_reply_rkey, 1005 "record": { 1006 "$type": reply_collection, 1007 "text": "Alice replies to Bob's reply", 1008 "createdAt": Utc::now().to_rfc3339(), 1009 "reply": { 1010 "root": { 1011 "uri": root_uri, 1012 "cid": root_cid 1013 }, 1014 "parent": { 1015 "uri": reply_uri, 1016 "cid": reply_cid 1017 } 1018 } 1019 } 1020 }); 1021 1022 let nested_res = client 1023 .post(format!( 1024 "{}/xrpc/com.atproto.repo.putRecord", 1025 base_url().await 1026 )) 1027 .bearer_auth(&alice_jwt) 1028 .json(&nested_payload) 1029 .send() 1030 .await 1031 .expect("Failed to create nested reply"); 1032 1033 assert_eq!(nested_res.status(), StatusCode::OK, "Failed to create nested reply"); 1034} 1035 1036#[tokio::test] 1037async fn test_like_lifecycle() { 1038 let client = client(); 1039 1040 let (alice_did, alice_jwt) = setup_new_user("alice-like").await; 1041 let (bob_did, bob_jwt) = setup_new_user("bob-like").await; 1042 1043 let (post_uri, post_cid) = create_post(&client, &alice_did, &alice_jwt, "Like this post!").await; 1044 1045 let (like_uri, _) = create_like(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await; 1046 1047 let like_rkey = like_uri.split('/').last().unwrap(); 1048 let get_like_res = client 1049 .get(format!( 1050 "{}/xrpc/com.atproto.repo.getRecord", 1051 base_url().await 1052 )) 1053 .query(&[ 1054 ("repo", bob_did.as_str()), 1055 ("collection", "app.bsky.feed.like"), 1056 ("rkey", like_rkey), 1057 ]) 1058 .send() 1059 .await 1060 .expect("Failed to get like"); 1061 1062 assert_eq!(get_like_res.status(), StatusCode::OK); 1063 let like_body: Value = get_like_res.json().await.unwrap(); 1064 assert_eq!(like_body["value"]["subject"]["uri"], post_uri); 1065 1066 let delete_payload = json!({ 1067 "repo": bob_did, 1068 "collection": "app.bsky.feed.like", 1069 "rkey": like_rkey 1070 }); 1071 1072 let delete_res = client 1073 .post(format!( 1074 "{}/xrpc/com.atproto.repo.deleteRecord", 1075 base_url().await 1076 )) 1077 .bearer_auth(&bob_jwt) 1078 .json(&delete_payload) 1079 .send() 1080 .await 1081 .expect("Failed to delete like"); 1082 1083 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete like"); 1084 1085 let get_deleted_res = client 1086 .get(format!( 1087 "{}/xrpc/com.atproto.repo.getRecord", 1088 base_url().await 1089 )) 1090 .query(&[ 1091 ("repo", bob_did.as_str()), 1092 ("collection", "app.bsky.feed.like"), 1093 ("rkey", like_rkey), 1094 ]) 1095 .send() 1096 .await 1097 .expect("Failed to check deleted like"); 1098 1099 assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Like should be deleted"); 1100} 1101 1102#[tokio::test] 1103async fn test_repost_lifecycle() { 1104 let client = client(); 1105 1106 let (alice_did, alice_jwt) = setup_new_user("alice-repost").await; 1107 let (bob_did, bob_jwt) = setup_new_user("bob-repost").await; 1108 1109 let (post_uri, post_cid) = create_post(&client, &alice_did, &alice_jwt, "Repost this!").await; 1110 1111 let (repost_uri, _) = create_repost(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await; 1112 1113 let repost_rkey = repost_uri.split('/').last().unwrap(); 1114 let get_repost_res = client 1115 .get(format!( 1116 "{}/xrpc/com.atproto.repo.getRecord", 1117 base_url().await 1118 )) 1119 .query(&[ 1120 ("repo", bob_did.as_str()), 1121 ("collection", "app.bsky.feed.repost"), 1122 ("rkey", repost_rkey), 1123 ]) 1124 .send() 1125 .await 1126 .expect("Failed to get repost"); 1127 1128 assert_eq!(get_repost_res.status(), StatusCode::OK); 1129 let repost_body: Value = get_repost_res.json().await.unwrap(); 1130 assert_eq!(repost_body["value"]["subject"]["uri"], post_uri); 1131 1132 let delete_payload = json!({ 1133 "repo": bob_did, 1134 "collection": "app.bsky.feed.repost", 1135 "rkey": repost_rkey 1136 }); 1137 1138 let delete_res = client 1139 .post(format!( 1140 "{}/xrpc/com.atproto.repo.deleteRecord", 1141 base_url().await 1142 )) 1143 .bearer_auth(&bob_jwt) 1144 .json(&delete_payload) 1145 .send() 1146 .await 1147 .expect("Failed to delete repost"); 1148 1149 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete repost"); 1150} 1151 1152#[tokio::test] 1153async fn test_unfollow_lifecycle() { 1154 let client = client(); 1155 1156 let (alice_did, _alice_jwt) = setup_new_user("alice-unfollow").await; 1157 let (bob_did, bob_jwt) = setup_new_user("bob-unfollow").await; 1158 1159 let (follow_uri, _) = create_follow(&client, &bob_did, &bob_jwt, &alice_did).await; 1160 1161 let follow_rkey = follow_uri.split('/').last().unwrap(); 1162 let get_follow_res = client 1163 .get(format!( 1164 "{}/xrpc/com.atproto.repo.getRecord", 1165 base_url().await 1166 )) 1167 .query(&[ 1168 ("repo", bob_did.as_str()), 1169 ("collection", "app.bsky.graph.follow"), 1170 ("rkey", follow_rkey), 1171 ]) 1172 .send() 1173 .await 1174 .expect("Failed to get follow"); 1175 1176 assert_eq!(get_follow_res.status(), StatusCode::OK); 1177 1178 let unfollow_payload = json!({ 1179 "repo": bob_did, 1180 "collection": "app.bsky.graph.follow", 1181 "rkey": follow_rkey 1182 }); 1183 1184 let unfollow_res = client 1185 .post(format!( 1186 "{}/xrpc/com.atproto.repo.deleteRecord", 1187 base_url().await 1188 )) 1189 .bearer_auth(&bob_jwt) 1190 .json(&unfollow_payload) 1191 .send() 1192 .await 1193 .expect("Failed to unfollow"); 1194 1195 assert_eq!(unfollow_res.status(), StatusCode::OK, "Failed to unfollow"); 1196 1197 let get_deleted_res = client 1198 .get(format!( 1199 "{}/xrpc/com.atproto.repo.getRecord", 1200 base_url().await 1201 )) 1202 .query(&[ 1203 ("repo", bob_did.as_str()), 1204 ("collection", "app.bsky.graph.follow"), 1205 ("rkey", follow_rkey), 1206 ]) 1207 .send() 1208 .await 1209 .expect("Failed to check deleted follow"); 1210 1211 assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Follow should be deleted"); 1212} 1213 1214#[tokio::test] 1215async fn test_timeline_after_unfollow() { 1216 let client = client(); 1217 1218 let (alice_did, alice_jwt) = setup_new_user("alice-tl-unfollow").await; 1219 let (bob_did, bob_jwt) = setup_new_user("bob-tl-unfollow").await; 1220 1221 let (follow_uri, _) = create_follow(&client, &bob_did, &bob_jwt, &alice_did).await; 1222 1223 create_post(&client, &alice_did, &alice_jwt, "Post while following").await; 1224 1225 tokio::time::sleep(Duration::from_secs(1)).await; 1226 1227 let timeline_res = client 1228 .get(format!( 1229 "{}/xrpc/app.bsky.feed.getTimeline", 1230 base_url().await 1231 )) 1232 .bearer_auth(&bob_jwt) 1233 .send() 1234 .await 1235 .expect("Failed to get timeline"); 1236 1237 assert_eq!(timeline_res.status(), StatusCode::OK); 1238 let timeline_body: Value = timeline_res.json().await.unwrap(); 1239 let feed = timeline_body["feed"].as_array().unwrap(); 1240 assert_eq!(feed.len(), 1, "Should see 1 post from Alice"); 1241 1242 let follow_rkey = follow_uri.split('/').last().unwrap(); 1243 let unfollow_payload = json!({ 1244 "repo": bob_did, 1245 "collection": "app.bsky.graph.follow", 1246 "rkey": follow_rkey 1247 }); 1248 client 1249 .post(format!( 1250 "{}/xrpc/com.atproto.repo.deleteRecord", 1251 base_url().await 1252 )) 1253 .bearer_auth(&bob_jwt) 1254 .json(&unfollow_payload) 1255 .send() 1256 .await 1257 .expect("Failed to unfollow"); 1258 1259 tokio::time::sleep(Duration::from_secs(1)).await; 1260 1261 let timeline_after_res = client 1262 .get(format!( 1263 "{}/xrpc/app.bsky.feed.getTimeline", 1264 base_url().await 1265 )) 1266 .bearer_auth(&bob_jwt) 1267 .send() 1268 .await 1269 .expect("Failed to get timeline after unfollow"); 1270 1271 assert_eq!(timeline_after_res.status(), StatusCode::OK); 1272 let timeline_after: Value = timeline_after_res.json().await.unwrap(); 1273 let feed_after = timeline_after["feed"].as_array().unwrap(); 1274 assert_eq!(feed_after.len(), 0, "Should see 0 posts after unfollowing"); 1275} 1276 1277#[tokio::test] 1278async fn test_blob_in_record_lifecycle() { 1279 let client = client(); 1280 let (did, jwt) = setup_new_user("blob-record").await; 1281 1282 let blob_data = b"This is test blob data for a profile avatar"; 1283 let upload_res = client 1284 .post(format!( 1285 "{}/xrpc/com.atproto.repo.uploadBlob", 1286 base_url().await 1287 )) 1288 .header(header::CONTENT_TYPE, "text/plain") 1289 .bearer_auth(&jwt) 1290 .body(blob_data.to_vec()) 1291 .send() 1292 .await 1293 .expect("Failed to upload blob"); 1294 1295 assert_eq!(upload_res.status(), StatusCode::OK); 1296 let upload_body: Value = upload_res.json().await.unwrap(); 1297 let blob_ref = upload_body["blob"].clone(); 1298 1299 let profile_payload = json!({ 1300 "repo": did, 1301 "collection": "app.bsky.actor.profile", 1302 "rkey": "self", 1303 "record": { 1304 "$type": "app.bsky.actor.profile", 1305 "displayName": "User With Avatar", 1306 "avatar": blob_ref 1307 } 1308 }); 1309 1310 let create_res = client 1311 .post(format!( 1312 "{}/xrpc/com.atproto.repo.putRecord", 1313 base_url().await 1314 )) 1315 .bearer_auth(&jwt) 1316 .json(&profile_payload) 1317 .send() 1318 .await 1319 .expect("Failed to create profile with blob"); 1320 1321 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile with blob"); 1322 1323 let get_res = client 1324 .get(format!( 1325 "{}/xrpc/com.atproto.repo.getRecord", 1326 base_url().await 1327 )) 1328 .query(&[ 1329 ("repo", did.as_str()), 1330 ("collection", "app.bsky.actor.profile"), 1331 ("rkey", "self"), 1332 ]) 1333 .send() 1334 .await 1335 .expect("Failed to get profile"); 1336 1337 assert_eq!(get_res.status(), StatusCode::OK); 1338 let profile: Value = get_res.json().await.unwrap(); 1339 assert!(profile["value"]["avatar"]["ref"]["$link"].is_string()); 1340} 1341 1342#[tokio::test] 1343async fn test_authorization_cannot_modify_other_repo() { 1344 let client = client(); 1345 1346 let (alice_did, _alice_jwt) = setup_new_user("alice-auth").await; 1347 let (_bob_did, bob_jwt) = setup_new_user("bob-auth").await; 1348 1349 let post_payload = json!({ 1350 "repo": alice_did, 1351 "collection": "app.bsky.feed.post", 1352 "rkey": "unauthorized-post", 1353 "record": { 1354 "$type": "app.bsky.feed.post", 1355 "text": "Bob trying to post as Alice", 1356 "createdAt": Utc::now().to_rfc3339() 1357 } 1358 }); 1359 1360 let res = client 1361 .post(format!( 1362 "{}/xrpc/com.atproto.repo.putRecord", 1363 base_url().await 1364 )) 1365 .bearer_auth(&bob_jwt) 1366 .json(&post_payload) 1367 .send() 1368 .await 1369 .expect("Failed to send request"); 1370 1371 assert!( 1372 res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED, 1373 "Expected 403 or 401 when writing to another user's repo, got {}", 1374 res.status() 1375 ); 1376} 1377 1378#[tokio::test] 1379async fn test_authorization_cannot_delete_other_record() { 1380 let client = client(); 1381 1382 let (alice_did, alice_jwt) = setup_new_user("alice-del-auth").await; 1383 let (_bob_did, bob_jwt) = setup_new_user("bob-del-auth").await; 1384 1385 let (post_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's post").await; 1386 let post_rkey = post_uri.split('/').last().unwrap(); 1387 1388 let delete_payload = json!({ 1389 "repo": alice_did, 1390 "collection": "app.bsky.feed.post", 1391 "rkey": post_rkey 1392 }); 1393 1394 let res = client 1395 .post(format!( 1396 "{}/xrpc/com.atproto.repo.deleteRecord", 1397 base_url().await 1398 )) 1399 .bearer_auth(&bob_jwt) 1400 .json(&delete_payload) 1401 .send() 1402 .await 1403 .expect("Failed to send request"); 1404 1405 assert!( 1406 res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED, 1407 "Expected 403 or 401 when deleting another user's record, got {}", 1408 res.status() 1409 ); 1410 1411 let get_res = client 1412 .get(format!( 1413 "{}/xrpc/com.atproto.repo.getRecord", 1414 base_url().await 1415 )) 1416 .query(&[ 1417 ("repo", alice_did.as_str()), 1418 ("collection", "app.bsky.feed.post"), 1419 ("rkey", post_rkey), 1420 ]) 1421 .send() 1422 .await 1423 .expect("Failed to verify record exists"); 1424 1425 assert_eq!(get_res.status(), StatusCode::OK, "Record should still exist"); 1426} 1427 1428#[tokio::test] 1429async fn test_list_records_pagination() { 1430 let client = client(); 1431 let (did, jwt) = setup_new_user("list-pagination").await; 1432 1433 for i in 0..5 { 1434 tokio::time::sleep(Duration::from_millis(50)).await; 1435 create_post(&client, &did, &jwt, &format!("Post number {}", i)).await; 1436 } 1437 1438 let list_res = client 1439 .get(format!( 1440 "{}/xrpc/com.atproto.repo.listRecords", 1441 base_url().await 1442 )) 1443 .query(&[ 1444 ("repo", did.as_str()), 1445 ("collection", "app.bsky.feed.post"), 1446 ("limit", "2"), 1447 ]) 1448 .send() 1449 .await 1450 .expect("Failed to list records"); 1451 1452 assert_eq!(list_res.status(), StatusCode::OK); 1453 let list_body: Value = list_res.json().await.unwrap(); 1454 let records = list_body["records"].as_array().unwrap(); 1455 assert_eq!(records.len(), 2, "Should return 2 records with limit=2"); 1456 1457 if let Some(cursor) = list_body["cursor"].as_str() { 1458 let list_page2_res = client 1459 .get(format!( 1460 "{}/xrpc/com.atproto.repo.listRecords", 1461 base_url().await 1462 )) 1463 .query(&[ 1464 ("repo", did.as_str()), 1465 ("collection", "app.bsky.feed.post"), 1466 ("limit", "2"), 1467 ("cursor", cursor), 1468 ]) 1469 .send() 1470 .await 1471 .expect("Failed to list records page 2"); 1472 1473 assert_eq!(list_page2_res.status(), StatusCode::OK); 1474 let page2_body: Value = list_page2_res.json().await.unwrap(); 1475 let page2_records = page2_body["records"].as_array().unwrap(); 1476 assert_eq!(page2_records.len(), 2, "Page 2 should have 2 more records"); 1477 } 1478} 1479 1480#[tokio::test] 1481async fn test_mutual_follow_lifecycle() { 1482 let client = client(); 1483 1484 let (alice_did, alice_jwt) = setup_new_user("alice-mutual").await; 1485 let (bob_did, bob_jwt) = setup_new_user("bob-mutual").await; 1486 1487 create_follow(&client, &alice_did, &alice_jwt, &bob_did).await; 1488 create_follow(&client, &bob_did, &bob_jwt, &alice_did).await; 1489 1490 create_post(&client, &alice_did, &alice_jwt, "Alice's post for mutual").await; 1491 create_post(&client, &bob_did, &bob_jwt, "Bob's post for mutual").await; 1492 1493 tokio::time::sleep(Duration::from_secs(1)).await; 1494 1495 let alice_timeline_res = client 1496 .get(format!( 1497 "{}/xrpc/app.bsky.feed.getTimeline", 1498 base_url().await 1499 )) 1500 .bearer_auth(&alice_jwt) 1501 .send() 1502 .await 1503 .expect("Failed to get Alice's timeline"); 1504 1505 assert_eq!(alice_timeline_res.status(), StatusCode::OK); 1506 let alice_tl: Value = alice_timeline_res.json().await.unwrap(); 1507 let alice_feed = alice_tl["feed"].as_array().unwrap(); 1508 assert_eq!(alice_feed.len(), 1, "Alice should see Bob's 1 post"); 1509 1510 let bob_timeline_res = client 1511 .get(format!( 1512 "{}/xrpc/app.bsky.feed.getTimeline", 1513 base_url().await 1514 )) 1515 .bearer_auth(&bob_jwt) 1516 .send() 1517 .await 1518 .expect("Failed to get Bob's timeline"); 1519 1520 assert_eq!(bob_timeline_res.status(), StatusCode::OK); 1521 let bob_tl: Value = bob_timeline_res.json().await.unwrap(); 1522 let bob_feed = bob_tl["feed"].as_array().unwrap(); 1523 assert_eq!(bob_feed.len(), 1, "Bob should see Alice's 1 post"); 1524} 1525 1526#[tokio::test] 1527async fn test_account_to_post_full_lifecycle() { 1528 let client = client(); 1529 let ts = Utc::now().timestamp_millis(); 1530 let handle = format!("fullcycle-{}.test", ts); 1531 let email = format!("fullcycle-{}@test.com", ts); 1532 let password = "fullcycle-password"; 1533 1534 let create_account_res = client 1535 .post(format!( 1536 "{}/xrpc/com.atproto.server.createAccount", 1537 base_url().await 1538 )) 1539 .json(&json!({ 1540 "handle": handle, 1541 "email": email, 1542 "password": password 1543 })) 1544 .send() 1545 .await 1546 .expect("Failed to create account"); 1547 1548 assert_eq!(create_account_res.status(), StatusCode::OK); 1549 let account_body: Value = create_account_res.json().await.unwrap(); 1550 let did = account_body["did"].as_str().unwrap().to_string(); 1551 let access_jwt = account_body["accessJwt"].as_str().unwrap().to_string(); 1552 1553 let get_session_res = client 1554 .get(format!( 1555 "{}/xrpc/com.atproto.server.getSession", 1556 base_url().await 1557 )) 1558 .bearer_auth(&access_jwt) 1559 .send() 1560 .await 1561 .expect("Failed to get session"); 1562 1563 assert_eq!(get_session_res.status(), StatusCode::OK); 1564 let session_body: Value = get_session_res.json().await.unwrap(); 1565 assert_eq!(session_body["did"], did); 1566 assert_eq!(session_body["handle"], handle); 1567 1568 let profile_res = client 1569 .post(format!( 1570 "{}/xrpc/com.atproto.repo.putRecord", 1571 base_url().await 1572 )) 1573 .bearer_auth(&access_jwt) 1574 .json(&json!({ 1575 "repo": did, 1576 "collection": "app.bsky.actor.profile", 1577 "rkey": "self", 1578 "record": { 1579 "$type": "app.bsky.actor.profile", 1580 "displayName": "Full Cycle User" 1581 } 1582 })) 1583 .send() 1584 .await 1585 .expect("Failed to create profile"); 1586 1587 assert_eq!(profile_res.status(), StatusCode::OK); 1588 1589 let (post_uri, post_cid) = create_post(&client, &did, &access_jwt, "My first post!").await; 1590 1591 let get_post_res = client 1592 .get(format!( 1593 "{}/xrpc/com.atproto.repo.getRecord", 1594 base_url().await 1595 )) 1596 .query(&[ 1597 ("repo", did.as_str()), 1598 ("collection", "app.bsky.feed.post"), 1599 ("rkey", post_uri.split('/').last().unwrap()), 1600 ]) 1601 .send() 1602 .await 1603 .expect("Failed to get post"); 1604 1605 assert_eq!(get_post_res.status(), StatusCode::OK); 1606 1607 create_like(&client, &did, &access_jwt, &post_uri, &post_cid).await; 1608 1609 let describe_res = client 1610 .get(format!( 1611 "{}/xrpc/com.atproto.repo.describeRepo", 1612 base_url().await 1613 )) 1614 .query(&[("repo", did.as_str())]) 1615 .send() 1616 .await 1617 .expect("Failed to describe repo"); 1618 1619 assert_eq!(describe_res.status(), StatusCode::OK); 1620 let describe_body: Value = describe_res.json().await.unwrap(); 1621 assert_eq!(describe_body["did"], did); 1622 assert_eq!(describe_body["handle"], handle); 1623} 1624 1625#[tokio::test] 1626async fn test_app_password_lifecycle() { 1627 let client = client(); 1628 let ts = Utc::now().timestamp_millis(); 1629 let handle = format!("apppass-{}.test", ts); 1630 let email = format!("apppass-{}@test.com", ts); 1631 let password = "apppass-password"; 1632 1633 let create_res = client 1634 .post(format!( 1635 "{}/xrpc/com.atproto.server.createAccount", 1636 base_url().await 1637 )) 1638 .json(&json!({ 1639 "handle": handle, 1640 "email": email, 1641 "password": password 1642 })) 1643 .send() 1644 .await 1645 .expect("Failed to create account"); 1646 1647 assert_eq!(create_res.status(), StatusCode::OK); 1648 let account: Value = create_res.json().await.unwrap(); 1649 let jwt = account["accessJwt"].as_str().unwrap(); 1650 1651 let create_app_pass_res = client 1652 .post(format!( 1653 "{}/xrpc/com.atproto.server.createAppPassword", 1654 base_url().await 1655 )) 1656 .bearer_auth(jwt) 1657 .json(&json!({ "name": "Test App" })) 1658 .send() 1659 .await 1660 .expect("Failed to create app password"); 1661 1662 assert_eq!(create_app_pass_res.status(), StatusCode::OK); 1663 let app_pass: Value = create_app_pass_res.json().await.unwrap(); 1664 let app_password = app_pass["password"].as_str().unwrap().to_string(); 1665 assert_eq!(app_pass["name"], "Test App"); 1666 1667 let list_res = client 1668 .get(format!( 1669 "{}/xrpc/com.atproto.server.listAppPasswords", 1670 base_url().await 1671 )) 1672 .bearer_auth(jwt) 1673 .send() 1674 .await 1675 .expect("Failed to list app passwords"); 1676 1677 assert_eq!(list_res.status(), StatusCode::OK); 1678 let list_body: Value = list_res.json().await.unwrap(); 1679 let passwords = list_body["passwords"].as_array().unwrap(); 1680 assert_eq!(passwords.len(), 1); 1681 assert_eq!(passwords[0]["name"], "Test App"); 1682 1683 let login_res = client 1684 .post(format!( 1685 "{}/xrpc/com.atproto.server.createSession", 1686 base_url().await 1687 )) 1688 .json(&json!({ 1689 "identifier": handle, 1690 "password": app_password 1691 })) 1692 .send() 1693 .await 1694 .expect("Failed to login with app password"); 1695 1696 assert_eq!(login_res.status(), StatusCode::OK, "App password login should work"); 1697 1698 let revoke_res = client 1699 .post(format!( 1700 "{}/xrpc/com.atproto.server.revokeAppPassword", 1701 base_url().await 1702 )) 1703 .bearer_auth(jwt) 1704 .json(&json!({ "name": "Test App" })) 1705 .send() 1706 .await 1707 .expect("Failed to revoke app password"); 1708 1709 assert_eq!(revoke_res.status(), StatusCode::OK); 1710 1711 let login_after_revoke = client 1712 .post(format!( 1713 "{}/xrpc/com.atproto.server.createSession", 1714 base_url().await 1715 )) 1716 .json(&json!({ 1717 "identifier": handle, 1718 "password": app_password 1719 })) 1720 .send() 1721 .await 1722 .expect("Failed to attempt login after revoke"); 1723 1724 assert!( 1725 login_after_revoke.status() == StatusCode::UNAUTHORIZED 1726 || login_after_revoke.status() == StatusCode::BAD_REQUEST, 1727 "Revoked app password should not work" 1728 ); 1729 1730 let list_after_revoke = client 1731 .get(format!( 1732 "{}/xrpc/com.atproto.server.listAppPasswords", 1733 base_url().await 1734 )) 1735 .bearer_auth(jwt) 1736 .send() 1737 .await 1738 .expect("Failed to list after revoke"); 1739 1740 let list_after: Value = list_after_revoke.json().await.unwrap(); 1741 let passwords_after = list_after["passwords"].as_array().unwrap(); 1742 assert_eq!(passwords_after.len(), 0, "No app passwords should remain"); 1743} 1744 1745#[tokio::test] 1746async fn test_account_deactivation_lifecycle() { 1747 let client = client(); 1748 let ts = Utc::now().timestamp_millis(); 1749 let handle = format!("deactivate-{}.test", ts); 1750 let email = format!("deactivate-{}@test.com", ts); 1751 let password = "deactivate-password"; 1752 1753 let create_res = client 1754 .post(format!( 1755 "{}/xrpc/com.atproto.server.createAccount", 1756 base_url().await 1757 )) 1758 .json(&json!({ 1759 "handle": handle, 1760 "email": email, 1761 "password": password 1762 })) 1763 .send() 1764 .await 1765 .expect("Failed to create account"); 1766 1767 assert_eq!(create_res.status(), StatusCode::OK); 1768 let account: Value = create_res.json().await.unwrap(); 1769 let did = account["did"].as_str().unwrap().to_string(); 1770 let jwt = account["accessJwt"].as_str().unwrap().to_string(); 1771 1772 let (post_uri, _) = create_post(&client, &did, &jwt, "Post before deactivation").await; 1773 let post_rkey = post_uri.split('/').last().unwrap(); 1774 1775 let status_before = client 1776 .get(format!( 1777 "{}/xrpc/com.atproto.server.checkAccountStatus", 1778 base_url().await 1779 )) 1780 .bearer_auth(&jwt) 1781 .send() 1782 .await 1783 .expect("Failed to check status"); 1784 1785 assert_eq!(status_before.status(), StatusCode::OK); 1786 let status_body: Value = status_before.json().await.unwrap(); 1787 assert_eq!(status_body["activated"], true); 1788 1789 let deactivate_res = client 1790 .post(format!( 1791 "{}/xrpc/com.atproto.server.deactivateAccount", 1792 base_url().await 1793 )) 1794 .bearer_auth(&jwt) 1795 .json(&json!({})) 1796 .send() 1797 .await 1798 .expect("Failed to deactivate"); 1799 1800 assert_eq!(deactivate_res.status(), StatusCode::OK); 1801 1802 let get_post_res = client 1803 .get(format!( 1804 "{}/xrpc/com.atproto.repo.getRecord", 1805 base_url().await 1806 )) 1807 .query(&[ 1808 ("repo", did.as_str()), 1809 ("collection", "app.bsky.feed.post"), 1810 ("rkey", post_rkey), 1811 ]) 1812 .send() 1813 .await 1814 .expect("Failed to get post while deactivated"); 1815 1816 assert_eq!(get_post_res.status(), StatusCode::OK, "Records should still be readable"); 1817 1818 let activate_res = client 1819 .post(format!( 1820 "{}/xrpc/com.atproto.server.activateAccount", 1821 base_url().await 1822 )) 1823 .bearer_auth(&jwt) 1824 .json(&json!({})) 1825 .send() 1826 .await 1827 .expect("Failed to reactivate"); 1828 1829 assert_eq!(activate_res.status(), StatusCode::OK); 1830 1831 let status_after_activate = client 1832 .get(format!( 1833 "{}/xrpc/com.atproto.server.checkAccountStatus", 1834 base_url().await 1835 )) 1836 .bearer_auth(&jwt) 1837 .send() 1838 .await 1839 .expect("Failed to check status after activate"); 1840 1841 assert_eq!(status_after_activate.status(), StatusCode::OK); 1842 1843 let (new_post_uri, _) = create_post(&client, &did, &jwt, "Post after reactivation").await; 1844 assert!(!new_post_uri.is_empty(), "Should be able to post after reactivation"); 1845} 1846 1847#[tokio::test] 1848async fn test_sync_record_lifecycle() { 1849 let client = client(); 1850 let (did, jwt) = setup_new_user("sync-record-lifecycle").await; 1851 1852 let (post_uri, _post_cid) = 1853 create_post(&client, &did, &jwt, "Post for sync record test").await; 1854 let post_rkey = post_uri.split('/').last().unwrap(); 1855 1856 let sync_record_res = client 1857 .get(format!( 1858 "{}/xrpc/com.atproto.sync.getRecord", 1859 base_url().await 1860 )) 1861 .query(&[ 1862 ("did", did.as_str()), 1863 ("collection", "app.bsky.feed.post"), 1864 ("rkey", post_rkey), 1865 ]) 1866 .send() 1867 .await 1868 .expect("Failed to get sync record"); 1869 1870 assert_eq!(sync_record_res.status(), StatusCode::OK); 1871 assert_eq!( 1872 sync_record_res 1873 .headers() 1874 .get("content-type") 1875 .and_then(|h| h.to_str().ok()), 1876 Some("application/vnd.ipld.car") 1877 ); 1878 let car_bytes = sync_record_res.bytes().await.unwrap(); 1879 assert!(!car_bytes.is_empty(), "CAR data should not be empty"); 1880 1881 let latest_before = client 1882 .get(format!( 1883 "{}/xrpc/com.atproto.sync.getLatestCommit", 1884 base_url().await 1885 )) 1886 .query(&[("did", did.as_str())]) 1887 .send() 1888 .await 1889 .expect("Failed to get latest commit"); 1890 let latest_before_body: Value = latest_before.json().await.unwrap(); 1891 let rev_before = latest_before_body["rev"].as_str().unwrap().to_string(); 1892 1893 let (post2_uri, _) = create_post(&client, &did, &jwt, "Second post for sync test").await; 1894 1895 let latest_after = client 1896 .get(format!( 1897 "{}/xrpc/com.atproto.sync.getLatestCommit", 1898 base_url().await 1899 )) 1900 .query(&[("did", did.as_str())]) 1901 .send() 1902 .await 1903 .expect("Failed to get latest commit after"); 1904 let latest_after_body: Value = latest_after.json().await.unwrap(); 1905 let rev_after = latest_after_body["rev"].as_str().unwrap().to_string(); 1906 assert_ne!(rev_before, rev_after, "Revision should change after new record"); 1907 1908 let delete_payload = json!({ 1909 "repo": did, 1910 "collection": "app.bsky.feed.post", 1911 "rkey": post_rkey 1912 }); 1913 let delete_res = client 1914 .post(format!( 1915 "{}/xrpc/com.atproto.repo.deleteRecord", 1916 base_url().await 1917 )) 1918 .bearer_auth(&jwt) 1919 .json(&delete_payload) 1920 .send() 1921 .await 1922 .expect("Failed to delete record"); 1923 assert_eq!(delete_res.status(), StatusCode::OK); 1924 1925 let sync_deleted_res = client 1926 .get(format!( 1927 "{}/xrpc/com.atproto.sync.getRecord", 1928 base_url().await 1929 )) 1930 .query(&[ 1931 ("did", did.as_str()), 1932 ("collection", "app.bsky.feed.post"), 1933 ("rkey", post_rkey), 1934 ]) 1935 .send() 1936 .await 1937 .expect("Failed to check deleted record via sync"); 1938 assert_eq!( 1939 sync_deleted_res.status(), 1940 StatusCode::NOT_FOUND, 1941 "Deleted record should return 404 via sync.getRecord" 1942 ); 1943 1944 let post2_rkey = post2_uri.split('/').last().unwrap(); 1945 let sync_post2_res = client 1946 .get(format!( 1947 "{}/xrpc/com.atproto.sync.getRecord", 1948 base_url().await 1949 )) 1950 .query(&[ 1951 ("did", did.as_str()), 1952 ("collection", "app.bsky.feed.post"), 1953 ("rkey", post2_rkey), 1954 ]) 1955 .send() 1956 .await 1957 .expect("Failed to get second post via sync"); 1958 assert_eq!( 1959 sync_post2_res.status(), 1960 StatusCode::OK, 1961 "Second post should still be accessible" 1962 ); 1963} 1964 1965#[tokio::test] 1966async fn test_sync_repo_export_lifecycle() { 1967 let client = client(); 1968 let (did, jwt) = setup_new_user("sync-repo-export").await; 1969 1970 let profile_payload = json!({ 1971 "repo": did, 1972 "collection": "app.bsky.actor.profile", 1973 "rkey": "self", 1974 "record": { 1975 "$type": "app.bsky.actor.profile", 1976 "displayName": "Sync Export User" 1977 } 1978 }); 1979 let profile_res = client 1980 .post(format!( 1981 "{}/xrpc/com.atproto.repo.putRecord", 1982 base_url().await 1983 )) 1984 .bearer_auth(&jwt) 1985 .json(&profile_payload) 1986 .send() 1987 .await 1988 .expect("Failed to create profile"); 1989 assert_eq!(profile_res.status(), StatusCode::OK); 1990 1991 for i in 0..3 { 1992 tokio::time::sleep(Duration::from_millis(50)).await; 1993 create_post(&client, &did, &jwt, &format!("Export test post {}", i)).await; 1994 } 1995 1996 let blob_data = b"blob data for sync export test"; 1997 let upload_res = client 1998 .post(format!( 1999 "{}/xrpc/com.atproto.repo.uploadBlob", 2000 base_url().await 2001 )) 2002 .header(header::CONTENT_TYPE, "application/octet-stream") 2003 .bearer_auth(&jwt) 2004 .body(blob_data.to_vec()) 2005 .send() 2006 .await 2007 .expect("Failed to upload blob"); 2008 assert_eq!(upload_res.status(), StatusCode::OK); 2009 let blob_body: Value = upload_res.json().await.unwrap(); 2010 let blob_cid = blob_body["blob"]["ref"]["$link"].as_str().unwrap().to_string(); 2011 2012 let repo_status_res = client 2013 .get(format!( 2014 "{}/xrpc/com.atproto.sync.getRepoStatus", 2015 base_url().await 2016 )) 2017 .query(&[("did", did.as_str())]) 2018 .send() 2019 .await 2020 .expect("Failed to get repo status"); 2021 assert_eq!(repo_status_res.status(), StatusCode::OK); 2022 let status_body: Value = repo_status_res.json().await.unwrap(); 2023 assert_eq!(status_body["did"], did); 2024 assert_eq!(status_body["active"], true); 2025 2026 let get_repo_res = client 2027 .get(format!( 2028 "{}/xrpc/com.atproto.sync.getRepo", 2029 base_url().await 2030 )) 2031 .query(&[("did", did.as_str())]) 2032 .send() 2033 .await 2034 .expect("Failed to get full repo"); 2035 assert_eq!(get_repo_res.status(), StatusCode::OK); 2036 assert_eq!( 2037 get_repo_res 2038 .headers() 2039 .get("content-type") 2040 .and_then(|h| h.to_str().ok()), 2041 Some("application/vnd.ipld.car") 2042 ); 2043 let repo_car = get_repo_res.bytes().await.unwrap(); 2044 assert!(repo_car.len() > 100, "Repo CAR should have substantial data"); 2045 2046 let list_blobs_res = client 2047 .get(format!( 2048 "{}/xrpc/com.atproto.sync.listBlobs", 2049 base_url().await 2050 )) 2051 .query(&[("did", did.as_str())]) 2052 .send() 2053 .await 2054 .expect("Failed to list blobs"); 2055 assert_eq!(list_blobs_res.status(), StatusCode::OK); 2056 let blobs_body: Value = list_blobs_res.json().await.unwrap(); 2057 let cids = blobs_body["cids"].as_array().unwrap(); 2058 assert!(!cids.is_empty(), "Should have at least one blob"); 2059 2060 let get_blob_res = client 2061 .get(format!( 2062 "{}/xrpc/com.atproto.sync.getBlob", 2063 base_url().await 2064 )) 2065 .query(&[("did", did.as_str()), ("cid", &blob_cid)]) 2066 .send() 2067 .await 2068 .expect("Failed to get blob"); 2069 assert_eq!(get_blob_res.status(), StatusCode::OK); 2070 let retrieved_blob = get_blob_res.bytes().await.unwrap(); 2071 assert_eq!( 2072 retrieved_blob.as_ref(), 2073 blob_data, 2074 "Retrieved blob should match uploaded data" 2075 ); 2076 2077 let latest_commit_res = client 2078 .get(format!( 2079 "{}/xrpc/com.atproto.sync.getLatestCommit", 2080 base_url().await 2081 )) 2082 .query(&[("did", did.as_str())]) 2083 .send() 2084 .await 2085 .expect("Failed to get latest commit"); 2086 assert_eq!(latest_commit_res.status(), StatusCode::OK); 2087 let commit_body: Value = latest_commit_res.json().await.unwrap(); 2088 let root_cid = commit_body["cid"].as_str().unwrap(); 2089 2090 let get_blocks_url = format!( 2091 "{}/xrpc/com.atproto.sync.getBlocks?did={}&cids={}", 2092 base_url().await, 2093 did, 2094 root_cid 2095 ); 2096 let get_blocks_res = client 2097 .get(&get_blocks_url) 2098 .send() 2099 .await 2100 .expect("Failed to get blocks"); 2101 assert_eq!(get_blocks_res.status(), StatusCode::OK); 2102 assert_eq!( 2103 get_blocks_res 2104 .headers() 2105 .get("content-type") 2106 .and_then(|h| h.to_str().ok()), 2107 Some("application/vnd.ipld.car") 2108 ); 2109} 2110 2111#[tokio::test] 2112async fn test_apply_writes_batch_lifecycle() { 2113 let client = client(); 2114 let (did, jwt) = setup_new_user("apply-writes-batch").await; 2115 2116 let now = Utc::now().to_rfc3339(); 2117 let writes_payload = json!({ 2118 "repo": did, 2119 "writes": [ 2120 { 2121 "$type": "com.atproto.repo.applyWrites#create", 2122 "collection": "app.bsky.feed.post", 2123 "rkey": "batch-post-1", 2124 "value": { 2125 "$type": "app.bsky.feed.post", 2126 "text": "First batch post", 2127 "createdAt": now 2128 } 2129 }, 2130 { 2131 "$type": "com.atproto.repo.applyWrites#create", 2132 "collection": "app.bsky.feed.post", 2133 "rkey": "batch-post-2", 2134 "value": { 2135 "$type": "app.bsky.feed.post", 2136 "text": "Second batch post", 2137 "createdAt": now 2138 } 2139 }, 2140 { 2141 "$type": "com.atproto.repo.applyWrites#create", 2142 "collection": "app.bsky.actor.profile", 2143 "rkey": "self", 2144 "value": { 2145 "$type": "app.bsky.actor.profile", 2146 "displayName": "Batch User" 2147 } 2148 } 2149 ] 2150 }); 2151 2152 let apply_res = client 2153 .post(format!( 2154 "{}/xrpc/com.atproto.repo.applyWrites", 2155 base_url().await 2156 )) 2157 .bearer_auth(&jwt) 2158 .json(&writes_payload) 2159 .send() 2160 .await 2161 .expect("Failed to apply writes"); 2162 2163 assert_eq!(apply_res.status(), StatusCode::OK); 2164 2165 let get_post1 = client 2166 .get(format!( 2167 "{}/xrpc/com.atproto.repo.getRecord", 2168 base_url().await 2169 )) 2170 .query(&[ 2171 ("repo", did.as_str()), 2172 ("collection", "app.bsky.feed.post"), 2173 ("rkey", "batch-post-1"), 2174 ]) 2175 .send() 2176 .await 2177 .expect("Failed to get post 1"); 2178 assert_eq!(get_post1.status(), StatusCode::OK); 2179 let post1_body: Value = get_post1.json().await.unwrap(); 2180 assert_eq!(post1_body["value"]["text"], "First batch post"); 2181 2182 let get_post2 = client 2183 .get(format!( 2184 "{}/xrpc/com.atproto.repo.getRecord", 2185 base_url().await 2186 )) 2187 .query(&[ 2188 ("repo", did.as_str()), 2189 ("collection", "app.bsky.feed.post"), 2190 ("rkey", "batch-post-2"), 2191 ]) 2192 .send() 2193 .await 2194 .expect("Failed to get post 2"); 2195 assert_eq!(get_post2.status(), StatusCode::OK); 2196 2197 let get_profile = client 2198 .get(format!( 2199 "{}/xrpc/com.atproto.repo.getRecord", 2200 base_url().await 2201 )) 2202 .query(&[ 2203 ("repo", did.as_str()), 2204 ("collection", "app.bsky.actor.profile"), 2205 ("rkey", "self"), 2206 ]) 2207 .send() 2208 .await 2209 .expect("Failed to get profile"); 2210 assert_eq!(get_profile.status(), StatusCode::OK); 2211 let profile_body: Value = get_profile.json().await.unwrap(); 2212 assert_eq!(profile_body["value"]["displayName"], "Batch User"); 2213 2214 let update_writes = json!({ 2215 "repo": did, 2216 "writes": [ 2217 { 2218 "$type": "com.atproto.repo.applyWrites#update", 2219 "collection": "app.bsky.actor.profile", 2220 "rkey": "self", 2221 "value": { 2222 "$type": "app.bsky.actor.profile", 2223 "displayName": "Updated Batch User" 2224 } 2225 }, 2226 { 2227 "$type": "com.atproto.repo.applyWrites#delete", 2228 "collection": "app.bsky.feed.post", 2229 "rkey": "batch-post-1" 2230 } 2231 ] 2232 }); 2233 2234 let update_res = client 2235 .post(format!( 2236 "{}/xrpc/com.atproto.repo.applyWrites", 2237 base_url().await 2238 )) 2239 .bearer_auth(&jwt) 2240 .json(&update_writes) 2241 .send() 2242 .await 2243 .expect("Failed to apply update writes"); 2244 assert_eq!(update_res.status(), StatusCode::OK); 2245 2246 let get_updated_profile = client 2247 .get(format!( 2248 "{}/xrpc/com.atproto.repo.getRecord", 2249 base_url().await 2250 )) 2251 .query(&[ 2252 ("repo", did.as_str()), 2253 ("collection", "app.bsky.actor.profile"), 2254 ("rkey", "self"), 2255 ]) 2256 .send() 2257 .await 2258 .expect("Failed to get updated profile"); 2259 let updated_profile: Value = get_updated_profile.json().await.unwrap(); 2260 assert_eq!(updated_profile["value"]["displayName"], "Updated Batch User"); 2261 2262 let get_deleted_post = client 2263 .get(format!( 2264 "{}/xrpc/com.atproto.repo.getRecord", 2265 base_url().await 2266 )) 2267 .query(&[ 2268 ("repo", did.as_str()), 2269 ("collection", "app.bsky.feed.post"), 2270 ("rkey", "batch-post-1"), 2271 ]) 2272 .send() 2273 .await 2274 .expect("Failed to check deleted post"); 2275 assert_eq!( 2276 get_deleted_post.status(), 2277 StatusCode::NOT_FOUND, 2278 "Batch-deleted post should be gone" 2279 ); 2280} 2281 2282#[tokio::test] 2283async fn test_resolve_handle_lifecycle() { 2284 let client = client(); 2285 let ts = Utc::now().timestamp_millis(); 2286 let handle = format!("resolve-test-{}.test", ts); 2287 let email = format!("resolve-test-{}@test.com", ts); 2288 2289 let create_res = client 2290 .post(format!( 2291 "{}/xrpc/com.atproto.server.createAccount", 2292 base_url().await 2293 )) 2294 .json(&json!({ 2295 "handle": handle, 2296 "email": email, 2297 "password": "resolve-test-pw" 2298 })) 2299 .send() 2300 .await 2301 .expect("Failed to create account"); 2302 assert_eq!(create_res.status(), StatusCode::OK); 2303 let account: Value = create_res.json().await.unwrap(); 2304 let did = account["did"].as_str().unwrap(); 2305 2306 let resolve_res = client 2307 .get(format!( 2308 "{}/xrpc/com.atproto.identity.resolveHandle", 2309 base_url().await 2310 )) 2311 .query(&[("handle", handle.as_str())]) 2312 .send() 2313 .await 2314 .expect("Failed to resolve handle"); 2315 2316 assert_eq!(resolve_res.status(), StatusCode::OK); 2317 let resolve_body: Value = resolve_res.json().await.unwrap(); 2318 assert_eq!(resolve_body["did"], did); 2319} 2320 2321#[tokio::test] 2322async fn test_service_auth_lifecycle() { 2323 let client = client(); 2324 let (did, jwt) = setup_new_user("service-auth-test").await; 2325 2326 let service_auth_res = client 2327 .get(format!( 2328 "{}/xrpc/com.atproto.server.getServiceAuth", 2329 base_url().await 2330 )) 2331 .query(&[ 2332 ("aud", "did:web:api.bsky.app"), 2333 ("lxm", "com.atproto.repo.uploadBlob"), 2334 ]) 2335 .bearer_auth(&jwt) 2336 .send() 2337 .await 2338 .expect("Failed to get service auth"); 2339 2340 assert_eq!(service_auth_res.status(), StatusCode::OK); 2341 let auth_body: Value = service_auth_res.json().await.unwrap(); 2342 let service_token = auth_body["token"].as_str().expect("No token in response"); 2343 2344 let parts: Vec<&str> = service_token.split('.').collect(); 2345 assert_eq!(parts.len(), 3, "Service token should be a valid JWT"); 2346 2347 let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD 2348 .decode(parts[1]) 2349 .expect("Failed to decode JWT payload"); 2350 let claims: Value = serde_json::from_slice(&payload_bytes).expect("Invalid JWT payload"); 2351 2352 assert_eq!(claims["iss"], did); 2353 assert_eq!(claims["aud"], "did:web:api.bsky.app"); 2354 assert_eq!(claims["lxm"], "com.atproto.repo.uploadBlob"); 2355} 2356 2357#[tokio::test] 2358async fn test_moderation_report_lifecycle() { 2359 let client = client(); 2360 let (alice_did, alice_jwt) = setup_new_user("alice-report").await; 2361 let (bob_did, bob_jwt) = setup_new_user("bob-report").await; 2362 2363 let (post_uri, post_cid) = 2364 create_post(&client, &bob_did, &bob_jwt, "This is a reportable post").await; 2365 2366 let report_payload = json!({ 2367 "reasonType": "com.atproto.moderation.defs#reasonSpam", 2368 "reason": "This looks like spam to me", 2369 "subject": { 2370 "$type": "com.atproto.repo.strongRef", 2371 "uri": post_uri, 2372 "cid": post_cid 2373 } 2374 }); 2375 2376 let report_res = client 2377 .post(format!( 2378 "{}/xrpc/com.atproto.moderation.createReport", 2379 base_url().await 2380 )) 2381 .bearer_auth(&alice_jwt) 2382 .json(&report_payload) 2383 .send() 2384 .await 2385 .expect("Failed to create report"); 2386 2387 assert_eq!(report_res.status(), StatusCode::OK); 2388 let report_body: Value = report_res.json().await.unwrap(); 2389 assert!(report_body["id"].is_number(), "Report should have an ID"); 2390 assert_eq!(report_body["reasonType"], "com.atproto.moderation.defs#reasonSpam"); 2391 assert_eq!(report_body["reportedBy"], alice_did); 2392 2393 let account_report_payload = json!({ 2394 "reasonType": "com.atproto.moderation.defs#reasonOther", 2395 "reason": "Suspicious account activity", 2396 "subject": { 2397 "$type": "com.atproto.admin.defs#repoRef", 2398 "did": bob_did 2399 } 2400 }); 2401 2402 let account_report_res = client 2403 .post(format!( 2404 "{}/xrpc/com.atproto.moderation.createReport", 2405 base_url().await 2406 )) 2407 .bearer_auth(&alice_jwt) 2408 .json(&account_report_payload) 2409 .send() 2410 .await 2411 .expect("Failed to create account report"); 2412 2413 assert_eq!(account_report_res.status(), StatusCode::OK); 2414}