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