this repo has no description
1mod common; 2mod helpers; 3use common::*; 4use helpers::*; 5use reqwest::StatusCode; 6use reqwest::header; 7use serde_json::{Value, json}; 8 9#[tokio::test] 10async fn test_get_latest_commit_success() { 11 let client = client(); 12 let (_, did) = create_account_and_login(&client).await; 13 let params = [("did", did.as_str())]; 14 let res = client 15 .get(format!( 16 "{}/xrpc/com.atproto.sync.getLatestCommit", 17 base_url().await 18 )) 19 .query(&params) 20 .send() 21 .await 22 .expect("Failed to send request"); 23 assert_eq!(res.status(), StatusCode::OK); 24 let body: Value = res.json().await.expect("Response was not valid JSON"); 25 assert!(body["cid"].is_string()); 26 assert!(body["rev"].is_string()); 27} 28 29#[tokio::test] 30async fn test_get_latest_commit_not_found() { 31 let client = client(); 32 let params = [("did", "did:plc:nonexistent12345")]; 33 let res = client 34 .get(format!( 35 "{}/xrpc/com.atproto.sync.getLatestCommit", 36 base_url().await 37 )) 38 .query(&params) 39 .send() 40 .await 41 .expect("Failed to send request"); 42 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 43 let body: Value = res.json().await.expect("Response was not valid JSON"); 44 assert_eq!(body["error"], "RepoNotFound"); 45} 46 47#[tokio::test] 48async fn test_get_latest_commit_missing_param() { 49 let client = client(); 50 let res = client 51 .get(format!( 52 "{}/xrpc/com.atproto.sync.getLatestCommit", 53 base_url().await 54 )) 55 .send() 56 .await 57 .expect("Failed to send request"); 58 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 59} 60 61#[tokio::test] 62async fn test_list_repos() { 63 let client = client(); 64 let _ = create_account_and_login(&client).await; 65 let res = client 66 .get(format!( 67 "{}/xrpc/com.atproto.sync.listRepos", 68 base_url().await 69 )) 70 .send() 71 .await 72 .expect("Failed to send request"); 73 assert_eq!(res.status(), StatusCode::OK); 74 let body: Value = res.json().await.expect("Response was not valid JSON"); 75 assert!(body["repos"].is_array()); 76 let repos = body["repos"].as_array().unwrap(); 77 assert!(!repos.is_empty()); 78 let repo = &repos[0]; 79 assert!(repo["did"].is_string()); 80 assert!(repo["head"].is_string()); 81 assert!(repo["active"].is_boolean()); 82} 83 84#[tokio::test] 85async fn test_list_repos_with_limit() { 86 let client = client(); 87 let _ = create_account_and_login(&client).await; 88 let _ = create_account_and_login(&client).await; 89 let _ = create_account_and_login(&client).await; 90 let params = [("limit", "2")]; 91 let res = client 92 .get(format!( 93 "{}/xrpc/com.atproto.sync.listRepos", 94 base_url().await 95 )) 96 .query(&params) 97 .send() 98 .await 99 .expect("Failed to send request"); 100 assert_eq!(res.status(), StatusCode::OK); 101 let body: Value = res.json().await.expect("Response was not valid JSON"); 102 let repos = body["repos"].as_array().unwrap(); 103 assert!(repos.len() <= 2); 104} 105 106#[tokio::test] 107async fn test_list_repos_pagination() { 108 let client = client(); 109 let (_, did1) = create_account_and_login(&client).await; 110 let (_, did2) = create_account_and_login(&client).await; 111 let (_, did3) = create_account_and_login(&client).await; 112 let our_dids: std::collections::HashSet<String> = [did1, did2, did3].into_iter().collect(); 113 let mut all_dids_seen: std::collections::HashSet<String> = std::collections::HashSet::new(); 114 let mut cursor: Option<String> = None; 115 let mut page_count = 0; 116 let max_pages = 100; 117 loop { 118 let mut params: Vec<(&str, String)> = vec![("limit".into(), "10".into())]; 119 if let Some(ref c) = cursor { 120 params.push(("cursor", c.clone())); 121 } 122 let res = client 123 .get(format!( 124 "{}/xrpc/com.atproto.sync.listRepos", 125 base_url().await 126 )) 127 .query(&params) 128 .send() 129 .await 130 .expect("Failed to send request"); 131 assert_eq!(res.status(), StatusCode::OK); 132 let body: Value = res.json().await.expect("Response was not valid JSON"); 133 let repos = body["repos"].as_array().unwrap(); 134 for repo in repos { 135 let did = repo["did"].as_str().unwrap().to_string(); 136 assert!( 137 !all_dids_seen.contains(&did), 138 "Pagination returned duplicate DID: {}", 139 did 140 ); 141 all_dids_seen.insert(did); 142 } 143 cursor = body["cursor"].as_str().map(String::from); 144 page_count += 1; 145 if cursor.is_none() || page_count >= max_pages { 146 break; 147 } 148 } 149 for did in &our_dids { 150 assert!( 151 all_dids_seen.contains(did), 152 "Our created DID {} was not found in paginated results", 153 did 154 ); 155 } 156} 157 158#[tokio::test] 159async fn test_get_repo_status_success() { 160 let client = client(); 161 let (_, did) = create_account_and_login(&client).await; 162 let params = [("did", did.as_str())]; 163 let res = client 164 .get(format!( 165 "{}/xrpc/com.atproto.sync.getRepoStatus", 166 base_url().await 167 )) 168 .query(&params) 169 .send() 170 .await 171 .expect("Failed to send request"); 172 assert_eq!(res.status(), StatusCode::OK); 173 let body: Value = res.json().await.expect("Response was not valid JSON"); 174 assert_eq!(body["did"], did); 175 assert_eq!(body["active"], true); 176 assert!(body["rev"].is_string()); 177} 178 179#[tokio::test] 180async fn test_get_repo_status_not_found() { 181 let client = client(); 182 let params = [("did", "did:plc:nonexistent12345")]; 183 let res = client 184 .get(format!( 185 "{}/xrpc/com.atproto.sync.getRepoStatus", 186 base_url().await 187 )) 188 .query(&params) 189 .send() 190 .await 191 .expect("Failed to send request"); 192 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 193 let body: Value = res.json().await.expect("Response was not valid JSON"); 194 assert_eq!(body["error"], "RepoNotFound"); 195} 196 197#[tokio::test] 198async fn test_notify_of_update() { 199 let client = client(); 200 let params = [("hostname", "example.com")]; 201 let res = client 202 .post(format!( 203 "{}/xrpc/com.atproto.sync.notifyOfUpdate", 204 base_url().await 205 )) 206 .query(&params) 207 .send() 208 .await 209 .expect("Failed to send request"); 210 assert_eq!(res.status(), StatusCode::OK); 211} 212 213#[tokio::test] 214async fn test_request_crawl() { 215 let client = client(); 216 let payload = serde_json::json!({"hostname": "example.com"}); 217 let res = client 218 .post(format!( 219 "{}/xrpc/com.atproto.sync.requestCrawl", 220 base_url().await 221 )) 222 .json(&payload) 223 .send() 224 .await 225 .expect("Failed to send request"); 226 assert_eq!(res.status(), StatusCode::OK); 227} 228 229#[tokio::test] 230async fn test_get_repo_success() { 231 let client = client(); 232 let (access_jwt, did) = create_account_and_login(&client).await; 233 let post_payload = serde_json::json!({ 234 "repo": did, 235 "collection": "app.bsky.feed.post", 236 "record": { 237 "$type": "app.bsky.feed.post", 238 "text": "Test post for getRepo", 239 "createdAt": chrono::Utc::now().to_rfc3339() 240 } 241 }); 242 let _ = client 243 .post(format!( 244 "{}/xrpc/com.atproto.repo.createRecord", 245 base_url().await 246 )) 247 .bearer_auth(&access_jwt) 248 .json(&post_payload) 249 .send() 250 .await 251 .expect("Failed to create record"); 252 let params = [("did", did.as_str())]; 253 let res = client 254 .get(format!( 255 "{}/xrpc/com.atproto.sync.getRepo", 256 base_url().await 257 )) 258 .query(&params) 259 .send() 260 .await 261 .expect("Failed to send request"); 262 assert_eq!(res.status(), StatusCode::OK); 263 assert_eq!( 264 res.headers() 265 .get("content-type") 266 .and_then(|h| h.to_str().ok()), 267 Some("application/vnd.ipld.car") 268 ); 269 let body = res.bytes().await.expect("Failed to get body"); 270 assert!(!body.is_empty()); 271} 272 273#[tokio::test] 274async fn test_get_repo_not_found() { 275 let client = client(); 276 let params = [("did", "did:plc:nonexistent12345")]; 277 let res = client 278 .get(format!( 279 "{}/xrpc/com.atproto.sync.getRepo", 280 base_url().await 281 )) 282 .query(&params) 283 .send() 284 .await 285 .expect("Failed to send request"); 286 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 287 let body: Value = res.json().await.expect("Response was not valid JSON"); 288 assert_eq!(body["error"], "RepoNotFound"); 289} 290 291#[tokio::test] 292async fn test_get_record_sync_success() { 293 let client = client(); 294 let (access_jwt, did) = create_account_and_login(&client).await; 295 let post_payload = serde_json::json!({ 296 "repo": did, 297 "collection": "app.bsky.feed.post", 298 "record": { 299 "$type": "app.bsky.feed.post", 300 "text": "Test post for sync getRecord", 301 "createdAt": chrono::Utc::now().to_rfc3339() 302 } 303 }); 304 let create_res = client 305 .post(format!( 306 "{}/xrpc/com.atproto.repo.createRecord", 307 base_url().await 308 )) 309 .bearer_auth(&access_jwt) 310 .json(&post_payload) 311 .send() 312 .await 313 .expect("Failed to create record"); 314 let create_body: Value = create_res.json().await.expect("Invalid JSON"); 315 let uri = create_body["uri"].as_str().expect("No URI"); 316 let rkey = uri.split('/').last().expect("Invalid URI"); 317 let params = [ 318 ("did", did.as_str()), 319 ("collection", "app.bsky.feed.post"), 320 ("rkey", rkey), 321 ]; 322 let res = client 323 .get(format!( 324 "{}/xrpc/com.atproto.sync.getRecord", 325 base_url().await 326 )) 327 .query(&params) 328 .send() 329 .await 330 .expect("Failed to send request"); 331 assert_eq!(res.status(), StatusCode::OK); 332 assert_eq!( 333 res.headers() 334 .get("content-type") 335 .and_then(|h| h.to_str().ok()), 336 Some("application/vnd.ipld.car") 337 ); 338 let body = res.bytes().await.expect("Failed to get body"); 339 assert!(!body.is_empty()); 340} 341 342#[tokio::test] 343async fn test_get_record_sync_not_found() { 344 let client = client(); 345 let (_, did) = create_account_and_login(&client).await; 346 let params = [ 347 ("did", did.as_str()), 348 ("collection", "app.bsky.feed.post"), 349 ("rkey", "nonexistent12345"), 350 ]; 351 let res = client 352 .get(format!( 353 "{}/xrpc/com.atproto.sync.getRecord", 354 base_url().await 355 )) 356 .query(&params) 357 .send() 358 .await 359 .expect("Failed to send request"); 360 assert_eq!(res.status(), StatusCode::NOT_FOUND); 361 let body: Value = res.json().await.expect("Response was not valid JSON"); 362 assert_eq!(body["error"], "RecordNotFound"); 363} 364 365#[tokio::test] 366async fn test_get_blocks_success() { 367 let client = client(); 368 let (_, did) = create_account_and_login(&client).await; 369 let params = [("did", did.as_str())]; 370 let latest_res = client 371 .get(format!( 372 "{}/xrpc/com.atproto.sync.getLatestCommit", 373 base_url().await 374 )) 375 .query(&params) 376 .send() 377 .await 378 .expect("Failed to get latest commit"); 379 let latest_body: Value = latest_res.json().await.expect("Invalid JSON"); 380 let root_cid = latest_body["cid"].as_str().expect("No CID"); 381 let url = format!( 382 "{}/xrpc/com.atproto.sync.getBlocks?did={}&cids={}", 383 base_url().await, 384 did, 385 root_cid 386 ); 387 let res = client 388 .get(&url) 389 .send() 390 .await 391 .expect("Failed to send request"); 392 assert_eq!(res.status(), StatusCode::OK); 393 assert_eq!( 394 res.headers() 395 .get("content-type") 396 .and_then(|h| h.to_str().ok()), 397 Some("application/vnd.ipld.car") 398 ); 399} 400 401#[tokio::test] 402async fn test_get_blocks_not_found() { 403 let client = client(); 404 let url = format!( 405 "{}/xrpc/com.atproto.sync.getBlocks?did=did:plc:nonexistent12345&cids=bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku", 406 base_url().await 407 ); 408 let res = client 409 .get(&url) 410 .send() 411 .await 412 .expect("Failed to send request"); 413 assert_eq!(res.status(), StatusCode::BAD_REQUEST); 414} 415 416#[tokio::test] 417async fn test_sync_record_lifecycle() { 418 let client = client(); 419 let (did, jwt) = setup_new_user("sync-record-lifecycle").await; 420 let (post_uri, _post_cid) = create_post(&client, &did, &jwt, "Post for sync record test").await; 421 let post_rkey = post_uri.split('/').last().unwrap(); 422 let sync_record_res = client 423 .get(format!( 424 "{}/xrpc/com.atproto.sync.getRecord", 425 base_url().await 426 )) 427 .query(&[ 428 ("did", did.as_str()), 429 ("collection", "app.bsky.feed.post"), 430 ("rkey", post_rkey), 431 ]) 432 .send() 433 .await 434 .expect("Failed to get sync record"); 435 assert_eq!(sync_record_res.status(), StatusCode::OK); 436 assert_eq!( 437 sync_record_res 438 .headers() 439 .get("content-type") 440 .and_then(|h| h.to_str().ok()), 441 Some("application/vnd.ipld.car") 442 ); 443 let car_bytes = sync_record_res.bytes().await.unwrap(); 444 assert!(!car_bytes.is_empty(), "CAR data should not be empty"); 445 let latest_before = client 446 .get(format!( 447 "{}/xrpc/com.atproto.sync.getLatestCommit", 448 base_url().await 449 )) 450 .query(&[("did", did.as_str())]) 451 .send() 452 .await 453 .expect("Failed to get latest commit"); 454 let latest_before_body: Value = latest_before.json().await.unwrap(); 455 let rev_before = latest_before_body["rev"].as_str().unwrap().to_string(); 456 let (post2_uri, _) = create_post(&client, &did, &jwt, "Second post for sync test").await; 457 let latest_after = client 458 .get(format!( 459 "{}/xrpc/com.atproto.sync.getLatestCommit", 460 base_url().await 461 )) 462 .query(&[("did", did.as_str())]) 463 .send() 464 .await 465 .expect("Failed to get latest commit after"); 466 let latest_after_body: Value = latest_after.json().await.unwrap(); 467 let rev_after = latest_after_body["rev"].as_str().unwrap().to_string(); 468 assert_ne!( 469 rev_before, rev_after, 470 "Revision should change after new record" 471 ); 472 let delete_payload = json!({ 473 "repo": did, 474 "collection": "app.bsky.feed.post", 475 "rkey": post_rkey 476 }); 477 let delete_res = client 478 .post(format!( 479 "{}/xrpc/com.atproto.repo.deleteRecord", 480 base_url().await 481 )) 482 .bearer_auth(&jwt) 483 .json(&delete_payload) 484 .send() 485 .await 486 .expect("Failed to delete record"); 487 assert_eq!(delete_res.status(), StatusCode::OK); 488 let sync_deleted_res = client 489 .get(format!( 490 "{}/xrpc/com.atproto.sync.getRecord", 491 base_url().await 492 )) 493 .query(&[ 494 ("did", did.as_str()), 495 ("collection", "app.bsky.feed.post"), 496 ("rkey", post_rkey), 497 ]) 498 .send() 499 .await 500 .expect("Failed to check deleted record via sync"); 501 assert_eq!( 502 sync_deleted_res.status(), 503 StatusCode::NOT_FOUND, 504 "Deleted record should return 404 via sync.getRecord" 505 ); 506 let post2_rkey = post2_uri.split('/').last().unwrap(); 507 let sync_post2_res = client 508 .get(format!( 509 "{}/xrpc/com.atproto.sync.getRecord", 510 base_url().await 511 )) 512 .query(&[ 513 ("did", did.as_str()), 514 ("collection", "app.bsky.feed.post"), 515 ("rkey", post2_rkey), 516 ]) 517 .send() 518 .await 519 .expect("Failed to get second post via sync"); 520 assert_eq!( 521 sync_post2_res.status(), 522 StatusCode::OK, 523 "Second post should still be accessible" 524 ); 525} 526 527#[tokio::test] 528async fn test_sync_repo_export_lifecycle() { 529 let client = client(); 530 let (did, jwt) = setup_new_user("sync-repo-export").await; 531 let profile_payload = json!({ 532 "repo": did, 533 "collection": "app.bsky.actor.profile", 534 "rkey": "self", 535 "record": { 536 "$type": "app.bsky.actor.profile", 537 "displayName": "Sync Export User" 538 } 539 }); 540 let profile_res = client 541 .post(format!( 542 "{}/xrpc/com.atproto.repo.putRecord", 543 base_url().await 544 )) 545 .bearer_auth(&jwt) 546 .json(&profile_payload) 547 .send() 548 .await 549 .expect("Failed to create profile"); 550 assert_eq!(profile_res.status(), StatusCode::OK); 551 for i in 0..3 { 552 tokio::time::sleep(std::time::Duration::from_millis(100)).await; 553 create_post(&client, &did, &jwt, &format!("Export test post {}", i)).await; 554 } 555 let blob_data = b"blob data for sync export test"; 556 let upload_res = client 557 .post(format!( 558 "{}/xrpc/com.atproto.repo.uploadBlob", 559 base_url().await 560 )) 561 .header(header::CONTENT_TYPE, "application/octet-stream") 562 .bearer_auth(&jwt) 563 .body(blob_data.to_vec()) 564 .send() 565 .await 566 .expect("Failed to upload blob"); 567 assert_eq!(upload_res.status(), StatusCode::OK); 568 let blob_body: Value = upload_res.json().await.unwrap(); 569 let blob_cid = blob_body["blob"]["ref"]["$link"] 570 .as_str() 571 .unwrap() 572 .to_string(); 573 let repo_status_res = client 574 .get(format!( 575 "{}/xrpc/com.atproto.sync.getRepoStatus", 576 base_url().await 577 )) 578 .query(&[("did", did.as_str())]) 579 .send() 580 .await 581 .expect("Failed to get repo status"); 582 assert_eq!(repo_status_res.status(), StatusCode::OK); 583 let status_body: Value = repo_status_res.json().await.unwrap(); 584 assert_eq!(status_body["did"], did); 585 assert_eq!(status_body["active"], true); 586 let get_repo_res = client 587 .get(format!( 588 "{}/xrpc/com.atproto.sync.getRepo", 589 base_url().await 590 )) 591 .query(&[("did", did.as_str())]) 592 .send() 593 .await 594 .expect("Failed to get full repo"); 595 assert_eq!(get_repo_res.status(), StatusCode::OK); 596 assert_eq!( 597 get_repo_res 598 .headers() 599 .get("content-type") 600 .and_then(|h| h.to_str().ok()), 601 Some("application/vnd.ipld.car") 602 ); 603 let repo_car = get_repo_res.bytes().await.unwrap(); 604 assert!( 605 repo_car.len() > 100, 606 "Repo CAR should have substantial data" 607 ); 608 let list_blobs_res = client 609 .get(format!( 610 "{}/xrpc/com.atproto.sync.listBlobs", 611 base_url().await 612 )) 613 .query(&[("did", did.as_str())]) 614 .send() 615 .await 616 .expect("Failed to list blobs"); 617 assert_eq!(list_blobs_res.status(), StatusCode::OK); 618 let blobs_body: Value = list_blobs_res.json().await.unwrap(); 619 let cids = blobs_body["cids"].as_array().unwrap(); 620 assert!(!cids.is_empty(), "Should have at least one blob"); 621 let get_blob_res = client 622 .get(format!( 623 "{}/xrpc/com.atproto.sync.getBlob", 624 base_url().await 625 )) 626 .query(&[("did", did.as_str()), ("cid", &blob_cid)]) 627 .send() 628 .await 629 .expect("Failed to get blob"); 630 assert_eq!(get_blob_res.status(), StatusCode::OK); 631 let retrieved_blob = get_blob_res.bytes().await.unwrap(); 632 assert_eq!( 633 retrieved_blob.as_ref(), 634 blob_data, 635 "Retrieved blob should match uploaded data" 636 ); 637 let latest_commit_res = client 638 .get(format!( 639 "{}/xrpc/com.atproto.sync.getLatestCommit", 640 base_url().await 641 )) 642 .query(&[("did", did.as_str())]) 643 .send() 644 .await 645 .expect("Failed to get latest commit"); 646 assert_eq!(latest_commit_res.status(), StatusCode::OK); 647 let commit_body: Value = latest_commit_res.json().await.unwrap(); 648 let root_cid = commit_body["cid"].as_str().unwrap(); 649 let get_blocks_url = format!( 650 "{}/xrpc/com.atproto.sync.getBlocks?did={}&cids={}", 651 base_url().await, 652 did, 653 root_cid 654 ); 655 let get_blocks_res = client 656 .get(&get_blocks_url) 657 .send() 658 .await 659 .expect("Failed to get blocks"); 660 assert_eq!(get_blocks_res.status(), StatusCode::OK); 661 assert_eq!( 662 get_blocks_res 663 .headers() 664 .get("content-type") 665 .and_then(|h| h.to_str().ok()), 666 Some("application/vnd.ipld.car") 667 ); 668}