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] 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}