this repo has no description
1mod common; 2use common::*; 3 4use chrono::Utc; 5use reqwest; 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] 56#[ignore] 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] 224#[ignore] 225async fn test_record_update_conflict_lifecycle() { 226 let client = client(); 227 let (user_did, user_jwt) = setup_new_user("user-conflict").await; 228 229 let profile_payload = json!({ 230 "repo": user_did, 231 "collection": "app.bsky.actor.profile", 232 "rkey": "self", 233 "record": { 234 "$type": "app.bsky.actor.profile", 235 "displayName": "Original Name" 236 } 237 }); 238 let create_res = client 239 .post(format!( 240 "{}/xrpc/com.atproto.repo.putRecord", 241 base_url().await 242 )) 243 .bearer_auth(&user_jwt) 244 .json(&profile_payload) 245 .send() 246 .await 247 .expect("create profile failed"); 248 249 if create_res.status() != reqwest::StatusCode::OK { 250 return; 251 } 252 253 let get_res = client 254 .get(format!( 255 "{}/xrpc/com.atproto.repo.getRecord", 256 base_url().await 257 )) 258 .query(&[ 259 ("repo", &user_did), 260 ("collection", &"app.bsky.actor.profile".to_string()), 261 ("rkey", &"self".to_string()), 262 ]) 263 .send() 264 .await 265 .expect("getRecord failed"); 266 let get_body: Value = get_res.json().await.expect("getRecord not json"); 267 let cid_v1 = get_body["cid"] 268 .as_str() 269 .expect("Profile v1 had no CID") 270 .to_string(); 271 272 let update_payload_v2 = json!({ 273 "repo": user_did, 274 "collection": "app.bsky.actor.profile", 275 "rkey": "self", 276 "record": { 277 "$type": "app.bsky.actor.profile", 278 "displayName": "Updated Name (v2)" 279 }, 280 "swapCommit": cid_v1 // <-- Correctly point to v1 281 }); 282 let update_res_v2 = client 283 .post(format!( 284 "{}/xrpc/com.atproto.repo.putRecord", 285 base_url().await 286 )) 287 .bearer_auth(&user_jwt) 288 .json(&update_payload_v2) 289 .send() 290 .await 291 .expect("putRecord v2 failed"); 292 assert_eq!( 293 update_res_v2.status(), 294 reqwest::StatusCode::OK, 295 "v2 update failed" 296 ); 297 let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json"); 298 let cid_v2 = update_body_v2["cid"] 299 .as_str() 300 .expect("v2 response had no CID") 301 .to_string(); 302 303 let update_payload_v3_stale = json!({ 304 "repo": user_did, 305 "collection": "app.bsky.actor.profile", 306 "rkey": "self", 307 "record": { 308 "$type": "app.bsky.actor.profile", 309 "displayName": "Stale Update (v3)" 310 }, 311 "swapCommit": cid_v1 312 }); 313 let update_res_v3_stale = client 314 .post(format!( 315 "{}/xrpc/com.atproto.repo.putRecord", 316 base_url().await 317 )) 318 .bearer_auth(&user_jwt) 319 .json(&update_payload_v3_stale) 320 .send() 321 .await 322 .expect("putRecord v3 (stale) failed"); 323 324 assert_eq!( 325 update_res_v3_stale.status(), 326 reqwest::StatusCode::CONFLICT, 327 "Stale update did not cause a 409 Conflict" 328 ); 329 330 let update_payload_v3_good = json!({ 331 "repo": user_did, 332 "collection": "app.bsky.actor.profile", 333 "rkey": "self", 334 "record": { 335 "$type": "app.bsky.actor.profile", 336 "displayName": "Good Update (v3)" 337 }, 338 "swapCommit": cid_v2 // <-- Correct 339 }); 340 let update_res_v3_good = client 341 .post(format!( 342 "{}/xrpc/com.atproto.repo.putRecord", 343 base_url().await 344 )) 345 .bearer_auth(&user_jwt) 346 .json(&update_payload_v3_good) 347 .send() 348 .await 349 .expect("putRecord v3 (good) failed"); 350 351 assert_eq!( 352 update_res_v3_good.status(), 353 reqwest::StatusCode::OK, 354 "v3 (good) update failed" 355 ); 356} 357 358async fn create_post( 359 client: &reqwest::Client, 360 did: &str, 361 jwt: &str, 362 text: &str, 363) -> (String, String) { 364 let collection = "app.bsky.feed.post"; 365 let rkey = format!("e2e_social_{}", Utc::now().timestamp_millis()); 366 let now = Utc::now().to_rfc3339(); 367 368 let create_payload = json!({ 369 "repo": did, 370 "collection": collection, 371 "rkey": rkey, 372 "record": { 373 "$type": collection, 374 "text": text, 375 "createdAt": now 376 } 377 }); 378 379 let create_res = client 380 .post(format!( 381 "{}/xrpc/com.atproto.repo.putRecord", 382 base_url().await 383 )) 384 .bearer_auth(jwt) 385 .json(&create_payload) 386 .send() 387 .await 388 .expect("Failed to send create post request"); 389 390 assert_eq!( 391 create_res.status(), 392 reqwest::StatusCode::OK, 393 "Failed to create post record" 394 ); 395 let create_body: Value = create_res 396 .json() 397 .await 398 .expect("create post response was not JSON"); 399 let uri = create_body["uri"].as_str().unwrap().to_string(); 400 let cid = create_body["cid"].as_str().unwrap().to_string(); 401 (uri, cid) 402} 403 404async fn create_follow( 405 client: &reqwest::Client, 406 follower_did: &str, 407 follower_jwt: &str, 408 followee_did: &str, 409) -> (String, String) { 410 let collection = "app.bsky.graph.follow"; 411 let rkey = format!("e2e_follow_{}", Utc::now().timestamp_millis()); 412 let now = Utc::now().to_rfc3339(); 413 414 let create_payload = json!({ 415 "repo": follower_did, 416 "collection": collection, 417 "rkey": rkey, 418 "record": { 419 "$type": collection, 420 "subject": followee_did, 421 "createdAt": now 422 } 423 }); 424 425 let create_res = client 426 .post(format!( 427 "{}/xrpc/com.atproto.repo.putRecord", 428 base_url().await 429 )) 430 .bearer_auth(follower_jwt) 431 .json(&create_payload) 432 .send() 433 .await 434 .expect("Failed to send create follow request"); 435 436 assert_eq!( 437 create_res.status(), 438 reqwest::StatusCode::OK, 439 "Failed to create follow record" 440 ); 441 let create_body: Value = create_res 442 .json() 443 .await 444 .expect("create follow response was not JSON"); 445 let uri = create_body["uri"].as_str().unwrap().to_string(); 446 let cid = create_body["cid"].as_str().unwrap().to_string(); 447 (uri, cid) 448} 449 450#[tokio::test] 451#[ignore] 452async fn test_social_flow_lifecycle() { 453 let client = client(); 454 455 let (alice_did, alice_jwt) = setup_new_user("alice-social").await; 456 let (bob_did, bob_jwt) = setup_new_user("bob-social").await; 457 458 let (post1_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's first post!").await; 459 460 create_follow(&client, &bob_did, &bob_jwt, &alice_did).await; 461 462 tokio::time::sleep(Duration::from_secs(1)).await; 463 464 let timeline_res_1 = client 465 .get(format!( 466 "{}/xrpc/app.bsky.feed.getTimeline", 467 base_url().await 468 )) 469 .bearer_auth(&bob_jwt) 470 .send() 471 .await 472 .expect("Failed to get timeline (1)"); 473 474 assert_eq!( 475 timeline_res_1.status(), 476 reqwest::StatusCode::OK, 477 "Failed to get timeline (1)" 478 ); 479 let timeline_body_1: Value = timeline_res_1.json().await.expect("Timeline (1) not JSON"); 480 let feed_1 = timeline_body_1["feed"].as_array().unwrap(); 481 assert_eq!(feed_1.len(), 1, "Timeline should have 1 post"); 482 assert_eq!( 483 feed_1[0]["post"]["uri"], post1_uri, 484 "Post URI mismatch in timeline (1)" 485 ); 486 487 let (post2_uri, _) = create_post( 488 &client, 489 &alice_did, 490 &alice_jwt, 491 "Alice's second post, so exciting!", 492 ) 493 .await; 494 495 tokio::time::sleep(Duration::from_secs(1)).await; 496 497 let timeline_res_2 = client 498 .get(format!( 499 "{}/xrpc/app.bsky.feed.getTimeline", 500 base_url().await 501 )) 502 .bearer_auth(&bob_jwt) 503 .send() 504 .await 505 .expect("Failed to get timeline (2)"); 506 507 assert_eq!( 508 timeline_res_2.status(), 509 reqwest::StatusCode::OK, 510 "Failed to get timeline (2)" 511 ); 512 let timeline_body_2: Value = timeline_res_2.json().await.expect("Timeline (2) not JSON"); 513 let feed_2 = timeline_body_2["feed"].as_array().unwrap(); 514 assert_eq!(feed_2.len(), 2, "Timeline should have 2 posts"); 515 assert_eq!( 516 feed_2[0]["post"]["uri"], post2_uri, 517 "Post 2 should be first" 518 ); 519 assert_eq!( 520 feed_2[1]["post"]["uri"], post1_uri, 521 "Post 1 should be second" 522 ); 523 524 let delete_payload = json!({ 525 "repo": alice_did, 526 "collection": "app.bsky.feed.post", 527 "rkey": post1_uri.split('/').last().unwrap() 528 }); 529 let delete_res = client 530 .post(format!( 531 "{}/xrpc/com.atproto.repo.deleteRecord", 532 base_url().await 533 )) 534 .bearer_auth(&alice_jwt) 535 .json(&delete_payload) 536 .send() 537 .await 538 .expect("Failed to send delete request"); 539 assert_eq!( 540 delete_res.status(), 541 reqwest::StatusCode::OK, 542 "Failed to delete record" 543 ); 544 545 tokio::time::sleep(Duration::from_secs(1)).await; 546 547 let timeline_res_3 = client 548 .get(format!( 549 "{}/xrpc/app.bsky.feed.getTimeline", 550 base_url().await 551 )) 552 .bearer_auth(&bob_jwt) 553 .send() 554 .await 555 .expect("Failed to get timeline (3)"); 556 557 assert_eq!( 558 timeline_res_3.status(), 559 reqwest::StatusCode::OK, 560 "Failed to get timeline (3)" 561 ); 562 let timeline_body_3: Value = timeline_res_3.json().await.expect("Timeline (3) not JSON"); 563 let feed_3 = timeline_body_3["feed"].as_array().unwrap(); 564 assert_eq!(feed_3.len(), 1, "Timeline should have 1 post after delete"); 565 assert_eq!( 566 feed_3[0]["post"]["uri"], post2_uri, 567 "Only post 2 should remain" 568 ); 569}