this repo has no description
1mod common; 2use common::*; 3 4use reqwest::StatusCode; 5use serde_json::{json, Value}; 6use chrono::Utc; 7use std::time::Duration; 8 9use reqwest::Client; 10#[allow(unused_imports)] 11use std::collections::HashMap; 12 13#[tokio::test] 14async fn test_post_crud_lifecycle() { 15 let client = client(); 16 let collection = "app.bsky.feed.post"; 17 18 let rkey = format!("e2e_lifecycle_{}", Utc::now().timestamp_millis()); 19 let now = Utc::now().to_rfc3339(); 20 21 let original_text = "Hello from the lifecycle test!"; 22 let create_payload = json!({ 23 "repo": AUTH_DID, 24 "collection": collection, 25 "rkey": rkey, 26 "record": { 27 "$type": collection, 28 "text": original_text, 29 "createdAt": now 30 } 31 }); 32 33 let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 34 .bearer_auth(AUTH_TOKEN) 35 .json(&create_payload) 36 .send() 37 .await 38 .expect("Failed to send create request"); 39 40 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create record"); 41 let create_body: Value = create_res.json().await.expect("create response was not JSON"); 42 let uri = create_body["uri"].as_str().unwrap(); 43 44 45 let params = [ 46 ("repo", AUTH_DID), 47 ("collection", collection), 48 ("rkey", &rkey), 49 ]; 50 let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 51 .query(&params) 52 .send() 53 .await 54 .expect("Failed to send get request"); 55 56 assert_eq!(get_res.status(), StatusCode::OK, "Failed to get record after create"); 57 let get_body: Value = get_res.json().await.expect("get response was not JSON"); 58 assert_eq!(get_body["uri"], uri); 59 assert_eq!(get_body["value"]["text"], original_text); 60 61 62 let updated_text = "This post has been updated."; 63 let update_payload = json!({ 64 "repo": AUTH_DID, 65 "collection": collection, 66 "rkey": rkey, 67 "record": { 68 "$type": collection, 69 "text": updated_text, 70 "createdAt": now 71 } 72 }); 73 74 let update_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 75 .bearer_auth(AUTH_TOKEN) 76 .json(&update_payload) 77 .send() 78 .await 79 .expect("Failed to send update request"); 80 81 assert_eq!(update_res.status(), StatusCode::OK, "Failed to update record"); 82 83 84 let get_updated_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 85 .query(&params) 86 .send() 87 .await 88 .expect("Failed to send get-after-update request"); 89 90 assert_eq!(get_updated_res.status(), StatusCode::OK, "Failed to get record after update"); 91 let get_updated_body: Value = get_updated_res.json().await.expect("get-updated response was not JSON"); 92 assert_eq!(get_updated_body["value"]["text"], updated_text, "Text was not updated"); 93 94 95 let delete_payload = json!({ 96 "repo": AUTH_DID, 97 "collection": collection, 98 "rkey": rkey 99 }); 100 101 let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 102 .bearer_auth(AUTH_TOKEN) 103 .json(&delete_payload) 104 .send() 105 .await 106 .expect("Failed to send delete request"); 107 108 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete record"); 109 110 111 let get_deleted_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 112 .query(&params) 113 .send() 114 .await 115 .expect("Failed to send get-after-delete request"); 116 117 assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Record was found, but it should be deleted"); 118} 119 120#[tokio::test] 121async fn test_post_with_image_lifecycle() { 122 let client = client(); 123 124 let now_str = Utc::now().to_rfc3339(); 125 let fake_image_data = format!("This is a fake PNG for test at {}", now_str); 126 127 let image_blob = upload_test_blob( 128 &client, 129 Box::leak(fake_image_data.into_boxed_str()), 130 "image/png" 131 ).await; 132 133 let blob_ref = image_blob["ref"].clone(); 134 assert!(blob_ref.is_object(), "Blob ref is not an object"); 135 136 137 let collection = "app.bsky.feed.post"; 138 let rkey = format!("e2e_image_post_{}", Utc::now().timestamp_millis()); 139 140 let create_payload = json!({ 141 "repo": AUTH_DID, 142 "collection": collection, 143 "rkey": rkey, 144 "record": { 145 "$type": collection, 146 "text": "Check out this image!", 147 "createdAt": Utc::now().to_rfc3339(), 148 "embed": { 149 "$type": "app.bsky.embed.images", 150 "images": [ 151 { 152 "image": image_blob, 153 "alt": "A test image" 154 } 155 ] 156 } 157 } 158 }); 159 160 let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 161 .bearer_auth(AUTH_TOKEN) 162 .json(&create_payload) 163 .send() 164 .await 165 .expect("Failed to create image post"); 166 167 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create post with image"); 168 169 170 let params = [ 171 ("repo", AUTH_DID), 172 ("collection", collection), 173 ("rkey", &rkey), 174 ]; 175 let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 176 .query(&params) 177 .send() 178 .await 179 .expect("Failed to get image post"); 180 181 assert_eq!(get_res.status(), StatusCode::OK, "Failed to get image post"); 182 let get_body: Value = get_res.json().await.expect("get image post was not JSON"); 183 184 let embed_image = &get_body["value"]["embed"]["images"][0]["image"]; 185 assert!(embed_image.is_object(), "Embedded image is missing"); 186 assert_eq!(embed_image["ref"], blob_ref, "Embedded blob ref does not match uploaded ref"); 187} 188 189#[tokio::test] 190async fn test_graph_lifecycle_follow_unfollow() { 191 let client = client(); 192 let collection = "app.bsky.graph.follow"; 193 194 let create_payload = json!({ 195 "repo": AUTH_DID, 196 "collection": collection, 197 // "rkey" is omitted, server will generate it right? 198 "record": { 199 "$type": collection, 200 "subject": TARGET_DID, 201 "createdAt": Utc::now().to_rfc3339() 202 } 203 }); 204 205 let create_res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) 206 .bearer_auth(AUTH_TOKEN) 207 .json(&create_payload) 208 .send() 209 .await 210 .expect("Failed to send follow createRecord"); 211 212 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create follow record"); 213 let create_body: Value = create_res.json().await.expect("create follow response was not JSON"); 214 let follow_uri = create_body["uri"].as_str().expect("Response had no URI"); 215 216 let rkey = follow_uri.split('/').last().expect("URI was malformed"); 217 218 219 let params_get_follows = [ 220 ("actor", AUTH_DID), 221 ]; 222 let get_follows_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", base_url().await)) 223 .query(&params_get_follows) 224 .bearer_auth(AUTH_TOKEN) 225 .send() 226 .await 227 .expect("Failed to send getFollows"); 228 229 assert_eq!(get_follows_res.status(), StatusCode::OK, "getFollows did not return 200"); 230 let get_follows_body: Value = get_follows_res.json().await.expect("getFollows response was not JSON"); 231 232 let follows_list = get_follows_body["follows"].as_array().expect("follows key was not an array"); 233 let is_following = follows_list.iter().any(|actor| { 234 actor["did"].as_str() == Some(TARGET_DID) 235 }); 236 237 assert!(is_following, "getFollows list did not contain the target DID"); 238 239 240 let delete_payload = json!({ 241 "repo": AUTH_DID, 242 "collection": collection, 243 "rkey": rkey 244 }); 245 246 let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 247 .bearer_auth(AUTH_TOKEN) 248 .json(&delete_payload) 249 .send() 250 .await 251 .expect("Failed to send unfollow deleteRecord"); 252 253 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete follow record"); 254 255 256 let get_unfollowed_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", base_url().await)) 257 .query(&params_get_follows) 258 .bearer_auth(AUTH_TOKEN) 259 .send() 260 .await 261 .expect("Failed to send getFollows after delete"); 262 263 assert_eq!(get_unfollowed_res.status(), StatusCode::OK, "getFollows (after delete) did not return 200"); 264 let get_unfollowed_body: Value = get_unfollowed_res.json().await.expect("getFollows (after delete) was not JSON"); 265 266 let follows_list_after = get_unfollowed_body["follows"].as_array().expect("follows key was not an array"); 267 let is_still_following = follows_list_after.iter().any(|actor| { 268 actor["did"].as_str() == Some(TARGET_DID) 269 }); 270 271 assert!(!is_still_following, "getFollows list *still* contains the target DID after unfollow"); 272} 273 274#[tokio::test] 275async fn test_list_records_pagination() { 276 let client = client(); 277 let collection = "app.bsky.feed.post"; 278 let mut created_rkeys = Vec::new(); 279 280 for i in 0..3 { 281 let rkey = format!("e2e_pagination_{}", Utc::now().timestamp_millis()); 282 let payload = json!({ 283 "repo": AUTH_DID, 284 "collection": collection, 285 "rkey": rkey, 286 "record": { 287 "$type": collection, 288 "text": format!("Pagination test post #{}", i), 289 "createdAt": Utc::now().to_rfc3339() 290 } 291 }); 292 293 let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 294 .bearer_auth(AUTH_TOKEN) 295 .json(&payload) 296 .send() 297 .await 298 .expect("Failed to create pagination post"); 299 300 assert_eq!(res.status(), StatusCode::OK, "Failed to create post for pagination test"); 301 created_rkeys.push(rkey); 302 tokio::time::sleep(Duration::from_millis(10)).await; 303 } 304 305 let params_page1 = [ 306 ("repo", AUTH_DID), 307 ("collection", collection), 308 ("limit", "2"), 309 ]; 310 311 let page1_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await)) 312 .query(&params_page1) 313 .send() 314 .await 315 .expect("Failed to send listRecords (page 1)"); 316 317 assert_eq!(page1_res.status(), StatusCode::OK, "listRecords (page 1) failed"); 318 let page1_body: Value = page1_res.json().await.expect("listRecords (page 1) was not JSON"); 319 320 let page1_records = page1_body["records"].as_array().expect("records was not an array"); 321 assert_eq!(page1_records.len(), 2, "Page 1 did not return 2 records"); 322 323 let cursor = page1_body["cursor"].as_str().expect("Page 1 did not have a cursor"); 324 325 326 let params_page2 = [ 327 ("repo", AUTH_DID), 328 ("collection", collection), 329 ("limit", "2"), 330 ("cursor", cursor), 331 ]; 332 333 let page2_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await)) 334 .query(&params_page2) 335 .send() 336 .await 337 .expect("Failed to send listRecords (page 2)"); 338 339 assert_eq!(page2_res.status(), StatusCode::OK, "listRecords (page 2) failed"); 340 let page2_body: Value = page2_res.json().await.expect("listRecords (page 2) was not JSON"); 341 342 let page2_records = page2_body["records"].as_array().expect("records was not an array"); 343 assert_eq!(page2_records.len(), 1, "Page 2 did not return 1 record"); 344 345 assert!(page2_body["cursor"].is_null() || page2_body["cursor"].as_str().is_none(), "Page 2 should not have a cursor"); 346 347 348 for rkey in created_rkeys { 349 let delete_payload = json!({ 350 "repo": AUTH_DID, 351 "collection": collection, 352 "rkey": rkey 353 }); 354 client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 355 .bearer_auth(AUTH_TOKEN) 356 .json(&delete_payload) 357 .send() 358 .await 359 .expect("Failed to cleanup pagination post"); 360 } 361} 362 363#[tokio::test] 364async fn test_reply_thread_lifecycle() { 365 let client = client(); 366 367 let (root_uri, root_cid, root_rkey) = create_test_post( 368 &client, 369 "This is the root of the thread", 370 None 371 ).await; 372 373 374 let reply_ref = json!({ 375 "root": { "uri": root_uri.clone(), "cid": root_cid.clone() }, 376 "parent": { "uri": root_uri.clone(), "cid": root_cid.clone() } 377 }); 378 379 let (reply_uri, _reply_cid, reply_rkey) = create_test_post( 380 &client, 381 "This is a reply!", 382 Some(reply_ref) 383 ).await; 384 385 386 let params = [ 387 ("uri", &root_uri), 388 ]; 389 let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await)) 390 .query(&params) 391 .bearer_auth(AUTH_TOKEN) 392 .send() 393 .await 394 .expect("Failed to send getPostThread"); 395 396 assert_eq!(res.status(), StatusCode::OK, "getPostThread did not return 200"); 397 let body: Value = res.json().await.expect("getPostThread response was not JSON"); 398 399 assert_eq!(body["thread"]["$type"], "app.bsky.feed.defs#threadViewPost"); 400 assert_eq!(body["thread"]["post"]["uri"], root_uri); 401 402 let replies = body["thread"]["replies"].as_array().expect("replies was not an array"); 403 assert!(!replies.is_empty(), "Replies array is empty, but should contain the reply"); 404 405 let found_reply = replies.iter().find(|r| { 406 r["post"]["uri"] == reply_uri 407 }); 408 409 assert!(found_reply.is_some(), "Our specific reply was not found in the thread's replies"); 410 411 412 let collection = "app.bsky.feed.post"; 413 client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 414 .bearer_auth(AUTH_TOKEN) 415 .json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": reply_rkey })) 416 .send().await.expect("Failed to delete reply"); 417 418 client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 419 .bearer_auth(AUTH_TOKEN) 420 .json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": root_rkey })) 421 .send().await.expect("Failed to delete root post"); 422} 423 424#[tokio::test] 425async fn test_account_journey_lifecycle() { 426 let client = client(); 427 428 let ts = Utc::now().timestamp_millis(); 429 let handle = format!("e2e-user-{}.test", ts); 430 let email = format!("e2e-user-{}@test.com", ts); 431 let password = "e2e-password-123"; 432 433 let create_account_payload = json!({ 434 "handle": handle, 435 "email": email, 436 "password": password 437 }); 438 439 let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 440 .json(&create_account_payload) 441 .send() 442 .await 443 .expect("Failed to send createAccount"); 444 445 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create account"); 446 let create_body: Value = create_res.json().await.expect("createAccount response was not JSON"); 447 448 let new_did = create_body["did"].as_str().expect("Response had no DID").to_string(); 449 let _new_jwt = create_body["accessJwt"].as_str().expect("Response had no accessJwt").to_string(); 450 assert_eq!(create_body["handle"], handle); 451 452 453 let session_payload = json!({ 454 "identifier": handle, 455 "password": password 456 }); 457 458 let session_res = client.post(format!("{}/xrpc/com.atproto.server.createSession", base_url().await)) 459 .json(&session_payload) 460 .send() 461 .await 462 .expect("Failed to send createSession"); 463 464 assert_eq!(session_res.status(), StatusCode::OK, "Failed to create session"); 465 let session_body: Value = session_res.json().await.expect("createSession response was not JSON"); 466 467 let session_jwt = session_body["accessJwt"].as_str().expect("Session response had no accessJwt").to_string(); 468 assert_eq!(session_body["did"], new_did); 469 470 471 let profile_payload = json!({ 472 "repo": new_did, 473 "collection": "app.bsky.actor.profile", 474 "rkey": "self", // The rkey for a profile is always "self" 475 "record": { 476 "$type": "app.bsky.actor.profile", 477 "displayName": "E2E Test User", 478 "description": "A user created by the e2e test suite." 479 } 480 }); 481 482 let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 483 .bearer_auth(&session_jwt) 484 .json(&profile_payload) 485 .send() 486 .await 487 .expect("Failed to send putRecord for profile"); 488 489 assert_eq!(profile_res.status(), StatusCode::OK, "Failed to create profile"); 490 491 492 let params_get_profile = [ 493 ("actor", &handle), 494 ]; 495 let get_profile_res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", base_url().await)) 496 .query(&params_get_profile) 497 .send() 498 .await 499 .expect("Failed to send getProfile"); 500 501 assert_eq!(get_profile_res.status(), StatusCode::OK, "getProfile did not return 200"); 502 let profile_body: Value = get_profile_res.json().await.expect("getProfile response was not JSON"); 503 504 assert_eq!(profile_body["did"], new_did); 505 assert_eq!(profile_body["handle"], handle); 506 assert_eq!(profile_body["displayName"], "E2E Test User"); 507 508 509 let logout_res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", base_url().await)) 510 .bearer_auth(&session_jwt) 511 .send() 512 .await 513 .expect("Failed to send deleteSession"); 514 515 assert_eq!(logout_res.status(), StatusCode::OK, "Failed to delete session"); 516 517 518 let get_session_res = client.get(format!("{}/xrpc/com.atproto.server.getSession", base_url().await)) 519 .bearer_auth(&session_jwt) 520 .send() 521 .await 522 .expect("Failed to send getSession"); 523 524 assert_eq!(get_session_res.status(), StatusCode::UNAUTHORIZED, "Session was still valid after logout"); 525} 526 527async fn setup_new_user(handle_prefix: &str) -> (String, String) { 528 let client = client(); 529 let ts = Utc::now().timestamp_millis(); 530 let handle = format!("{}-{}.test", handle_prefix, ts); 531 let email = format!("{}-{}@test.com", handle_prefix, ts); 532 let password = "e2e-password-123"; 533 534 let create_account_payload = json!({ 535 "handle": handle, 536 "email": email, 537 "password": password 538 }); 539 let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base_url().await)) 540 .json(&create_account_payload) 541 .send() 542 .await 543 .expect("setup_new_user: Failed to send createAccount"); 544 assert_eq!(create_res.status(), StatusCode::OK, "setup_new_user: Failed to create account"); 545 let create_body: Value = create_res.json().await.expect("setup_new_user: createAccount response was not JSON"); 546 547 let new_did = create_body["did"].as_str().expect("setup_new_user: Response had no DID").to_string(); 548 let new_jwt = create_body["accessJwt"].as_str().expect("setup_new_user: Response had no accessJwt").to_string(); 549 550 let profile_payload = json!({ 551 "repo": new_did.clone(), 552 "collection": "app.bsky.actor.profile", 553 "rkey": "self", 554 "record": { 555 "$type": "app.bsky.actor.profile", 556 "displayName": format!("E2E User {}", handle), 557 "description": "A user created by the e2e test suite." 558 } 559 }); 560 let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 561 .bearer_auth(&new_jwt) 562 .json(&profile_payload) 563 .send() 564 .await 565 .expect("setup_new_user: Failed to send putRecord for profile"); 566 assert_eq!(profile_res.status(), StatusCode::OK, "setup_new_user: Failed to create profile"); 567 568 (new_did, new_jwt) 569} 570 571async fn create_record_as( 572 client: &Client, 573 jwt: &str, 574 did: &str, 575 collection: &str, 576 record: Value, 577) -> (String, String) { 578 let payload = json!({ 579 "repo": did, 580 "collection": collection, 581 "record": record 582 }); 583 584 let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) 585 .bearer_auth(jwt) 586 .json(&payload) 587 .send() 588 .await 589 .expect("create_record_as: Failed to send createRecord"); 590 591 assert_eq!(res.status(), StatusCode::OK, "create_record_as: Failed to create record"); 592 let body: Value = res.json().await.expect("create_record_as: response was not JSON"); 593 594 let uri = body["uri"].as_str().expect("create_record_as: Response had no URI").to_string(); 595 let cid = body["cid"].as_str().expect("create_record_as: Response had no CID").to_string(); 596 (uri, cid) 597} 598 599async fn delete_record_as( 600 client: &Client, 601 jwt: &str, 602 did: &str, 603 collection: &str, 604 rkey: &str, 605) { 606 let payload = json!({ 607 "repo": did, 608 "collection": collection, 609 "rkey": rkey 610 }); 611 612 let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 613 .bearer_auth(jwt) 614 .json(&payload) 615 .send() 616 .await 617 .expect("delete_record_as: Failed to send deleteRecord"); 618 619 assert_eq!(res.status(), StatusCode::OK, "delete_record_as: Failed to delete record"); 620} 621 622 623#[tokio::test] 624async fn test_notification_lifecycle() { 625 let client = client(); 626 627 let (user_a_did, user_a_jwt) = setup_new_user("user-a-notif").await; 628 let (user_b_did, user_b_jwt) = setup_new_user("user-b-notif").await; 629 630 let (post_uri, post_cid) = create_record_as( 631 &client, 632 &user_a_jwt, 633 &user_a_did, 634 "app.bsky.feed.post", 635 json!({ 636 "$type": "app.bsky.feed.post", 637 "text": "A post to be notified about", 638 "createdAt": Utc::now().to_rfc3339() 639 }), 640 ).await; 641 let post_ref = json!({ "uri": post_uri, "cid": post_cid }); 642 643 let count_res_1 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await)) 644 .bearer_auth(&user_a_jwt) 645 .send().await.expect("getUnreadCount 1 failed"); 646 let count_body_1: Value = count_res_1.json().await.expect("count 1 not json"); 647 assert_eq!(count_body_1["count"], 0, "Initial unread count was not 0"); 648 649 create_record_as( 650 &client, &user_b_jwt, &user_b_did, 651 "app.bsky.graph.follow", 652 json!({ 653 "$type": "app.bsky.graph.follow", 654 "subject": user_a_did, 655 "createdAt": Utc::now().to_rfc3339() 656 }), 657 ).await; 658 create_record_as( 659 &client, &user_b_jwt, &user_b_did, 660 "app.bsky.feed.like", 661 json!({ 662 "$type": "app.bsky.feed.like", 663 "subject": post_ref, 664 "createdAt": Utc::now().to_rfc3339() 665 }), 666 ).await; 667 create_record_as( 668 &client, &user_b_jwt, &user_b_did, 669 "app.bsky.feed.post", 670 json!({ 671 "$type": "app.bsky.feed.post", 672 "text": "This is a reply!", 673 "reply": { "root": post_ref.clone(), "parent": post_ref.clone() }, 674 "createdAt": Utc::now().to_rfc3339() 675 }), 676 ).await; 677 678 tokio::time::sleep(Duration::from_millis(500)).await; 679 680 let count_res_2 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await)) 681 .bearer_auth(&user_a_jwt) 682 .send().await.expect("getUnreadCount 2 failed"); 683 let count_body_2: Value = count_res_2.json().await.expect("count 2 not json"); 684 assert_eq!(count_body_2["count"], 3, "Unread count was not 3 after actions"); 685 686 let list_res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", base_url().await)) 687 .bearer_auth(&user_a_jwt) 688 .send().await.expect("listNotifications failed"); 689 let list_body: Value = list_res.json().await.expect("list not json"); 690 691 let notifs = list_body["notifications"].as_array().expect("notifications not array"); 692 assert_eq!(notifs.len(), 3, "Notification list did not have 3 items"); 693 694 let has_follow = notifs.iter().any(|n| n["reason"] == "follow" && n["author"]["did"] == user_b_did); 695 let has_like = notifs.iter().any(|n| n["reason"] == "like" && n["author"]["did"] == user_b_did); 696 let has_reply = notifs.iter().any(|n| n["reason"] == "reply" && n["author"]["did"] == user_b_did); 697 698 assert!(has_follow, "Notification list missing 'follow'"); 699 assert!(has_like, "Notification list missing 'like'"); 700 assert!(has_reply, "Notification list missing 'reply'"); 701 702 let count_res_3 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", base_url().await)) 703 .bearer_auth(&user_a_jwt) 704 .send().await.expect("getUnreadCount 3 failed"); 705 let count_body_3: Value = count_res_3.json().await.expect("count 3 not json"); 706 assert_eq!(count_body_3["count"], 0, "Unread count was not 0 after list"); 707} 708 709 710#[tokio::test] 711async fn test_mute_lifecycle_filters_feed() { 712 let client = client(); 713 714 let (user_a_did, user_a_jwt) = setup_new_user("user-a-mute").await; 715 let (user_b_did, user_b_jwt) = setup_new_user("user-b-mute").await; 716 717 let (post_uri, _) = create_record_as( 718 &client, 719 &user_b_jwt, 720 &user_b_did, 721 "app.bsky.feed.post", 722 json!({ 723 "$type": "app.bsky.feed.post", 724 "text": "A post from User B", 725 "createdAt": Utc::now().to_rfc3339() 726 }), 727 ).await; 728 729 let feed_params_1 = [("actor", &user_b_did)]; 730 let feed_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await)) 731 .query(&feed_params_1) 732 .bearer_auth(&user_a_jwt) 733 .send().await.expect("getAuthorFeed 1 failed"); 734 let feed_body_1: Value = feed_res_1.json().await.expect("feed 1 not json"); 735 736 let feed_1 = feed_body_1["feed"].as_array().expect("feed 1 not array"); 737 let found_post_1 = feed_1.iter().any(|p| p["post"]["uri"] == post_uri); 738 assert!(found_post_1, "User B's post was not in their feed before mute"); 739 740 let (mute_uri, _) = create_record_as( 741 &client, &user_a_jwt, &user_a_did, 742 "app.bsky.graph.mute", 743 json!({ 744 "$type": "app.bsky.graph.mute", 745 "subject": user_b_did, 746 "createdAt": Utc::now().to_rfc3339() 747 }), 748 ).await; 749 let mute_rkey = mute_uri.split('/').last().unwrap(); 750 751 let feed_params_2 = [("actor", &user_b_did)]; 752 let feed_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await)) 753 .query(&feed_params_2) 754 .bearer_auth(&user_a_jwt) 755 .send().await.expect("getAuthorFeed 2 failed"); 756 let feed_body_2: Value = feed_res_2.json().await.expect("feed 2 not json"); 757 758 let feed_2 = feed_body_2["feed"].as_array().expect("feed 2 not array"); 759 assert!(feed_2.is_empty(), "User B's feed was not empty after mute"); 760 761 delete_record_as( 762 &client, &user_a_jwt, &user_a_did, 763 "app.bsky.graph.mute", 764 mute_rkey, 765 ).await; 766 767 let feed_params_3 = [("actor", &user_b_did)]; 768 let feed_res_3 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", base_url().await)) 769 .query(&feed_params_3) 770 .bearer_auth(&user_a_jwt) 771 .send().await.expect("getAuthorFeed 3 failed"); 772 let feed_body_3: Value = feed_res_3.json().await.expect("feed 3 not json"); 773 774 let feed_3 = feed_body_3["feed"].as_array().expect("feed 3 not array"); 775 let found_post_3 = feed_3.iter().any(|p| p["post"]["uri"] == post_uri); 776 assert!(found_post_3, "User B's post did not reappear after unmute"); 777} 778 779 780#[tokio::test] 781async fn test_record_update_conflict_lifecycle() { 782 let client = client(); 783 784 let (user_did, user_jwt) = setup_new_user("user-conflict").await; 785 786 let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 787 .query(&[ 788 ("repo", &user_did), 789 ("collection", &"app.bsky.actor.profile".to_string()), 790 ("rkey", &"self".to_string()), 791 ]) 792 .send().await.expect("getRecord failed"); 793 let get_body: Value = get_res.json().await.expect("getRecord not json"); 794 let cid_v1 = get_body["cid"].as_str().expect("Profile v1 had no CID").to_string(); 795 796 let update_payload_v2 = json!({ 797 "repo": user_did, 798 "collection": "app.bsky.actor.profile", 799 "rkey": "self", 800 "record": { 801 "$type": "app.bsky.actor.profile", 802 "displayName": "Updated Name (v2)" 803 }, 804 "swapCommit": cid_v1 // <-- Correctly point to v1 805 }); 806 let update_res_v2 = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 807 .bearer_auth(&user_jwt) 808 .json(&update_payload_v2) 809 .send().await.expect("putRecord v2 failed"); 810 assert_eq!(update_res_v2.status(), StatusCode::OK, "v2 update failed"); 811 let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json"); 812 let cid_v2 = update_body_v2["cid"].as_str().expect("v2 response had no CID").to_string(); 813 814 let update_payload_v3_stale = json!({ 815 "repo": user_did, 816 "collection": "app.bsky.actor.profile", 817 "rkey": "self", 818 "record": { 819 "$type": "app.bsky.actor.profile", 820 "displayName": "Stale Update (v3)" 821 }, 822 "swapCommit": cid_v1 823 }); 824 let update_res_v3_stale = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 825 .bearer_auth(&user_jwt) 826 .json(&update_payload_v3_stale) 827 .send().await.expect("putRecord v3 (stale) failed"); 828 829 assert_eq!( 830 update_res_v3_stale.status(), 831 StatusCode::CONFLICT, 832 "Stale update did not cause a 409 Conflict" 833 ); 834 835 let update_payload_v3_good = json!({ 836 "repo": user_did, 837 "collection": "app.bsky.actor.profile", 838 "rkey": "self", 839 "record": { 840 "$type": "app.bsky.actor.profile", 841 "displayName": "Good Update (v3)" 842 }, 843 "swapCommit": cid_v2 // <-- Correct 844 }); 845 let update_res_v3_good = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 846 .bearer_auth(&user_jwt) 847 .json(&update_payload_v3_good) 848 .send().await.expect("putRecord v3 (good) failed"); 849 850 assert_eq!(update_res_v3_good.status(), StatusCode::OK, "v3 (good) update failed"); 851} 852 853 854#[tokio::test] 855async fn test_complex_thread_deletion_lifecycle() { 856 let client = client(); 857 858 let (user_a_did, user_a_jwt) = setup_new_user("user-a-thread").await; 859 let (user_b_did, user_b_jwt) = setup_new_user("user-b-thread").await; 860 let (user_c_did, user_c_jwt) = setup_new_user("user-c-thread").await; 861 862 let (p1_uri, p1_cid) = create_record_as( 863 &client, &user_a_jwt, &user_a_did, 864 "app.bsky.feed.post", 865 json!({ 866 "$type": "app.bsky.feed.post", 867 "text": "P1 (Root)", 868 "createdAt": Utc::now().to_rfc3339() 869 }), 870 ).await; 871 let p1_ref = json!({ "uri": p1_uri.clone(), "cid": p1_cid.clone() }); 872 873 let (p2_uri, p2_cid) = create_record_as( 874 &client, &user_b_jwt, &user_b_did, 875 "app.bsky.feed.post", 876 json!({ 877 "$type": "app.bsky.feed.post", 878 "text": "P2 (Reply)", 879 "reply": { "root": p1_ref.clone(), "parent": p1_ref.clone() }, 880 "createdAt": Utc::now().to_rfc3339() 881 }), 882 ).await; 883 let p2_ref = json!({ "uri": p2_uri.clone(), "cid": p2_cid.clone() }); 884 let p2_rkey = p2_uri.split('/').last().unwrap().to_string(); 885 886 let (p3_uri, _) = create_record_as( 887 &client, &user_c_jwt, &user_c_did, 888 "app.bsky.feed.post", 889 json!({ 890 "$type": "app.bsky.feed.post", 891 "text": "P3 (Grandchild)", 892 "reply": { "root": p1_ref.clone(), "parent": p2_ref.clone() }, 893 "createdAt": Utc::now().to_rfc3339() 894 }), 895 ).await; 896 897 let thread_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await)) 898 .query(&[("uri", &p1_uri)]) 899 .bearer_auth(&user_a_jwt) 900 .send().await.expect("getThread 1 failed"); 901 let thread_body_1: Value = thread_res_1.json().await.expect("thread 1 not json"); 902 903 let p1_replies = thread_body_1["thread"]["replies"].as_array().unwrap(); 904 assert_eq!(p1_replies.len(), 1, "P1 should have 1 reply"); 905 assert_eq!(p1_replies[0]["post"]["uri"], p2_uri, "P1's reply is not P2"); 906 907 let p2_replies = p1_replies[0]["replies"].as_array().unwrap(); 908 assert_eq!(p2_replies.len(), 1, "P2 should have 1 reply"); 909 assert_eq!(p2_replies[0]["post"]["uri"], p3_uri, "P2's reply is not P3"); 910 911 delete_record_as( 912 &client, &user_b_jwt, &user_b_did, 913 "app.bsky.feed.post", 914 &p2_rkey, 915 ).await; 916 917 let thread_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", base_url().await)) 918 .query(&[("uri", &p1_uri)]) 919 .bearer_auth(&user_a_jwt) 920 .send().await.expect("getThread 2 failed"); 921 let thread_body_2: Value = thread_res_2.json().await.expect("thread 2 not json"); 922 923 let p1_replies_2 = thread_body_2["thread"]["replies"].as_array().unwrap(); 924 assert_eq!(p1_replies_2.len(), 1, "P1 should still have 1 reply (the deleted one)"); 925 926 let deleted_post = &p1_replies_2[0]; 927 assert_eq!( 928 deleted_post["$type"], "app.bsky.feed.defs#notFoundPost", 929 "P2 did not appear as a notFoundPost" 930 ); 931 assert_eq!(deleted_post["uri"], p2_uri, "notFoundPost URI does not match P2"); 932 933 let p3_reply = deleted_post["replies"].as_array().unwrap(); 934 assert_eq!(p3_reply.len(), 1, "notFoundPost should still have P3 as a reply"); 935 assert_eq!(p3_reply[0]["post"]["uri"], p3_uri, "The reply to the deleted post is not P3"); 936}