this repo has no description
1mod common; 2use common::*; 3 4use chrono::Utc; 5use reqwest::{self, StatusCode, header}; 6use serde_json::{Value, json}; 7use std::time::Duration; 8 9async fn setup_new_user(handle_prefix: &str) -> (String, String) { 10 let client = client(); 11 let ts = Utc::now().timestamp_millis(); 12 let handle = format!("{}-{}.test", handle_prefix, ts); 13 let email = format!("{}-{}@test.com", handle_prefix, ts); 14 let password = "e2e-password-123"; 15 16 let create_account_payload = json!({ 17 "handle": handle, 18 "email": email, 19 "password": password 20 }); 21 let create_res = client 22 .post(format!( 23 "{}/xrpc/com.atproto.server.createAccount", 24 base_url().await 25 )) 26 .json(&create_account_payload) 27 .send() 28 .await 29 .expect("setup_new_user: Failed to send createAccount"); 30 31 if create_res.status() != reqwest::StatusCode::OK { 32 panic!( 33 "setup_new_user: Failed to create account: {:?}", 34 create_res.text().await 35 ); 36 } 37 38 let create_body: Value = create_res 39 .json() 40 .await 41 .expect("setup_new_user: createAccount response was not JSON"); 42 43 let new_did = create_body["did"] 44 .as_str() 45 .expect("setup_new_user: Response had no DID") 46 .to_string(); 47 let new_jwt = create_body["accessJwt"] 48 .as_str() 49 .expect("setup_new_user: Response had no accessJwt") 50 .to_string(); 51 52 (new_did, new_jwt) 53} 54 55#[tokio::test] 56async fn test_post_crud_lifecycle() { 57 let client = client(); 58 let (did, jwt) = setup_new_user("lifecycle-crud").await; 59 let collection = "app.bsky.feed.post"; 60 61 let rkey = format!("e2e_lifecycle_{}", Utc::now().timestamp_millis()); 62 let now = Utc::now().to_rfc3339(); 63 64 let original_text = "Hello from the lifecycle test!"; 65 let create_payload = json!({ 66 "repo": did, 67 "collection": collection, 68 "rkey": rkey, 69 "record": { 70 "$type": collection, 71 "text": original_text, 72 "createdAt": now 73 } 74 }); 75 76 let create_res = client 77 .post(format!( 78 "{}/xrpc/com.atproto.repo.putRecord", 79 base_url().await 80 )) 81 .bearer_auth(&jwt) 82 .json(&create_payload) 83 .send() 84 .await 85 .expect("Failed to send create request"); 86 87 if create_res.status() != reqwest::StatusCode::OK { 88 let status = create_res.status(); 89 let body = create_res 90 .text() 91 .await 92 .unwrap_or_else(|_| "Could not get body".to_string()); 93 panic!( 94 "Failed to create record. Status: {}, Body: {}", 95 status, body 96 ); 97 } 98 99 let create_body: Value = create_res 100 .json() 101 .await 102 .expect("create response was not JSON"); 103 let uri = create_body["uri"].as_str().unwrap(); 104 105 let params = [ 106 ("repo", did.as_str()), 107 ("collection", collection), 108 ("rkey", &rkey), 109 ]; 110 let get_res = client 111 .get(format!( 112 "{}/xrpc/com.atproto.repo.getRecord", 113 base_url().await 114 )) 115 .query(&params) 116 .send() 117 .await 118 .expect("Failed to send get request"); 119 120 assert_eq!( 121 get_res.status(), 122 reqwest::StatusCode::OK, 123 "Failed to get record after create" 124 ); 125 let get_body: Value = get_res.json().await.expect("get response was not JSON"); 126 assert_eq!(get_body["uri"], uri); 127 assert_eq!(get_body["value"]["text"], original_text); 128 129 let updated_text = "This post has been updated."; 130 let update_payload = json!({ 131 "repo": did, 132 "collection": collection, 133 "rkey": rkey, 134 "record": { 135 "$type": collection, 136 "text": updated_text, 137 "createdAt": now 138 } 139 }); 140 141 let update_res = client 142 .post(format!( 143 "{}/xrpc/com.atproto.repo.putRecord", 144 base_url().await 145 )) 146 .bearer_auth(&jwt) 147 .json(&update_payload) 148 .send() 149 .await 150 .expect("Failed to send update request"); 151 152 assert_eq!( 153 update_res.status(), 154 reqwest::StatusCode::OK, 155 "Failed to update record" 156 ); 157 158 let get_updated_res = client 159 .get(format!( 160 "{}/xrpc/com.atproto.repo.getRecord", 161 base_url().await 162 )) 163 .query(&params) 164 .send() 165 .await 166 .expect("Failed to send get-after-update request"); 167 168 assert_eq!( 169 get_updated_res.status(), 170 reqwest::StatusCode::OK, 171 "Failed to get record after update" 172 ); 173 let get_updated_body: Value = get_updated_res 174 .json() 175 .await 176 .expect("get-updated response was not JSON"); 177 assert_eq!( 178 get_updated_body["value"]["text"], updated_text, 179 "Text was not updated" 180 ); 181 182 let delete_payload = json!({ 183 "repo": did, 184 "collection": collection, 185 "rkey": rkey 186 }); 187 188 let delete_res = client 189 .post(format!( 190 "{}/xrpc/com.atproto.repo.deleteRecord", 191 base_url().await 192 )) 193 .bearer_auth(&jwt) 194 .json(&delete_payload) 195 .send() 196 .await 197 .expect("Failed to send delete request"); 198 199 assert_eq!( 200 delete_res.status(), 201 reqwest::StatusCode::OK, 202 "Failed to delete record" 203 ); 204 205 let get_deleted_res = client 206 .get(format!( 207 "{}/xrpc/com.atproto.repo.getRecord", 208 base_url().await 209 )) 210 .query(&params) 211 .send() 212 .await 213 .expect("Failed to send get-after-delete request"); 214 215 assert_eq!( 216 get_deleted_res.status(), 217 reqwest::StatusCode::NOT_FOUND, 218 "Record was found, but it should be deleted" 219 ); 220} 221 222#[tokio::test] 223async fn test_record_update_conflict_lifecycle() { 224 let client = client(); 225 let (user_did, user_jwt) = setup_new_user("user-conflict").await; 226 227 let profile_payload = json!({ 228 "repo": user_did, 229 "collection": "app.bsky.actor.profile", 230 "rkey": "self", 231 "record": { 232 "$type": "app.bsky.actor.profile", 233 "displayName": "Original Name" 234 } 235 }); 236 let create_res = client 237 .post(format!( 238 "{}/xrpc/com.atproto.repo.putRecord", 239 base_url().await 240 )) 241 .bearer_auth(&user_jwt) 242 .json(&profile_payload) 243 .send() 244 .await 245 .expect("create profile failed"); 246 247 if create_res.status() != reqwest::StatusCode::OK { 248 return; 249 } 250 251 let get_res = client 252 .get(format!( 253 "{}/xrpc/com.atproto.repo.getRecord", 254 base_url().await 255 )) 256 .query(&[ 257 ("repo", &user_did), 258 ("collection", &"app.bsky.actor.profile".to_string()), 259 ("rkey", &"self".to_string()), 260 ]) 261 .send() 262 .await 263 .expect("getRecord failed"); 264 let get_body: Value = get_res.json().await.expect("getRecord not json"); 265 let cid_v1 = get_body["cid"] 266 .as_str() 267 .expect("Profile v1 had no CID") 268 .to_string(); 269 270 let update_payload_v2 = json!({ 271 "repo": user_did, 272 "collection": "app.bsky.actor.profile", 273 "rkey": "self", 274 "record": { 275 "$type": "app.bsky.actor.profile", 276 "displayName": "Updated Name (v2)" 277 }, 278 "swapRecord": cid_v1 279 }); 280 let update_res_v2 = client 281 .post(format!( 282 "{}/xrpc/com.atproto.repo.putRecord", 283 base_url().await 284 )) 285 .bearer_auth(&user_jwt) 286 .json(&update_payload_v2) 287 .send() 288 .await 289 .expect("putRecord v2 failed"); 290 assert_eq!( 291 update_res_v2.status(), 292 reqwest::StatusCode::OK, 293 "v2 update failed" 294 ); 295 let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json"); 296 let cid_v2 = update_body_v2["cid"] 297 .as_str() 298 .expect("v2 response had no CID") 299 .to_string(); 300 301 let update_payload_v3_stale = json!({ 302 "repo": user_did, 303 "collection": "app.bsky.actor.profile", 304 "rkey": "self", 305 "record": { 306 "$type": "app.bsky.actor.profile", 307 "displayName": "Stale Update (v3)" 308 }, 309 "swapRecord": cid_v1 310 }); 311 let update_res_v3_stale = client 312 .post(format!( 313 "{}/xrpc/com.atproto.repo.putRecord", 314 base_url().await 315 )) 316 .bearer_auth(&user_jwt) 317 .json(&update_payload_v3_stale) 318 .send() 319 .await 320 .expect("putRecord v3 (stale) failed"); 321 322 assert_eq!( 323 update_res_v3_stale.status(), 324 reqwest::StatusCode::CONFLICT, 325 "Stale update did not cause a 409 Conflict" 326 ); 327 328 let update_payload_v3_good = json!({ 329 "repo": user_did, 330 "collection": "app.bsky.actor.profile", 331 "rkey": "self", 332 "record": { 333 "$type": "app.bsky.actor.profile", 334 "displayName": "Good Update (v3)" 335 }, 336 "swapRecord": cid_v2 337 }); 338 let update_res_v3_good = client 339 .post(format!( 340 "{}/xrpc/com.atproto.repo.putRecord", 341 base_url().await 342 )) 343 .bearer_auth(&user_jwt) 344 .json(&update_payload_v3_good) 345 .send() 346 .await 347 .expect("putRecord v3 (good) failed"); 348 349 assert_eq!( 350 update_res_v3_good.status(), 351 reqwest::StatusCode::OK, 352 "v3 (good) update failed" 353 ); 354} 355 356async fn create_post( 357 client: &reqwest::Client, 358 did: &str, 359 jwt: &str, 360 text: &str, 361) -> (String, String) { 362 let collection = "app.bsky.feed.post"; 363 let rkey = format!("e2e_social_{}", Utc::now().timestamp_millis()); 364 let now = Utc::now().to_rfc3339(); 365 366 let create_payload = json!({ 367 "repo": did, 368 "collection": collection, 369 "rkey": rkey, 370 "record": { 371 "$type": collection, 372 "text": text, 373 "createdAt": now 374 } 375 }); 376 377 let create_res = client 378 .post(format!( 379 "{}/xrpc/com.atproto.repo.putRecord", 380 base_url().await 381 )) 382 .bearer_auth(jwt) 383 .json(&create_payload) 384 .send() 385 .await 386 .expect("Failed to send create post request"); 387 388 assert_eq!( 389 create_res.status(), 390 reqwest::StatusCode::OK, 391 "Failed to create post record" 392 ); 393 let create_body: Value = create_res 394 .json() 395 .await 396 .expect("create post response was not JSON"); 397 let uri = create_body["uri"].as_str().unwrap().to_string(); 398 let cid = create_body["cid"].as_str().unwrap().to_string(); 399 (uri, cid) 400} 401 402async fn create_follow( 403 client: &reqwest::Client, 404 follower_did: &str, 405 follower_jwt: &str, 406 followee_did: &str, 407) -> (String, String) { 408 let collection = "app.bsky.graph.follow"; 409 let rkey = format!("e2e_follow_{}", Utc::now().timestamp_millis()); 410 let now = Utc::now().to_rfc3339(); 411 412 let create_payload = json!({ 413 "repo": follower_did, 414 "collection": collection, 415 "rkey": rkey, 416 "record": { 417 "$type": collection, 418 "subject": followee_did, 419 "createdAt": now 420 } 421 }); 422 423 let create_res = client 424 .post(format!( 425 "{}/xrpc/com.atproto.repo.putRecord", 426 base_url().await 427 )) 428 .bearer_auth(follower_jwt) 429 .json(&create_payload) 430 .send() 431 .await 432 .expect("Failed to send create follow request"); 433 434 assert_eq!( 435 create_res.status(), 436 reqwest::StatusCode::OK, 437 "Failed to create follow record" 438 ); 439 let create_body: Value = create_res 440 .json() 441 .await 442 .expect("create follow response was not JSON"); 443 let uri = create_body["uri"].as_str().unwrap().to_string(); 444 let cid = create_body["cid"].as_str().unwrap().to_string(); 445 (uri, cid) 446} 447 448#[tokio::test] 449async fn test_social_flow_lifecycle() { 450 let client = client(); 451 452 let (alice_did, alice_jwt) = setup_new_user("alice-social").await; 453 let (bob_did, bob_jwt) = setup_new_user("bob-social").await; 454 455 let (post1_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's first post!").await; 456 457 create_follow(&client, &bob_did, &bob_jwt, &alice_did).await; 458 459 tokio::time::sleep(Duration::from_secs(1)).await; 460 461 let timeline_res_1 = client 462 .get(format!( 463 "{}/xrpc/app.bsky.feed.getTimeline", 464 base_url().await 465 )) 466 .bearer_auth(&bob_jwt) 467 .send() 468 .await 469 .expect("Failed to get timeline (1)"); 470 471 assert_eq!( 472 timeline_res_1.status(), 473 reqwest::StatusCode::OK, 474 "Failed to get timeline (1)" 475 ); 476 let timeline_body_1: Value = timeline_res_1.json().await.expect("Timeline (1) not JSON"); 477 let feed_1 = timeline_body_1["feed"].as_array().unwrap(); 478 assert_eq!(feed_1.len(), 1, "Timeline should have 1 post"); 479 assert_eq!( 480 feed_1[0]["post"]["uri"], post1_uri, 481 "Post URI mismatch in timeline (1)" 482 ); 483 484 let (post2_uri, _) = create_post( 485 &client, 486 &alice_did, 487 &alice_jwt, 488 "Alice's second post, so exciting!", 489 ) 490 .await; 491 492 tokio::time::sleep(Duration::from_secs(1)).await; 493 494 let timeline_res_2 = client 495 .get(format!( 496 "{}/xrpc/app.bsky.feed.getTimeline", 497 base_url().await 498 )) 499 .bearer_auth(&bob_jwt) 500 .send() 501 .await 502 .expect("Failed to get timeline (2)"); 503 504 assert_eq!( 505 timeline_res_2.status(), 506 reqwest::StatusCode::OK, 507 "Failed to get timeline (2)" 508 ); 509 let timeline_body_2: Value = timeline_res_2.json().await.expect("Timeline (2) not JSON"); 510 let feed_2 = timeline_body_2["feed"].as_array().unwrap(); 511 assert_eq!(feed_2.len(), 2, "Timeline should have 2 posts"); 512 assert_eq!( 513 feed_2[0]["post"]["uri"], post2_uri, 514 "Post 2 should be first" 515 ); 516 assert_eq!( 517 feed_2[1]["post"]["uri"], post1_uri, 518 "Post 1 should be second" 519 ); 520 521 let delete_payload = json!({ 522 "repo": alice_did, 523 "collection": "app.bsky.feed.post", 524 "rkey": post1_uri.split('/').last().unwrap() 525 }); 526 let delete_res = client 527 .post(format!( 528 "{}/xrpc/com.atproto.repo.deleteRecord", 529 base_url().await 530 )) 531 .bearer_auth(&alice_jwt) 532 .json(&delete_payload) 533 .send() 534 .await 535 .expect("Failed to send delete request"); 536 assert_eq!( 537 delete_res.status(), 538 reqwest::StatusCode::OK, 539 "Failed to delete record" 540 ); 541 542 tokio::time::sleep(Duration::from_secs(1)).await; 543 544 let timeline_res_3 = client 545 .get(format!( 546 "{}/xrpc/app.bsky.feed.getTimeline", 547 base_url().await 548 )) 549 .bearer_auth(&bob_jwt) 550 .send() 551 .await 552 .expect("Failed to get timeline (3)"); 553 554 assert_eq!( 555 timeline_res_3.status(), 556 reqwest::StatusCode::OK, 557 "Failed to get timeline (3)" 558 ); 559 let timeline_body_3: Value = timeline_res_3.json().await.expect("Timeline (3) not JSON"); 560 let feed_3 = timeline_body_3["feed"].as_array().unwrap(); 561 assert_eq!(feed_3.len(), 1, "Timeline should have 1 post after delete"); 562 assert_eq!( 563 feed_3[0]["post"]["uri"], post2_uri, 564 "Only post 2 should remain" 565 ); 566} 567 568#[tokio::test] 569async fn test_session_lifecycle_wrong_password() { 570 let client = client(); 571 let (_, _) = setup_new_user("session-wrong-pw").await; 572 573 let login_payload = json!({ 574 "identifier": format!("session-wrong-pw-{}.test", Utc::now().timestamp_millis()), 575 "password": "wrong-password" 576 }); 577 578 let res = client 579 .post(format!( 580 "{}/xrpc/com.atproto.server.createSession", 581 base_url().await 582 )) 583 .json(&login_payload) 584 .send() 585 .await 586 .expect("Failed to send request"); 587 588 assert!( 589 res.status() == StatusCode::UNAUTHORIZED || res.status() == StatusCode::BAD_REQUEST, 590 "Expected 401 or 400 for wrong password, got {}", 591 res.status() 592 ); 593} 594 595#[tokio::test] 596async fn test_session_lifecycle_multiple_sessions() { 597 let client = client(); 598 let ts = Utc::now().timestamp_millis(); 599 let handle = format!("multi-session-{}.test", ts); 600 let email = format!("multi-session-{}@test.com", ts); 601 let password = "multi-session-pw"; 602 603 let create_payload = json!({ 604 "handle": handle, 605 "email": email, 606 "password": password 607 }); 608 let create_res = client 609 .post(format!( 610 "{}/xrpc/com.atproto.server.createAccount", 611 base_url().await 612 )) 613 .json(&create_payload) 614 .send() 615 .await 616 .expect("Failed to create account"); 617 assert_eq!(create_res.status(), StatusCode::OK); 618 619 let login_payload = json!({ 620 "identifier": handle, 621 "password": password 622 }); 623 624 let session1_res = client 625 .post(format!( 626 "{}/xrpc/com.atproto.server.createSession", 627 base_url().await 628 )) 629 .json(&login_payload) 630 .send() 631 .await 632 .expect("Failed session 1"); 633 assert_eq!(session1_res.status(), StatusCode::OK); 634 let session1: Value = session1_res.json().await.unwrap(); 635 let jwt1 = session1["accessJwt"].as_str().unwrap(); 636 637 let session2_res = client 638 .post(format!( 639 "{}/xrpc/com.atproto.server.createSession", 640 base_url().await 641 )) 642 .json(&login_payload) 643 .send() 644 .await 645 .expect("Failed session 2"); 646 assert_eq!(session2_res.status(), StatusCode::OK); 647 let session2: Value = session2_res.json().await.unwrap(); 648 let jwt2 = session2["accessJwt"].as_str().unwrap(); 649 650 assert_ne!(jwt1, jwt2, "Sessions should have different tokens"); 651 652 let get1 = client 653 .get(format!( 654 "{}/xrpc/com.atproto.server.getSession", 655 base_url().await 656 )) 657 .bearer_auth(jwt1) 658 .send() 659 .await 660 .expect("Failed getSession 1"); 661 assert_eq!(get1.status(), StatusCode::OK); 662 663 let get2 = client 664 .get(format!( 665 "{}/xrpc/com.atproto.server.getSession", 666 base_url().await 667 )) 668 .bearer_auth(jwt2) 669 .send() 670 .await 671 .expect("Failed getSession 2"); 672 assert_eq!(get2.status(), StatusCode::OK); 673} 674 675#[tokio::test] 676async fn test_session_lifecycle_refresh_invalidates_old() { 677 let client = client(); 678 let ts = Utc::now().timestamp_millis(); 679 let handle = format!("refresh-inv-{}.test", ts); 680 let email = format!("refresh-inv-{}@test.com", ts); 681 let password = "refresh-inv-pw"; 682 683 let create_payload = json!({ 684 "handle": handle, 685 "email": email, 686 "password": password 687 }); 688 client 689 .post(format!( 690 "{}/xrpc/com.atproto.server.createAccount", 691 base_url().await 692 )) 693 .json(&create_payload) 694 .send() 695 .await 696 .expect("Failed to create account"); 697 698 let login_payload = json!({ 699 "identifier": handle, 700 "password": password 701 }); 702 let login_res = client 703 .post(format!( 704 "{}/xrpc/com.atproto.server.createSession", 705 base_url().await 706 )) 707 .json(&login_payload) 708 .send() 709 .await 710 .expect("Failed login"); 711 let login_body: Value = login_res.json().await.unwrap(); 712 let refresh_jwt = login_body["refreshJwt"].as_str().unwrap().to_string(); 713 714 let refresh_res = client 715 .post(format!( 716 "{}/xrpc/com.atproto.server.refreshSession", 717 base_url().await 718 )) 719 .bearer_auth(&refresh_jwt) 720 .send() 721 .await 722 .expect("Failed first refresh"); 723 assert_eq!(refresh_res.status(), StatusCode::OK); 724 let refresh_body: Value = refresh_res.json().await.unwrap(); 725 let new_refresh_jwt = refresh_body["refreshJwt"].as_str().unwrap(); 726 727 assert_ne!(refresh_jwt, new_refresh_jwt, "Refresh tokens should differ"); 728 729 let reuse_res = client 730 .post(format!( 731 "{}/xrpc/com.atproto.server.refreshSession", 732 base_url().await 733 )) 734 .bearer_auth(&refresh_jwt) 735 .send() 736 .await 737 .expect("Failed reuse attempt"); 738 739 assert!( 740 reuse_res.status() == StatusCode::UNAUTHORIZED || reuse_res.status() == StatusCode::BAD_REQUEST, 741 "Old refresh token should be invalid after use" 742 ); 743} 744 745async fn create_like( 746 client: &reqwest::Client, 747 liker_did: &str, 748 liker_jwt: &str, 749 subject_uri: &str, 750 subject_cid: &str, 751) -> (String, String) { 752 let collection = "app.bsky.feed.like"; 753 let rkey = format!("e2e_like_{}", Utc::now().timestamp_millis()); 754 let now = Utc::now().to_rfc3339(); 755 756 let payload = json!({ 757 "repo": liker_did, 758 "collection": collection, 759 "rkey": rkey, 760 "record": { 761 "$type": collection, 762 "subject": { 763 "uri": subject_uri, 764 "cid": subject_cid 765 }, 766 "createdAt": now 767 } 768 }); 769 770 let res = client 771 .post(format!( 772 "{}/xrpc/com.atproto.repo.putRecord", 773 base_url().await 774 )) 775 .bearer_auth(liker_jwt) 776 .json(&payload) 777 .send() 778 .await 779 .expect("Failed to create like"); 780 781 assert_eq!(res.status(), StatusCode::OK, "Failed to create like"); 782 let body: Value = res.json().await.expect("Like response not JSON"); 783 ( 784 body["uri"].as_str().unwrap().to_string(), 785 body["cid"].as_str().unwrap().to_string(), 786 ) 787} 788 789async fn create_repost( 790 client: &reqwest::Client, 791 reposter_did: &str, 792 reposter_jwt: &str, 793 subject_uri: &str, 794 subject_cid: &str, 795) -> (String, String) { 796 let collection = "app.bsky.feed.repost"; 797 let rkey = format!("e2e_repost_{}", Utc::now().timestamp_millis()); 798 let now = Utc::now().to_rfc3339(); 799 800 let payload = json!({ 801 "repo": reposter_did, 802 "collection": collection, 803 "rkey": rkey, 804 "record": { 805 "$type": collection, 806 "subject": { 807 "uri": subject_uri, 808 "cid": subject_cid 809 }, 810 "createdAt": now 811 } 812 }); 813 814 let res = client 815 .post(format!( 816 "{}/xrpc/com.atproto.repo.putRecord", 817 base_url().await 818 )) 819 .bearer_auth(reposter_jwt) 820 .json(&payload) 821 .send() 822 .await 823 .expect("Failed to create repost"); 824 825 assert_eq!(res.status(), StatusCode::OK, "Failed to create repost"); 826 let body: Value = res.json().await.expect("Repost response not JSON"); 827 ( 828 body["uri"].as_str().unwrap().to_string(), 829 body["cid"].as_str().unwrap().to_string(), 830 ) 831} 832 833#[tokio::test] 834async fn test_profile_lifecycle() { 835 let client = client(); 836 let (did, jwt) = setup_new_user("profile-lifecycle").await; 837 838 let profile_payload = json!({ 839 "repo": did, 840 "collection": "app.bsky.actor.profile", 841 "rkey": "self", 842 "record": { 843 "$type": "app.bsky.actor.profile", 844 "displayName": "Test User", 845 "description": "A test profile for lifecycle testing" 846 } 847 }); 848 849 let create_res = client 850 .post(format!( 851 "{}/xrpc/com.atproto.repo.putRecord", 852 base_url().await 853 )) 854 .bearer_auth(&jwt) 855 .json(&profile_payload) 856 .send() 857 .await 858 .expect("Failed to create profile"); 859 860 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile"); 861 let create_body: Value = create_res.json().await.unwrap(); 862 let initial_cid = create_body["cid"].as_str().unwrap().to_string(); 863 864 let get_res = client 865 .get(format!( 866 "{}/xrpc/com.atproto.repo.getRecord", 867 base_url().await 868 )) 869 .query(&[ 870 ("repo", did.as_str()), 871 ("collection", "app.bsky.actor.profile"), 872 ("rkey", "self"), 873 ]) 874 .send() 875 .await 876 .expect("Failed to get profile"); 877 878 assert_eq!(get_res.status(), StatusCode::OK); 879 let get_body: Value = get_res.json().await.unwrap(); 880 assert_eq!(get_body["value"]["displayName"], "Test User"); 881 assert_eq!(get_body["value"]["description"], "A test profile for lifecycle testing"); 882 883 let update_payload = json!({ 884 "repo": did, 885 "collection": "app.bsky.actor.profile", 886 "rkey": "self", 887 "record": { 888 "$type": "app.bsky.actor.profile", 889 "displayName": "Updated User", 890 "description": "Profile has been updated" 891 }, 892 "swapRecord": initial_cid 893 }); 894 895 let update_res = client 896 .post(format!( 897 "{}/xrpc/com.atproto.repo.putRecord", 898 base_url().await 899 )) 900 .bearer_auth(&jwt) 901 .json(&update_payload) 902 .send() 903 .await 904 .expect("Failed to update profile"); 905 906 assert_eq!(update_res.status(), StatusCode::OK, "Failed to update profile"); 907 908 let get_updated_res = client 909 .get(format!( 910 "{}/xrpc/com.atproto.repo.getRecord", 911 base_url().await 912 )) 913 .query(&[ 914 ("repo", did.as_str()), 915 ("collection", "app.bsky.actor.profile"), 916 ("rkey", "self"), 917 ]) 918 .send() 919 .await 920 .expect("Failed to get updated profile"); 921 922 let updated_body: Value = get_updated_res.json().await.unwrap(); 923 assert_eq!(updated_body["value"]["displayName"], "Updated User"); 924} 925 926#[tokio::test] 927async fn test_reply_thread_lifecycle() { 928 let client = client(); 929 930 let (alice_did, alice_jwt) = setup_new_user("alice-thread").await; 931 let (bob_did, bob_jwt) = setup_new_user("bob-thread").await; 932 933 let (root_uri, root_cid) = create_post(&client, &alice_did, &alice_jwt, "This is the root post").await; 934 935 tokio::time::sleep(Duration::from_millis(100)).await; 936 937 let reply_collection = "app.bsky.feed.post"; 938 let reply_rkey = format!("e2e_reply_{}", Utc::now().timestamp_millis()); 939 let now = Utc::now().to_rfc3339(); 940 941 let reply_payload = json!({ 942 "repo": bob_did, 943 "collection": reply_collection, 944 "rkey": reply_rkey, 945 "record": { 946 "$type": reply_collection, 947 "text": "This is Bob's reply to Alice", 948 "createdAt": now, 949 "reply": { 950 "root": { 951 "uri": root_uri, 952 "cid": root_cid 953 }, 954 "parent": { 955 "uri": root_uri, 956 "cid": root_cid 957 } 958 } 959 } 960 }); 961 962 let reply_res = client 963 .post(format!( 964 "{}/xrpc/com.atproto.repo.putRecord", 965 base_url().await 966 )) 967 .bearer_auth(&bob_jwt) 968 .json(&reply_payload) 969 .send() 970 .await 971 .expect("Failed to create reply"); 972 973 assert_eq!(reply_res.status(), StatusCode::OK, "Failed to create reply"); 974 let reply_body: Value = reply_res.json().await.unwrap(); 975 let reply_uri = reply_body["uri"].as_str().unwrap(); 976 let reply_cid = reply_body["cid"].as_str().unwrap(); 977 978 let get_reply_res = client 979 .get(format!( 980 "{}/xrpc/com.atproto.repo.getRecord", 981 base_url().await 982 )) 983 .query(&[ 984 ("repo", bob_did.as_str()), 985 ("collection", reply_collection), 986 ("rkey", reply_rkey.as_str()), 987 ]) 988 .send() 989 .await 990 .expect("Failed to get reply"); 991 992 assert_eq!(get_reply_res.status(), StatusCode::OK); 993 let reply_record: Value = get_reply_res.json().await.unwrap(); 994 assert_eq!(reply_record["value"]["reply"]["root"]["uri"], root_uri); 995 assert_eq!(reply_record["value"]["reply"]["parent"]["uri"], root_uri); 996 997 tokio::time::sleep(Duration::from_millis(100)).await; 998 999 let nested_reply_rkey = format!("e2e_nested_reply_{}", Utc::now().timestamp_millis()); 1000 let nested_payload = json!({ 1001 "repo": alice_did, 1002 "collection": reply_collection, 1003 "rkey": nested_reply_rkey, 1004 "record": { 1005 "$type": reply_collection, 1006 "text": "Alice replies to Bob's reply", 1007 "createdAt": Utc::now().to_rfc3339(), 1008 "reply": { 1009 "root": { 1010 "uri": root_uri, 1011 "cid": root_cid 1012 }, 1013 "parent": { 1014 "uri": reply_uri, 1015 "cid": reply_cid 1016 } 1017 } 1018 } 1019 }); 1020 1021 let nested_res = client 1022 .post(format!( 1023 "{}/xrpc/com.atproto.repo.putRecord", 1024 base_url().await 1025 )) 1026 .bearer_auth(&alice_jwt) 1027 .json(&nested_payload) 1028 .send() 1029 .await 1030 .expect("Failed to create nested reply"); 1031 1032 assert_eq!(nested_res.status(), StatusCode::OK, "Failed to create nested reply"); 1033} 1034 1035#[tokio::test] 1036async fn test_like_lifecycle() { 1037 let client = client(); 1038 1039 let (alice_did, alice_jwt) = setup_new_user("alice-like").await; 1040 let (bob_did, bob_jwt) = setup_new_user("bob-like").await; 1041 1042 let (post_uri, post_cid) = create_post(&client, &alice_did, &alice_jwt, "Like this post!").await; 1043 1044 let (like_uri, _) = create_like(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await; 1045 1046 let like_rkey = like_uri.split('/').last().unwrap(); 1047 let get_like_res = client 1048 .get(format!( 1049 "{}/xrpc/com.atproto.repo.getRecord", 1050 base_url().await 1051 )) 1052 .query(&[ 1053 ("repo", bob_did.as_str()), 1054 ("collection", "app.bsky.feed.like"), 1055 ("rkey", like_rkey), 1056 ]) 1057 .send() 1058 .await 1059 .expect("Failed to get like"); 1060 1061 assert_eq!(get_like_res.status(), StatusCode::OK); 1062 let like_body: Value = get_like_res.json().await.unwrap(); 1063 assert_eq!(like_body["value"]["subject"]["uri"], post_uri); 1064 1065 let delete_payload = json!({ 1066 "repo": bob_did, 1067 "collection": "app.bsky.feed.like", 1068 "rkey": like_rkey 1069 }); 1070 1071 let delete_res = client 1072 .post(format!( 1073 "{}/xrpc/com.atproto.repo.deleteRecord", 1074 base_url().await 1075 )) 1076 .bearer_auth(&bob_jwt) 1077 .json(&delete_payload) 1078 .send() 1079 .await 1080 .expect("Failed to delete like"); 1081 1082 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete like"); 1083 1084 let get_deleted_res = client 1085 .get(format!( 1086 "{}/xrpc/com.atproto.repo.getRecord", 1087 base_url().await 1088 )) 1089 .query(&[ 1090 ("repo", bob_did.as_str()), 1091 ("collection", "app.bsky.feed.like"), 1092 ("rkey", like_rkey), 1093 ]) 1094 .send() 1095 .await 1096 .expect("Failed to check deleted like"); 1097 1098 assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Like should be deleted"); 1099} 1100 1101#[tokio::test] 1102async fn test_repost_lifecycle() { 1103 let client = client(); 1104 1105 let (alice_did, alice_jwt) = setup_new_user("alice-repost").await; 1106 let (bob_did, bob_jwt) = setup_new_user("bob-repost").await; 1107 1108 let (post_uri, post_cid) = create_post(&client, &alice_did, &alice_jwt, "Repost this!").await; 1109 1110 let (repost_uri, _) = create_repost(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await; 1111 1112 let repost_rkey = repost_uri.split('/').last().unwrap(); 1113 let get_repost_res = client 1114 .get(format!( 1115 "{}/xrpc/com.atproto.repo.getRecord", 1116 base_url().await 1117 )) 1118 .query(&[ 1119 ("repo", bob_did.as_str()), 1120 ("collection", "app.bsky.feed.repost"), 1121 ("rkey", repost_rkey), 1122 ]) 1123 .send() 1124 .await 1125 .expect("Failed to get repost"); 1126 1127 assert_eq!(get_repost_res.status(), StatusCode::OK); 1128 let repost_body: Value = get_repost_res.json().await.unwrap(); 1129 assert_eq!(repost_body["value"]["subject"]["uri"], post_uri); 1130 1131 let delete_payload = json!({ 1132 "repo": bob_did, 1133 "collection": "app.bsky.feed.repost", 1134 "rkey": repost_rkey 1135 }); 1136 1137 let delete_res = client 1138 .post(format!( 1139 "{}/xrpc/com.atproto.repo.deleteRecord", 1140 base_url().await 1141 )) 1142 .bearer_auth(&bob_jwt) 1143 .json(&delete_payload) 1144 .send() 1145 .await 1146 .expect("Failed to delete repost"); 1147 1148 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete repost"); 1149} 1150 1151#[tokio::test] 1152async fn test_unfollow_lifecycle() { 1153 let client = client(); 1154 1155 let (alice_did, _alice_jwt) = setup_new_user("alice-unfollow").await; 1156 let (bob_did, bob_jwt) = setup_new_user("bob-unfollow").await; 1157 1158 let (follow_uri, _) = create_follow(&client, &bob_did, &bob_jwt, &alice_did).await; 1159 1160 let follow_rkey = follow_uri.split('/').last().unwrap(); 1161 let get_follow_res = client 1162 .get(format!( 1163 "{}/xrpc/com.atproto.repo.getRecord", 1164 base_url().await 1165 )) 1166 .query(&[ 1167 ("repo", bob_did.as_str()), 1168 ("collection", "app.bsky.graph.follow"), 1169 ("rkey", follow_rkey), 1170 ]) 1171 .send() 1172 .await 1173 .expect("Failed to get follow"); 1174 1175 assert_eq!(get_follow_res.status(), StatusCode::OK); 1176 1177 let unfollow_payload = json!({ 1178 "repo": bob_did, 1179 "collection": "app.bsky.graph.follow", 1180 "rkey": follow_rkey 1181 }); 1182 1183 let unfollow_res = client 1184 .post(format!( 1185 "{}/xrpc/com.atproto.repo.deleteRecord", 1186 base_url().await 1187 )) 1188 .bearer_auth(&bob_jwt) 1189 .json(&unfollow_payload) 1190 .send() 1191 .await 1192 .expect("Failed to unfollow"); 1193 1194 assert_eq!(unfollow_res.status(), StatusCode::OK, "Failed to unfollow"); 1195 1196 let get_deleted_res = client 1197 .get(format!( 1198 "{}/xrpc/com.atproto.repo.getRecord", 1199 base_url().await 1200 )) 1201 .query(&[ 1202 ("repo", bob_did.as_str()), 1203 ("collection", "app.bsky.graph.follow"), 1204 ("rkey", follow_rkey), 1205 ]) 1206 .send() 1207 .await 1208 .expect("Failed to check deleted follow"); 1209 1210 assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Follow should be deleted"); 1211} 1212 1213#[tokio::test] 1214async fn test_timeline_after_unfollow() { 1215 let client = client(); 1216 1217 let (alice_did, alice_jwt) = setup_new_user("alice-tl-unfollow").await; 1218 let (bob_did, bob_jwt) = setup_new_user("bob-tl-unfollow").await; 1219 1220 let (follow_uri, _) = create_follow(&client, &bob_did, &bob_jwt, &alice_did).await; 1221 1222 create_post(&client, &alice_did, &alice_jwt, "Post while following").await; 1223 1224 tokio::time::sleep(Duration::from_secs(1)).await; 1225 1226 let timeline_res = client 1227 .get(format!( 1228 "{}/xrpc/app.bsky.feed.getTimeline", 1229 base_url().await 1230 )) 1231 .bearer_auth(&bob_jwt) 1232 .send() 1233 .await 1234 .expect("Failed to get timeline"); 1235 1236 assert_eq!(timeline_res.status(), StatusCode::OK); 1237 let timeline_body: Value = timeline_res.json().await.unwrap(); 1238 let feed = timeline_body["feed"].as_array().unwrap(); 1239 assert_eq!(feed.len(), 1, "Should see 1 post from Alice"); 1240 1241 let follow_rkey = follow_uri.split('/').last().unwrap(); 1242 let unfollow_payload = json!({ 1243 "repo": bob_did, 1244 "collection": "app.bsky.graph.follow", 1245 "rkey": follow_rkey 1246 }); 1247 client 1248 .post(format!( 1249 "{}/xrpc/com.atproto.repo.deleteRecord", 1250 base_url().await 1251 )) 1252 .bearer_auth(&bob_jwt) 1253 .json(&unfollow_payload) 1254 .send() 1255 .await 1256 .expect("Failed to unfollow"); 1257 1258 tokio::time::sleep(Duration::from_secs(1)).await; 1259 1260 let timeline_after_res = client 1261 .get(format!( 1262 "{}/xrpc/app.bsky.feed.getTimeline", 1263 base_url().await 1264 )) 1265 .bearer_auth(&bob_jwt) 1266 .send() 1267 .await 1268 .expect("Failed to get timeline after unfollow"); 1269 1270 assert_eq!(timeline_after_res.status(), StatusCode::OK); 1271 let timeline_after: Value = timeline_after_res.json().await.unwrap(); 1272 let feed_after = timeline_after["feed"].as_array().unwrap(); 1273 assert_eq!(feed_after.len(), 0, "Should see 0 posts after unfollowing"); 1274} 1275 1276#[tokio::test] 1277async fn test_blob_in_record_lifecycle() { 1278 let client = client(); 1279 let (did, jwt) = setup_new_user("blob-record").await; 1280 1281 let blob_data = b"This is test blob data for a profile avatar"; 1282 let upload_res = client 1283 .post(format!( 1284 "{}/xrpc/com.atproto.repo.uploadBlob", 1285 base_url().await 1286 )) 1287 .header(header::CONTENT_TYPE, "text/plain") 1288 .bearer_auth(&jwt) 1289 .body(blob_data.to_vec()) 1290 .send() 1291 .await 1292 .expect("Failed to upload blob"); 1293 1294 assert_eq!(upload_res.status(), StatusCode::OK); 1295 let upload_body: Value = upload_res.json().await.unwrap(); 1296 let blob_ref = upload_body["blob"].clone(); 1297 1298 let profile_payload = json!({ 1299 "repo": did, 1300 "collection": "app.bsky.actor.profile", 1301 "rkey": "self", 1302 "record": { 1303 "$type": "app.bsky.actor.profile", 1304 "displayName": "User With Avatar", 1305 "avatar": blob_ref 1306 } 1307 }); 1308 1309 let create_res = client 1310 .post(format!( 1311 "{}/xrpc/com.atproto.repo.putRecord", 1312 base_url().await 1313 )) 1314 .bearer_auth(&jwt) 1315 .json(&profile_payload) 1316 .send() 1317 .await 1318 .expect("Failed to create profile with blob"); 1319 1320 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile with blob"); 1321 1322 let get_res = client 1323 .get(format!( 1324 "{}/xrpc/com.atproto.repo.getRecord", 1325 base_url().await 1326 )) 1327 .query(&[ 1328 ("repo", did.as_str()), 1329 ("collection", "app.bsky.actor.profile"), 1330 ("rkey", "self"), 1331 ]) 1332 .send() 1333 .await 1334 .expect("Failed to get profile"); 1335 1336 assert_eq!(get_res.status(), StatusCode::OK); 1337 let profile: Value = get_res.json().await.unwrap(); 1338 assert!(profile["value"]["avatar"]["ref"]["$link"].is_string()); 1339} 1340 1341#[tokio::test] 1342async fn test_authorization_cannot_modify_other_repo() { 1343 let client = client(); 1344 1345 let (alice_did, _alice_jwt) = setup_new_user("alice-auth").await; 1346 let (_bob_did, bob_jwt) = setup_new_user("bob-auth").await; 1347 1348 let post_payload = json!({ 1349 "repo": alice_did, 1350 "collection": "app.bsky.feed.post", 1351 "rkey": "unauthorized-post", 1352 "record": { 1353 "$type": "app.bsky.feed.post", 1354 "text": "Bob trying to post as Alice", 1355 "createdAt": Utc::now().to_rfc3339() 1356 } 1357 }); 1358 1359 let res = client 1360 .post(format!( 1361 "{}/xrpc/com.atproto.repo.putRecord", 1362 base_url().await 1363 )) 1364 .bearer_auth(&bob_jwt) 1365 .json(&post_payload) 1366 .send() 1367 .await 1368 .expect("Failed to send request"); 1369 1370 assert!( 1371 res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED, 1372 "Expected 403 or 401 when writing to another user's repo, got {}", 1373 res.status() 1374 ); 1375} 1376 1377#[tokio::test] 1378async fn test_authorization_cannot_delete_other_record() { 1379 let client = client(); 1380 1381 let (alice_did, alice_jwt) = setup_new_user("alice-del-auth").await; 1382 let (_bob_did, bob_jwt) = setup_new_user("bob-del-auth").await; 1383 1384 let (post_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's post").await; 1385 let post_rkey = post_uri.split('/').last().unwrap(); 1386 1387 let delete_payload = json!({ 1388 "repo": alice_did, 1389 "collection": "app.bsky.feed.post", 1390 "rkey": post_rkey 1391 }); 1392 1393 let res = client 1394 .post(format!( 1395 "{}/xrpc/com.atproto.repo.deleteRecord", 1396 base_url().await 1397 )) 1398 .bearer_auth(&bob_jwt) 1399 .json(&delete_payload) 1400 .send() 1401 .await 1402 .expect("Failed to send request"); 1403 1404 assert!( 1405 res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED, 1406 "Expected 403 or 401 when deleting another user's record, got {}", 1407 res.status() 1408 ); 1409 1410 let get_res = client 1411 .get(format!( 1412 "{}/xrpc/com.atproto.repo.getRecord", 1413 base_url().await 1414 )) 1415 .query(&[ 1416 ("repo", alice_did.as_str()), 1417 ("collection", "app.bsky.feed.post"), 1418 ("rkey", post_rkey), 1419 ]) 1420 .send() 1421 .await 1422 .expect("Failed to verify record exists"); 1423 1424 assert_eq!(get_res.status(), StatusCode::OK, "Record should still exist"); 1425} 1426 1427#[tokio::test] 1428async fn test_list_records_pagination() { 1429 let client = client(); 1430 let (did, jwt) = setup_new_user("list-pagination").await; 1431 1432 for i in 0..5 { 1433 tokio::time::sleep(Duration::from_millis(50)).await; 1434 create_post(&client, &did, &jwt, &format!("Post number {}", i)).await; 1435 } 1436 1437 let list_res = client 1438 .get(format!( 1439 "{}/xrpc/com.atproto.repo.listRecords", 1440 base_url().await 1441 )) 1442 .query(&[ 1443 ("repo", did.as_str()), 1444 ("collection", "app.bsky.feed.post"), 1445 ("limit", "2"), 1446 ]) 1447 .send() 1448 .await 1449 .expect("Failed to list records"); 1450 1451 assert_eq!(list_res.status(), StatusCode::OK); 1452 let list_body: Value = list_res.json().await.unwrap(); 1453 let records = list_body["records"].as_array().unwrap(); 1454 assert_eq!(records.len(), 2, "Should return 2 records with limit=2"); 1455 1456 if let Some(cursor) = list_body["cursor"].as_str() { 1457 let list_page2_res = client 1458 .get(format!( 1459 "{}/xrpc/com.atproto.repo.listRecords", 1460 base_url().await 1461 )) 1462 .query(&[ 1463 ("repo", did.as_str()), 1464 ("collection", "app.bsky.feed.post"), 1465 ("limit", "2"), 1466 ("cursor", cursor), 1467 ]) 1468 .send() 1469 .await 1470 .expect("Failed to list records page 2"); 1471 1472 assert_eq!(list_page2_res.status(), StatusCode::OK); 1473 let page2_body: Value = list_page2_res.json().await.unwrap(); 1474 let page2_records = page2_body["records"].as_array().unwrap(); 1475 assert_eq!(page2_records.len(), 2, "Page 2 should have 2 more records"); 1476 } 1477} 1478 1479#[tokio::test] 1480async fn test_mutual_follow_lifecycle() { 1481 let client = client(); 1482 1483 let (alice_did, alice_jwt) = setup_new_user("alice-mutual").await; 1484 let (bob_did, bob_jwt) = setup_new_user("bob-mutual").await; 1485 1486 create_follow(&client, &alice_did, &alice_jwt, &bob_did).await; 1487 create_follow(&client, &bob_did, &bob_jwt, &alice_did).await; 1488 1489 create_post(&client, &alice_did, &alice_jwt, "Alice's post for mutual").await; 1490 create_post(&client, &bob_did, &bob_jwt, "Bob's post for mutual").await; 1491 1492 tokio::time::sleep(Duration::from_secs(1)).await; 1493 1494 let alice_timeline_res = client 1495 .get(format!( 1496 "{}/xrpc/app.bsky.feed.getTimeline", 1497 base_url().await 1498 )) 1499 .bearer_auth(&alice_jwt) 1500 .send() 1501 .await 1502 .expect("Failed to get Alice's timeline"); 1503 1504 assert_eq!(alice_timeline_res.status(), StatusCode::OK); 1505 let alice_tl: Value = alice_timeline_res.json().await.unwrap(); 1506 let alice_feed = alice_tl["feed"].as_array().unwrap(); 1507 assert_eq!(alice_feed.len(), 1, "Alice should see Bob's 1 post"); 1508 1509 let bob_timeline_res = client 1510 .get(format!( 1511 "{}/xrpc/app.bsky.feed.getTimeline", 1512 base_url().await 1513 )) 1514 .bearer_auth(&bob_jwt) 1515 .send() 1516 .await 1517 .expect("Failed to get Bob's timeline"); 1518 1519 assert_eq!(bob_timeline_res.status(), StatusCode::OK); 1520 let bob_tl: Value = bob_timeline_res.json().await.unwrap(); 1521 let bob_feed = bob_tl["feed"].as_array().unwrap(); 1522 assert_eq!(bob_feed.len(), 1, "Bob should see Alice's 1 post"); 1523} 1524 1525#[tokio::test] 1526async fn test_account_to_post_full_lifecycle() { 1527 let client = client(); 1528 let ts = Utc::now().timestamp_millis(); 1529 let handle = format!("fullcycle-{}.test", ts); 1530 let email = format!("fullcycle-{}@test.com", ts); 1531 let password = "fullcycle-password"; 1532 1533 let create_account_res = client 1534 .post(format!( 1535 "{}/xrpc/com.atproto.server.createAccount", 1536 base_url().await 1537 )) 1538 .json(&json!({ 1539 "handle": handle, 1540 "email": email, 1541 "password": password 1542 })) 1543 .send() 1544 .await 1545 .expect("Failed to create account"); 1546 1547 assert_eq!(create_account_res.status(), StatusCode::OK); 1548 let account_body: Value = create_account_res.json().await.unwrap(); 1549 let did = account_body["did"].as_str().unwrap().to_string(); 1550 let access_jwt = account_body["accessJwt"].as_str().unwrap().to_string(); 1551 1552 let get_session_res = client 1553 .get(format!( 1554 "{}/xrpc/com.atproto.server.getSession", 1555 base_url().await 1556 )) 1557 .bearer_auth(&access_jwt) 1558 .send() 1559 .await 1560 .expect("Failed to get session"); 1561 1562 assert_eq!(get_session_res.status(), StatusCode::OK); 1563 let session_body: Value = get_session_res.json().await.unwrap(); 1564 assert_eq!(session_body["did"], did); 1565 assert_eq!(session_body["handle"], handle); 1566 1567 let profile_res = client 1568 .post(format!( 1569 "{}/xrpc/com.atproto.repo.putRecord", 1570 base_url().await 1571 )) 1572 .bearer_auth(&access_jwt) 1573 .json(&json!({ 1574 "repo": did, 1575 "collection": "app.bsky.actor.profile", 1576 "rkey": "self", 1577 "record": { 1578 "$type": "app.bsky.actor.profile", 1579 "displayName": "Full Cycle User" 1580 } 1581 })) 1582 .send() 1583 .await 1584 .expect("Failed to create profile"); 1585 1586 assert_eq!(profile_res.status(), StatusCode::OK); 1587 1588 let (post_uri, post_cid) = create_post(&client, &did, &access_jwt, "My first post!").await; 1589 1590 let get_post_res = client 1591 .get(format!( 1592 "{}/xrpc/com.atproto.repo.getRecord", 1593 base_url().await 1594 )) 1595 .query(&[ 1596 ("repo", did.as_str()), 1597 ("collection", "app.bsky.feed.post"), 1598 ("rkey", post_uri.split('/').last().unwrap()), 1599 ]) 1600 .send() 1601 .await 1602 .expect("Failed to get post"); 1603 1604 assert_eq!(get_post_res.status(), StatusCode::OK); 1605 1606 create_like(&client, &did, &access_jwt, &post_uri, &post_cid).await; 1607 1608 let describe_res = client 1609 .get(format!( 1610 "{}/xrpc/com.atproto.repo.describeRepo", 1611 base_url().await 1612 )) 1613 .query(&[("repo", did.as_str())]) 1614 .send() 1615 .await 1616 .expect("Failed to describe repo"); 1617 1618 assert_eq!(describe_res.status(), StatusCode::OK); 1619 let describe_body: Value = describe_res.json().await.unwrap(); 1620 assert_eq!(describe_body["did"], did); 1621 assert_eq!(describe_body["handle"], handle); 1622} 1623 1624#[tokio::test] 1625async fn test_app_password_lifecycle() { 1626 let client = client(); 1627 let ts = Utc::now().timestamp_millis(); 1628 let handle = format!("apppass-{}.test", ts); 1629 let email = format!("apppass-{}@test.com", ts); 1630 let password = "apppass-password"; 1631 1632 let create_res = client 1633 .post(format!( 1634 "{}/xrpc/com.atproto.server.createAccount", 1635 base_url().await 1636 )) 1637 .json(&json!({ 1638 "handle": handle, 1639 "email": email, 1640 "password": password 1641 })) 1642 .send() 1643 .await 1644 .expect("Failed to create account"); 1645 1646 assert_eq!(create_res.status(), StatusCode::OK); 1647 let account: Value = create_res.json().await.unwrap(); 1648 let jwt = account["accessJwt"].as_str().unwrap(); 1649 1650 let create_app_pass_res = client 1651 .post(format!( 1652 "{}/xrpc/com.atproto.server.createAppPassword", 1653 base_url().await 1654 )) 1655 .bearer_auth(jwt) 1656 .json(&json!({ "name": "Test App" })) 1657 .send() 1658 .await 1659 .expect("Failed to create app password"); 1660 1661 assert_eq!(create_app_pass_res.status(), StatusCode::OK); 1662 let app_pass: Value = create_app_pass_res.json().await.unwrap(); 1663 let app_password = app_pass["password"].as_str().unwrap().to_string(); 1664 assert_eq!(app_pass["name"], "Test App"); 1665 1666 let list_res = client 1667 .get(format!( 1668 "{}/xrpc/com.atproto.server.listAppPasswords", 1669 base_url().await 1670 )) 1671 .bearer_auth(jwt) 1672 .send() 1673 .await 1674 .expect("Failed to list app passwords"); 1675 1676 assert_eq!(list_res.status(), StatusCode::OK); 1677 let list_body: Value = list_res.json().await.unwrap(); 1678 let passwords = list_body["passwords"].as_array().unwrap(); 1679 assert_eq!(passwords.len(), 1); 1680 assert_eq!(passwords[0]["name"], "Test App"); 1681 1682 let login_res = client 1683 .post(format!( 1684 "{}/xrpc/com.atproto.server.createSession", 1685 base_url().await 1686 )) 1687 .json(&json!({ 1688 "identifier": handle, 1689 "password": app_password 1690 })) 1691 .send() 1692 .await 1693 .expect("Failed to login with app password"); 1694 1695 assert_eq!(login_res.status(), StatusCode::OK, "App password login should work"); 1696 1697 let revoke_res = client 1698 .post(format!( 1699 "{}/xrpc/com.atproto.server.revokeAppPassword", 1700 base_url().await 1701 )) 1702 .bearer_auth(jwt) 1703 .json(&json!({ "name": "Test App" })) 1704 .send() 1705 .await 1706 .expect("Failed to revoke app password"); 1707 1708 assert_eq!(revoke_res.status(), StatusCode::OK); 1709 1710 let login_after_revoke = client 1711 .post(format!( 1712 "{}/xrpc/com.atproto.server.createSession", 1713 base_url().await 1714 )) 1715 .json(&json!({ 1716 "identifier": handle, 1717 "password": app_password 1718 })) 1719 .send() 1720 .await 1721 .expect("Failed to attempt login after revoke"); 1722 1723 assert!( 1724 login_after_revoke.status() == StatusCode::UNAUTHORIZED 1725 || login_after_revoke.status() == StatusCode::BAD_REQUEST, 1726 "Revoked app password should not work" 1727 ); 1728 1729 let list_after_revoke = client 1730 .get(format!( 1731 "{}/xrpc/com.atproto.server.listAppPasswords", 1732 base_url().await 1733 )) 1734 .bearer_auth(jwt) 1735 .send() 1736 .await 1737 .expect("Failed to list after revoke"); 1738 1739 let list_after: Value = list_after_revoke.json().await.unwrap(); 1740 let passwords_after = list_after["passwords"].as_array().unwrap(); 1741 assert_eq!(passwords_after.len(), 0, "No app passwords should remain"); 1742} 1743 1744#[tokio::test] 1745async fn test_account_deactivation_lifecycle() { 1746 let client = client(); 1747 let ts = Utc::now().timestamp_millis(); 1748 let handle = format!("deactivate-{}.test", ts); 1749 let email = format!("deactivate-{}@test.com", ts); 1750 let password = "deactivate-password"; 1751 1752 let create_res = client 1753 .post(format!( 1754 "{}/xrpc/com.atproto.server.createAccount", 1755 base_url().await 1756 )) 1757 .json(&json!({ 1758 "handle": handle, 1759 "email": email, 1760 "password": password 1761 })) 1762 .send() 1763 .await 1764 .expect("Failed to create account"); 1765 1766 assert_eq!(create_res.status(), StatusCode::OK); 1767 let account: Value = create_res.json().await.unwrap(); 1768 let did = account["did"].as_str().unwrap().to_string(); 1769 let jwt = account["accessJwt"].as_str().unwrap().to_string(); 1770 1771 let (post_uri, _) = create_post(&client, &did, &jwt, "Post before deactivation").await; 1772 let post_rkey = post_uri.split('/').last().unwrap(); 1773 1774 let status_before = client 1775 .get(format!( 1776 "{}/xrpc/com.atproto.server.checkAccountStatus", 1777 base_url().await 1778 )) 1779 .bearer_auth(&jwt) 1780 .send() 1781 .await 1782 .expect("Failed to check status"); 1783 1784 assert_eq!(status_before.status(), StatusCode::OK); 1785 let status_body: Value = status_before.json().await.unwrap(); 1786 assert_eq!(status_body["activated"], true); 1787 1788 let deactivate_res = client 1789 .post(format!( 1790 "{}/xrpc/com.atproto.server.deactivateAccount", 1791 base_url().await 1792 )) 1793 .bearer_auth(&jwt) 1794 .json(&json!({})) 1795 .send() 1796 .await 1797 .expect("Failed to deactivate"); 1798 1799 assert_eq!(deactivate_res.status(), StatusCode::OK); 1800 1801 let get_post_res = client 1802 .get(format!( 1803 "{}/xrpc/com.atproto.repo.getRecord", 1804 base_url().await 1805 )) 1806 .query(&[ 1807 ("repo", did.as_str()), 1808 ("collection", "app.bsky.feed.post"), 1809 ("rkey", post_rkey), 1810 ]) 1811 .send() 1812 .await 1813 .expect("Failed to get post while deactivated"); 1814 1815 assert_eq!(get_post_res.status(), StatusCode::OK, "Records should still be readable"); 1816 1817 let activate_res = client 1818 .post(format!( 1819 "{}/xrpc/com.atproto.server.activateAccount", 1820 base_url().await 1821 )) 1822 .bearer_auth(&jwt) 1823 .json(&json!({})) 1824 .send() 1825 .await 1826 .expect("Failed to reactivate"); 1827 1828 assert_eq!(activate_res.status(), StatusCode::OK); 1829 1830 let status_after_activate = client 1831 .get(format!( 1832 "{}/xrpc/com.atproto.server.checkAccountStatus", 1833 base_url().await 1834 )) 1835 .bearer_auth(&jwt) 1836 .send() 1837 .await 1838 .expect("Failed to check status after activate"); 1839 1840 assert_eq!(status_after_activate.status(), StatusCode::OK); 1841 1842 let (new_post_uri, _) = create_post(&client, &did, &jwt, "Post after reactivation").await; 1843 assert!(!new_post_uri.is_empty(), "Should be able to post after reactivation"); 1844}