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