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(¶ms)
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(¶ms)
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(¶ms)
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(¶ms)
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(¶ms)
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(¶ms)
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(¶ms)
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(¶ms)
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(¶ms)
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(¶ms)
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(¶ms)
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(¶ms)
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}