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 = format!("blob data for sync export test {}", uuid::Uuid::new_v4());
556 let blob_bytes = blob_data.as_bytes().to_vec();
557 let upload_res = client
558 .post(format!(
559 "{}/xrpc/com.atproto.repo.uploadBlob",
560 base_url().await
561 ))
562 .header(header::CONTENT_TYPE, "application/octet-stream")
563 .bearer_auth(&jwt)
564 .body(blob_bytes.clone())
565 .send()
566 .await
567 .expect("Failed to upload blob");
568 assert_eq!(upload_res.status(), StatusCode::OK);
569 let blob_body: Value = upload_res.json().await.unwrap();
570 let blob_cid = blob_body["blob"]["ref"]["$link"]
571 .as_str()
572 .unwrap()
573 .to_string();
574 let repo_status_res = client
575 .get(format!(
576 "{}/xrpc/com.atproto.sync.getRepoStatus",
577 base_url().await
578 ))
579 .query(&[("did", did.as_str())])
580 .send()
581 .await
582 .expect("Failed to get repo status");
583 assert_eq!(repo_status_res.status(), StatusCode::OK);
584 let status_body: Value = repo_status_res.json().await.unwrap();
585 assert_eq!(status_body["did"], did);
586 assert_eq!(status_body["active"], true);
587 let get_repo_res = client
588 .get(format!(
589 "{}/xrpc/com.atproto.sync.getRepo",
590 base_url().await
591 ))
592 .query(&[("did", did.as_str())])
593 .send()
594 .await
595 .expect("Failed to get full repo");
596 assert_eq!(get_repo_res.status(), StatusCode::OK);
597 assert_eq!(
598 get_repo_res
599 .headers()
600 .get("content-type")
601 .and_then(|h| h.to_str().ok()),
602 Some("application/vnd.ipld.car")
603 );
604 let repo_car = get_repo_res.bytes().await.unwrap();
605 assert!(
606 repo_car.len() > 100,
607 "Repo CAR should have substantial data"
608 );
609 let list_blobs_res = client
610 .get(format!(
611 "{}/xrpc/com.atproto.sync.listBlobs",
612 base_url().await
613 ))
614 .query(&[("did", did.as_str())])
615 .send()
616 .await
617 .expect("Failed to list blobs");
618 assert_eq!(list_blobs_res.status(), StatusCode::OK);
619 let blobs_body: Value = list_blobs_res.json().await.unwrap();
620 let cids = blobs_body["cids"].as_array().unwrap();
621 assert!(!cids.is_empty(), "Should have at least one blob");
622 let get_blob_res = client
623 .get(format!(
624 "{}/xrpc/com.atproto.sync.getBlob",
625 base_url().await
626 ))
627 .query(&[("did", did.as_str()), ("cid", &blob_cid)])
628 .send()
629 .await
630 .expect("Failed to get blob");
631 assert_eq!(get_blob_res.status(), StatusCode::OK);
632 let retrieved_blob = get_blob_res.bytes().await.unwrap();
633 assert_eq!(
634 retrieved_blob.as_ref(),
635 blob_bytes.as_slice(),
636 "Retrieved blob should match uploaded data"
637 );
638 let latest_commit_res = client
639 .get(format!(
640 "{}/xrpc/com.atproto.sync.getLatestCommit",
641 base_url().await
642 ))
643 .query(&[("did", did.as_str())])
644 .send()
645 .await
646 .expect("Failed to get latest commit");
647 assert_eq!(latest_commit_res.status(), StatusCode::OK);
648 let commit_body: Value = latest_commit_res.json().await.unwrap();
649 let root_cid = commit_body["cid"].as_str().unwrap();
650 let get_blocks_url = format!(
651 "{}/xrpc/com.atproto.sync.getBlocks?did={}&cids={}",
652 base_url().await,
653 did,
654 root_cid
655 );
656 let get_blocks_res = client
657 .get(&get_blocks_url)
658 .send()
659 .await
660 .expect("Failed to get blocks");
661 assert_eq!(get_blocks_res.status(), StatusCode::OK);
662 assert_eq!(
663 get_blocks_res
664 .headers()
665 .get("content-type")
666 .and_then(|h| h.to_str().ok()),
667 Some("application/vnd.ipld.car")
668 );
669}