this repo has no description
1mod common;
2use common::*;
3
4use base64::Engine;
5use chrono::Utc;
6use reqwest::{self, StatusCode, header};
7use serde_json::{Value, json};
8use std::time::Duration;
9
10async fn setup_new_user(handle_prefix: &str) -> (String, String) {
11 let client = client();
12 let ts = Utc::now().timestamp_millis();
13 let handle = format!("{}-{}.test", handle_prefix, ts);
14 let email = format!("{}-{}@test.com", handle_prefix, ts);
15 let password = "e2e-password-123";
16
17 let create_account_payload = json!({
18 "handle": handle,
19 "email": email,
20 "password": password
21 });
22 let create_res = client
23 .post(format!(
24 "{}/xrpc/com.atproto.server.createAccount",
25 base_url().await
26 ))
27 .json(&create_account_payload)
28 .send()
29 .await
30 .expect("setup_new_user: Failed to send createAccount");
31
32 if create_res.status() != reqwest::StatusCode::OK {
33 panic!(
34 "setup_new_user: Failed to create account: {:?}",
35 create_res.text().await
36 );
37 }
38
39 let create_body: Value = create_res
40 .json()
41 .await
42 .expect("setup_new_user: createAccount response was not JSON");
43
44 let new_did = create_body["did"]
45 .as_str()
46 .expect("setup_new_user: Response had no DID")
47 .to_string();
48 let new_jwt = create_body["accessJwt"]
49 .as_str()
50 .expect("setup_new_user: Response had no accessJwt")
51 .to_string();
52
53 (new_did, new_jwt)
54}
55
56#[tokio::test]
57async fn test_post_crud_lifecycle() {
58 let client = client();
59 let (did, jwt) = setup_new_user("lifecycle-crud").await;
60 let collection = "app.bsky.feed.post";
61
62 let rkey = format!("e2e_lifecycle_{}", Utc::now().timestamp_millis());
63 let now = Utc::now().to_rfc3339();
64
65 let original_text = "Hello from the lifecycle test!";
66 let create_payload = json!({
67 "repo": did,
68 "collection": collection,
69 "rkey": rkey,
70 "record": {
71 "$type": collection,
72 "text": original_text,
73 "createdAt": now
74 }
75 });
76
77 let create_res = client
78 .post(format!(
79 "{}/xrpc/com.atproto.repo.putRecord",
80 base_url().await
81 ))
82 .bearer_auth(&jwt)
83 .json(&create_payload)
84 .send()
85 .await
86 .expect("Failed to send create request");
87
88 if create_res.status() != reqwest::StatusCode::OK {
89 let status = create_res.status();
90 let body = create_res
91 .text()
92 .await
93 .unwrap_or_else(|_| "Could not get body".to_string());
94 panic!(
95 "Failed to create record. Status: {}, Body: {}",
96 status, body
97 );
98 }
99
100 let create_body: Value = create_res
101 .json()
102 .await
103 .expect("create response was not JSON");
104 let uri = create_body["uri"].as_str().unwrap();
105
106 let params = [
107 ("repo", did.as_str()),
108 ("collection", collection),
109 ("rkey", &rkey),
110 ];
111 let get_res = client
112 .get(format!(
113 "{}/xrpc/com.atproto.repo.getRecord",
114 base_url().await
115 ))
116 .query(¶ms)
117 .send()
118 .await
119 .expect("Failed to send get request");
120
121 assert_eq!(
122 get_res.status(),
123 reqwest::StatusCode::OK,
124 "Failed to get record after create"
125 );
126 let get_body: Value = get_res.json().await.expect("get response was not JSON");
127 assert_eq!(get_body["uri"], uri);
128 assert_eq!(get_body["value"]["text"], original_text);
129
130 let updated_text = "This post has been updated.";
131 let update_payload = json!({
132 "repo": did,
133 "collection": collection,
134 "rkey": rkey,
135 "record": {
136 "$type": collection,
137 "text": updated_text,
138 "createdAt": now
139 }
140 });
141
142 let update_res = client
143 .post(format!(
144 "{}/xrpc/com.atproto.repo.putRecord",
145 base_url().await
146 ))
147 .bearer_auth(&jwt)
148 .json(&update_payload)
149 .send()
150 .await
151 .expect("Failed to send update request");
152
153 assert_eq!(
154 update_res.status(),
155 reqwest::StatusCode::OK,
156 "Failed to update record"
157 );
158
159 let get_updated_res = client
160 .get(format!(
161 "{}/xrpc/com.atproto.repo.getRecord",
162 base_url().await
163 ))
164 .query(¶ms)
165 .send()
166 .await
167 .expect("Failed to send get-after-update request");
168
169 assert_eq!(
170 get_updated_res.status(),
171 reqwest::StatusCode::OK,
172 "Failed to get record after update"
173 );
174 let get_updated_body: Value = get_updated_res
175 .json()
176 .await
177 .expect("get-updated response was not JSON");
178 assert_eq!(
179 get_updated_body["value"]["text"], updated_text,
180 "Text was not updated"
181 );
182
183 let delete_payload = json!({
184 "repo": did,
185 "collection": collection,
186 "rkey": rkey
187 });
188
189 let delete_res = client
190 .post(format!(
191 "{}/xrpc/com.atproto.repo.deleteRecord",
192 base_url().await
193 ))
194 .bearer_auth(&jwt)
195 .json(&delete_payload)
196 .send()
197 .await
198 .expect("Failed to send delete request");
199
200 assert_eq!(
201 delete_res.status(),
202 reqwest::StatusCode::OK,
203 "Failed to delete record"
204 );
205
206 let get_deleted_res = client
207 .get(format!(
208 "{}/xrpc/com.atproto.repo.getRecord",
209 base_url().await
210 ))
211 .query(¶ms)
212 .send()
213 .await
214 .expect("Failed to send get-after-delete request");
215
216 assert_eq!(
217 get_deleted_res.status(),
218 reqwest::StatusCode::NOT_FOUND,
219 "Record was found, but it should be deleted"
220 );
221}
222
223#[tokio::test]
224async fn test_record_update_conflict_lifecycle() {
225 let client = client();
226 let (user_did, user_jwt) = setup_new_user("user-conflict").await;
227
228 let profile_payload = json!({
229 "repo": user_did,
230 "collection": "app.bsky.actor.profile",
231 "rkey": "self",
232 "record": {
233 "$type": "app.bsky.actor.profile",
234 "displayName": "Original Name"
235 }
236 });
237 let create_res = client
238 .post(format!(
239 "{}/xrpc/com.atproto.repo.putRecord",
240 base_url().await
241 ))
242 .bearer_auth(&user_jwt)
243 .json(&profile_payload)
244 .send()
245 .await
246 .expect("create profile failed");
247
248 if create_res.status() != reqwest::StatusCode::OK {
249 return;
250 }
251
252 let get_res = client
253 .get(format!(
254 "{}/xrpc/com.atproto.repo.getRecord",
255 base_url().await
256 ))
257 .query(&[
258 ("repo", &user_did),
259 ("collection", &"app.bsky.actor.profile".to_string()),
260 ("rkey", &"self".to_string()),
261 ])
262 .send()
263 .await
264 .expect("getRecord failed");
265 let get_body: Value = get_res.json().await.expect("getRecord not json");
266 let cid_v1 = get_body["cid"]
267 .as_str()
268 .expect("Profile v1 had no CID")
269 .to_string();
270
271 let update_payload_v2 = json!({
272 "repo": user_did,
273 "collection": "app.bsky.actor.profile",
274 "rkey": "self",
275 "record": {
276 "$type": "app.bsky.actor.profile",
277 "displayName": "Updated Name (v2)"
278 },
279 "swapRecord": cid_v1
280 });
281 let update_res_v2 = client
282 .post(format!(
283 "{}/xrpc/com.atproto.repo.putRecord",
284 base_url().await
285 ))
286 .bearer_auth(&user_jwt)
287 .json(&update_payload_v2)
288 .send()
289 .await
290 .expect("putRecord v2 failed");
291 assert_eq!(
292 update_res_v2.status(),
293 reqwest::StatusCode::OK,
294 "v2 update failed"
295 );
296 let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json");
297 let cid_v2 = update_body_v2["cid"]
298 .as_str()
299 .expect("v2 response had no CID")
300 .to_string();
301
302 let update_payload_v3_stale = json!({
303 "repo": user_did,
304 "collection": "app.bsky.actor.profile",
305 "rkey": "self",
306 "record": {
307 "$type": "app.bsky.actor.profile",
308 "displayName": "Stale Update (v3)"
309 },
310 "swapRecord": cid_v1
311 });
312 let update_res_v3_stale = client
313 .post(format!(
314 "{}/xrpc/com.atproto.repo.putRecord",
315 base_url().await
316 ))
317 .bearer_auth(&user_jwt)
318 .json(&update_payload_v3_stale)
319 .send()
320 .await
321 .expect("putRecord v3 (stale) failed");
322
323 assert_eq!(
324 update_res_v3_stale.status(),
325 reqwest::StatusCode::CONFLICT,
326 "Stale update did not cause a 409 Conflict"
327 );
328
329 let update_payload_v3_good = json!({
330 "repo": user_did,
331 "collection": "app.bsky.actor.profile",
332 "rkey": "self",
333 "record": {
334 "$type": "app.bsky.actor.profile",
335 "displayName": "Good Update (v3)"
336 },
337 "swapRecord": cid_v2
338 });
339 let update_res_v3_good = client
340 .post(format!(
341 "{}/xrpc/com.atproto.repo.putRecord",
342 base_url().await
343 ))
344 .bearer_auth(&user_jwt)
345 .json(&update_payload_v3_good)
346 .send()
347 .await
348 .expect("putRecord v3 (good) failed");
349
350 assert_eq!(
351 update_res_v3_good.status(),
352 reqwest::StatusCode::OK,
353 "v3 (good) update failed"
354 );
355}
356
357async fn create_post(
358 client: &reqwest::Client,
359 did: &str,
360 jwt: &str,
361 text: &str,
362) -> (String, String) {
363 let collection = "app.bsky.feed.post";
364 let rkey = format!("e2e_social_{}", Utc::now().timestamp_millis());
365 let now = Utc::now().to_rfc3339();
366
367 let create_payload = json!({
368 "repo": did,
369 "collection": collection,
370 "rkey": rkey,
371 "record": {
372 "$type": collection,
373 "text": text,
374 "createdAt": now
375 }
376 });
377
378 let create_res = client
379 .post(format!(
380 "{}/xrpc/com.atproto.repo.putRecord",
381 base_url().await
382 ))
383 .bearer_auth(jwt)
384 .json(&create_payload)
385 .send()
386 .await
387 .expect("Failed to send create post request");
388
389 assert_eq!(
390 create_res.status(),
391 reqwest::StatusCode::OK,
392 "Failed to create post record"
393 );
394 let create_body: Value = create_res
395 .json()
396 .await
397 .expect("create post response was not JSON");
398 let uri = create_body["uri"].as_str().unwrap().to_string();
399 let cid = create_body["cid"].as_str().unwrap().to_string();
400 (uri, cid)
401}
402
403async fn create_follow(
404 client: &reqwest::Client,
405 follower_did: &str,
406 follower_jwt: &str,
407 followee_did: &str,
408) -> (String, String) {
409 let collection = "app.bsky.graph.follow";
410 let rkey = format!("e2e_follow_{}", Utc::now().timestamp_millis());
411 let now = Utc::now().to_rfc3339();
412
413 let create_payload = json!({
414 "repo": follower_did,
415 "collection": collection,
416 "rkey": rkey,
417 "record": {
418 "$type": collection,
419 "subject": followee_did,
420 "createdAt": now
421 }
422 });
423
424 let create_res = client
425 .post(format!(
426 "{}/xrpc/com.atproto.repo.putRecord",
427 base_url().await
428 ))
429 .bearer_auth(follower_jwt)
430 .json(&create_payload)
431 .send()
432 .await
433 .expect("Failed to send create follow request");
434
435 assert_eq!(
436 create_res.status(),
437 reqwest::StatusCode::OK,
438 "Failed to create follow record"
439 );
440 let create_body: Value = create_res
441 .json()
442 .await
443 .expect("create follow response was not JSON");
444 let uri = create_body["uri"].as_str().unwrap().to_string();
445 let cid = create_body["cid"].as_str().unwrap().to_string();
446 (uri, cid)
447}
448
449#[tokio::test]
450async fn test_social_flow_lifecycle() {
451 let client = client();
452
453 let (alice_did, alice_jwt) = setup_new_user("alice-social").await;
454 let (bob_did, bob_jwt) = setup_new_user("bob-social").await;
455
456 let (post1_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's first post!").await;
457
458 create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
459
460 tokio::time::sleep(Duration::from_secs(1)).await;
461
462 let timeline_res_1 = client
463 .get(format!(
464 "{}/xrpc/app.bsky.feed.getTimeline",
465 base_url().await
466 ))
467 .bearer_auth(&bob_jwt)
468 .send()
469 .await
470 .expect("Failed to get timeline (1)");
471
472 assert_eq!(
473 timeline_res_1.status(),
474 reqwest::StatusCode::OK,
475 "Failed to get timeline (1)"
476 );
477 let timeline_body_1: Value = timeline_res_1.json().await.expect("Timeline (1) not JSON");
478 let feed_1 = timeline_body_1["feed"].as_array().unwrap();
479 assert_eq!(feed_1.len(), 1, "Timeline should have 1 post");
480 assert_eq!(
481 feed_1[0]["post"]["uri"], post1_uri,
482 "Post URI mismatch in timeline (1)"
483 );
484
485 let (post2_uri, _) = create_post(
486 &client,
487 &alice_did,
488 &alice_jwt,
489 "Alice's second post, so exciting!",
490 )
491 .await;
492
493 tokio::time::sleep(Duration::from_secs(1)).await;
494
495 let timeline_res_2 = client
496 .get(format!(
497 "{}/xrpc/app.bsky.feed.getTimeline",
498 base_url().await
499 ))
500 .bearer_auth(&bob_jwt)
501 .send()
502 .await
503 .expect("Failed to get timeline (2)");
504
505 assert_eq!(
506 timeline_res_2.status(),
507 reqwest::StatusCode::OK,
508 "Failed to get timeline (2)"
509 );
510 let timeline_body_2: Value = timeline_res_2.json().await.expect("Timeline (2) not JSON");
511 let feed_2 = timeline_body_2["feed"].as_array().unwrap();
512 assert_eq!(feed_2.len(), 2, "Timeline should have 2 posts");
513 assert_eq!(
514 feed_2[0]["post"]["uri"], post2_uri,
515 "Post 2 should be first"
516 );
517 assert_eq!(
518 feed_2[1]["post"]["uri"], post1_uri,
519 "Post 1 should be second"
520 );
521
522 let delete_payload = json!({
523 "repo": alice_did,
524 "collection": "app.bsky.feed.post",
525 "rkey": post1_uri.split('/').last().unwrap()
526 });
527 let delete_res = client
528 .post(format!(
529 "{}/xrpc/com.atproto.repo.deleteRecord",
530 base_url().await
531 ))
532 .bearer_auth(&alice_jwt)
533 .json(&delete_payload)
534 .send()
535 .await
536 .expect("Failed to send delete request");
537 assert_eq!(
538 delete_res.status(),
539 reqwest::StatusCode::OK,
540 "Failed to delete record"
541 );
542
543 tokio::time::sleep(Duration::from_secs(1)).await;
544
545 let timeline_res_3 = client
546 .get(format!(
547 "{}/xrpc/app.bsky.feed.getTimeline",
548 base_url().await
549 ))
550 .bearer_auth(&bob_jwt)
551 .send()
552 .await
553 .expect("Failed to get timeline (3)");
554
555 assert_eq!(
556 timeline_res_3.status(),
557 reqwest::StatusCode::OK,
558 "Failed to get timeline (3)"
559 );
560 let timeline_body_3: Value = timeline_res_3.json().await.expect("Timeline (3) not JSON");
561 let feed_3 = timeline_body_3["feed"].as_array().unwrap();
562 assert_eq!(feed_3.len(), 1, "Timeline should have 1 post after delete");
563 assert_eq!(
564 feed_3[0]["post"]["uri"], post2_uri,
565 "Only post 2 should remain"
566 );
567}
568
569#[tokio::test]
570async fn test_session_lifecycle_wrong_password() {
571 let client = client();
572 let (_, _) = setup_new_user("session-wrong-pw").await;
573
574 let login_payload = json!({
575 "identifier": format!("session-wrong-pw-{}.test", Utc::now().timestamp_millis()),
576 "password": "wrong-password"
577 });
578
579 let res = client
580 .post(format!(
581 "{}/xrpc/com.atproto.server.createSession",
582 base_url().await
583 ))
584 .json(&login_payload)
585 .send()
586 .await
587 .expect("Failed to send request");
588
589 assert!(
590 res.status() == StatusCode::UNAUTHORIZED || res.status() == StatusCode::BAD_REQUEST,
591 "Expected 401 or 400 for wrong password, got {}",
592 res.status()
593 );
594}
595
596#[tokio::test]
597async fn test_session_lifecycle_multiple_sessions() {
598 let client = client();
599 let ts = Utc::now().timestamp_millis();
600 let handle = format!("multi-session-{}.test", ts);
601 let email = format!("multi-session-{}@test.com", ts);
602 let password = "multi-session-pw";
603
604 let create_payload = json!({
605 "handle": handle,
606 "email": email,
607 "password": password
608 });
609 let create_res = client
610 .post(format!(
611 "{}/xrpc/com.atproto.server.createAccount",
612 base_url().await
613 ))
614 .json(&create_payload)
615 .send()
616 .await
617 .expect("Failed to create account");
618 assert_eq!(create_res.status(), StatusCode::OK);
619
620 let login_payload = json!({
621 "identifier": handle,
622 "password": password
623 });
624
625 let session1_res = client
626 .post(format!(
627 "{}/xrpc/com.atproto.server.createSession",
628 base_url().await
629 ))
630 .json(&login_payload)
631 .send()
632 .await
633 .expect("Failed session 1");
634 assert_eq!(session1_res.status(), StatusCode::OK);
635 let session1: Value = session1_res.json().await.unwrap();
636 let jwt1 = session1["accessJwt"].as_str().unwrap();
637
638 let session2_res = client
639 .post(format!(
640 "{}/xrpc/com.atproto.server.createSession",
641 base_url().await
642 ))
643 .json(&login_payload)
644 .send()
645 .await
646 .expect("Failed session 2");
647 assert_eq!(session2_res.status(), StatusCode::OK);
648 let session2: Value = session2_res.json().await.unwrap();
649 let jwt2 = session2["accessJwt"].as_str().unwrap();
650
651 assert_ne!(jwt1, jwt2, "Sessions should have different tokens");
652
653 let get1 = client
654 .get(format!(
655 "{}/xrpc/com.atproto.server.getSession",
656 base_url().await
657 ))
658 .bearer_auth(jwt1)
659 .send()
660 .await
661 .expect("Failed getSession 1");
662 assert_eq!(get1.status(), StatusCode::OK);
663
664 let get2 = client
665 .get(format!(
666 "{}/xrpc/com.atproto.server.getSession",
667 base_url().await
668 ))
669 .bearer_auth(jwt2)
670 .send()
671 .await
672 .expect("Failed getSession 2");
673 assert_eq!(get2.status(), StatusCode::OK);
674}
675
676#[tokio::test]
677async fn test_session_lifecycle_refresh_invalidates_old() {
678 let client = client();
679 let ts = Utc::now().timestamp_millis();
680 let handle = format!("refresh-inv-{}.test", ts);
681 let email = format!("refresh-inv-{}@test.com", ts);
682 let password = "refresh-inv-pw";
683
684 let create_payload = json!({
685 "handle": handle,
686 "email": email,
687 "password": password
688 });
689 client
690 .post(format!(
691 "{}/xrpc/com.atproto.server.createAccount",
692 base_url().await
693 ))
694 .json(&create_payload)
695 .send()
696 .await
697 .expect("Failed to create account");
698
699 let login_payload = json!({
700 "identifier": handle,
701 "password": password
702 });
703 let login_res = client
704 .post(format!(
705 "{}/xrpc/com.atproto.server.createSession",
706 base_url().await
707 ))
708 .json(&login_payload)
709 .send()
710 .await
711 .expect("Failed login");
712 let login_body: Value = login_res.json().await.unwrap();
713 let refresh_jwt = login_body["refreshJwt"].as_str().unwrap().to_string();
714
715 let refresh_res = client
716 .post(format!(
717 "{}/xrpc/com.atproto.server.refreshSession",
718 base_url().await
719 ))
720 .bearer_auth(&refresh_jwt)
721 .send()
722 .await
723 .expect("Failed first refresh");
724 assert_eq!(refresh_res.status(), StatusCode::OK);
725 let refresh_body: Value = refresh_res.json().await.unwrap();
726 let new_refresh_jwt = refresh_body["refreshJwt"].as_str().unwrap();
727
728 assert_ne!(refresh_jwt, new_refresh_jwt, "Refresh tokens should differ");
729
730 let reuse_res = client
731 .post(format!(
732 "{}/xrpc/com.atproto.server.refreshSession",
733 base_url().await
734 ))
735 .bearer_auth(&refresh_jwt)
736 .send()
737 .await
738 .expect("Failed reuse attempt");
739
740 assert!(
741 reuse_res.status() == StatusCode::UNAUTHORIZED || reuse_res.status() == StatusCode::BAD_REQUEST,
742 "Old refresh token should be invalid after use"
743 );
744}
745
746async fn create_like(
747 client: &reqwest::Client,
748 liker_did: &str,
749 liker_jwt: &str,
750 subject_uri: &str,
751 subject_cid: &str,
752) -> (String, String) {
753 let collection = "app.bsky.feed.like";
754 let rkey = format!("e2e_like_{}", Utc::now().timestamp_millis());
755 let now = Utc::now().to_rfc3339();
756
757 let payload = json!({
758 "repo": liker_did,
759 "collection": collection,
760 "rkey": rkey,
761 "record": {
762 "$type": collection,
763 "subject": {
764 "uri": subject_uri,
765 "cid": subject_cid
766 },
767 "createdAt": now
768 }
769 });
770
771 let res = client
772 .post(format!(
773 "{}/xrpc/com.atproto.repo.putRecord",
774 base_url().await
775 ))
776 .bearer_auth(liker_jwt)
777 .json(&payload)
778 .send()
779 .await
780 .expect("Failed to create like");
781
782 assert_eq!(res.status(), StatusCode::OK, "Failed to create like");
783 let body: Value = res.json().await.expect("Like response not JSON");
784 (
785 body["uri"].as_str().unwrap().to_string(),
786 body["cid"].as_str().unwrap().to_string(),
787 )
788}
789
790async fn create_repost(
791 client: &reqwest::Client,
792 reposter_did: &str,
793 reposter_jwt: &str,
794 subject_uri: &str,
795 subject_cid: &str,
796) -> (String, String) {
797 let collection = "app.bsky.feed.repost";
798 let rkey = format!("e2e_repost_{}", Utc::now().timestamp_millis());
799 let now = Utc::now().to_rfc3339();
800
801 let payload = json!({
802 "repo": reposter_did,
803 "collection": collection,
804 "rkey": rkey,
805 "record": {
806 "$type": collection,
807 "subject": {
808 "uri": subject_uri,
809 "cid": subject_cid
810 },
811 "createdAt": now
812 }
813 });
814
815 let res = client
816 .post(format!(
817 "{}/xrpc/com.atproto.repo.putRecord",
818 base_url().await
819 ))
820 .bearer_auth(reposter_jwt)
821 .json(&payload)
822 .send()
823 .await
824 .expect("Failed to create repost");
825
826 assert_eq!(res.status(), StatusCode::OK, "Failed to create repost");
827 let body: Value = res.json().await.expect("Repost response not JSON");
828 (
829 body["uri"].as_str().unwrap().to_string(),
830 body["cid"].as_str().unwrap().to_string(),
831 )
832}
833
834#[tokio::test]
835async fn test_profile_lifecycle() {
836 let client = client();
837 let (did, jwt) = setup_new_user("profile-lifecycle").await;
838
839 let profile_payload = json!({
840 "repo": did,
841 "collection": "app.bsky.actor.profile",
842 "rkey": "self",
843 "record": {
844 "$type": "app.bsky.actor.profile",
845 "displayName": "Test User",
846 "description": "A test profile for lifecycle testing"
847 }
848 });
849
850 let create_res = client
851 .post(format!(
852 "{}/xrpc/com.atproto.repo.putRecord",
853 base_url().await
854 ))
855 .bearer_auth(&jwt)
856 .json(&profile_payload)
857 .send()
858 .await
859 .expect("Failed to create profile");
860
861 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile");
862 let create_body: Value = create_res.json().await.unwrap();
863 let initial_cid = create_body["cid"].as_str().unwrap().to_string();
864
865 let get_res = client
866 .get(format!(
867 "{}/xrpc/com.atproto.repo.getRecord",
868 base_url().await
869 ))
870 .query(&[
871 ("repo", did.as_str()),
872 ("collection", "app.bsky.actor.profile"),
873 ("rkey", "self"),
874 ])
875 .send()
876 .await
877 .expect("Failed to get profile");
878
879 assert_eq!(get_res.status(), StatusCode::OK);
880 let get_body: Value = get_res.json().await.unwrap();
881 assert_eq!(get_body["value"]["displayName"], "Test User");
882 assert_eq!(get_body["value"]["description"], "A test profile for lifecycle testing");
883
884 let update_payload = json!({
885 "repo": did,
886 "collection": "app.bsky.actor.profile",
887 "rkey": "self",
888 "record": {
889 "$type": "app.bsky.actor.profile",
890 "displayName": "Updated User",
891 "description": "Profile has been updated"
892 },
893 "swapRecord": initial_cid
894 });
895
896 let update_res = client
897 .post(format!(
898 "{}/xrpc/com.atproto.repo.putRecord",
899 base_url().await
900 ))
901 .bearer_auth(&jwt)
902 .json(&update_payload)
903 .send()
904 .await
905 .expect("Failed to update profile");
906
907 assert_eq!(update_res.status(), StatusCode::OK, "Failed to update profile");
908
909 let get_updated_res = client
910 .get(format!(
911 "{}/xrpc/com.atproto.repo.getRecord",
912 base_url().await
913 ))
914 .query(&[
915 ("repo", did.as_str()),
916 ("collection", "app.bsky.actor.profile"),
917 ("rkey", "self"),
918 ])
919 .send()
920 .await
921 .expect("Failed to get updated profile");
922
923 let updated_body: Value = get_updated_res.json().await.unwrap();
924 assert_eq!(updated_body["value"]["displayName"], "Updated User");
925}
926
927#[tokio::test]
928async fn test_reply_thread_lifecycle() {
929 let client = client();
930
931 let (alice_did, alice_jwt) = setup_new_user("alice-thread").await;
932 let (bob_did, bob_jwt) = setup_new_user("bob-thread").await;
933
934 let (root_uri, root_cid) = create_post(&client, &alice_did, &alice_jwt, "This is the root post").await;
935
936 tokio::time::sleep(Duration::from_millis(100)).await;
937
938 let reply_collection = "app.bsky.feed.post";
939 let reply_rkey = format!("e2e_reply_{}", Utc::now().timestamp_millis());
940 let now = Utc::now().to_rfc3339();
941
942 let reply_payload = json!({
943 "repo": bob_did,
944 "collection": reply_collection,
945 "rkey": reply_rkey,
946 "record": {
947 "$type": reply_collection,
948 "text": "This is Bob's reply to Alice",
949 "createdAt": now,
950 "reply": {
951 "root": {
952 "uri": root_uri,
953 "cid": root_cid
954 },
955 "parent": {
956 "uri": root_uri,
957 "cid": root_cid
958 }
959 }
960 }
961 });
962
963 let reply_res = client
964 .post(format!(
965 "{}/xrpc/com.atproto.repo.putRecord",
966 base_url().await
967 ))
968 .bearer_auth(&bob_jwt)
969 .json(&reply_payload)
970 .send()
971 .await
972 .expect("Failed to create reply");
973
974 assert_eq!(reply_res.status(), StatusCode::OK, "Failed to create reply");
975 let reply_body: Value = reply_res.json().await.unwrap();
976 let reply_uri = reply_body["uri"].as_str().unwrap();
977 let reply_cid = reply_body["cid"].as_str().unwrap();
978
979 let get_reply_res = client
980 .get(format!(
981 "{}/xrpc/com.atproto.repo.getRecord",
982 base_url().await
983 ))
984 .query(&[
985 ("repo", bob_did.as_str()),
986 ("collection", reply_collection),
987 ("rkey", reply_rkey.as_str()),
988 ])
989 .send()
990 .await
991 .expect("Failed to get reply");
992
993 assert_eq!(get_reply_res.status(), StatusCode::OK);
994 let reply_record: Value = get_reply_res.json().await.unwrap();
995 assert_eq!(reply_record["value"]["reply"]["root"]["uri"], root_uri);
996 assert_eq!(reply_record["value"]["reply"]["parent"]["uri"], root_uri);
997
998 tokio::time::sleep(Duration::from_millis(100)).await;
999
1000 let nested_reply_rkey = format!("e2e_nested_reply_{}", Utc::now().timestamp_millis());
1001 let nested_payload = json!({
1002 "repo": alice_did,
1003 "collection": reply_collection,
1004 "rkey": nested_reply_rkey,
1005 "record": {
1006 "$type": reply_collection,
1007 "text": "Alice replies to Bob's reply",
1008 "createdAt": Utc::now().to_rfc3339(),
1009 "reply": {
1010 "root": {
1011 "uri": root_uri,
1012 "cid": root_cid
1013 },
1014 "parent": {
1015 "uri": reply_uri,
1016 "cid": reply_cid
1017 }
1018 }
1019 }
1020 });
1021
1022 let nested_res = client
1023 .post(format!(
1024 "{}/xrpc/com.atproto.repo.putRecord",
1025 base_url().await
1026 ))
1027 .bearer_auth(&alice_jwt)
1028 .json(&nested_payload)
1029 .send()
1030 .await
1031 .expect("Failed to create nested reply");
1032
1033 assert_eq!(nested_res.status(), StatusCode::OK, "Failed to create nested reply");
1034}
1035
1036#[tokio::test]
1037async fn test_like_lifecycle() {
1038 let client = client();
1039
1040 let (alice_did, alice_jwt) = setup_new_user("alice-like").await;
1041 let (bob_did, bob_jwt) = setup_new_user("bob-like").await;
1042
1043 let (post_uri, post_cid) = create_post(&client, &alice_did, &alice_jwt, "Like this post!").await;
1044
1045 let (like_uri, _) = create_like(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await;
1046
1047 let like_rkey = like_uri.split('/').last().unwrap();
1048 let get_like_res = client
1049 .get(format!(
1050 "{}/xrpc/com.atproto.repo.getRecord",
1051 base_url().await
1052 ))
1053 .query(&[
1054 ("repo", bob_did.as_str()),
1055 ("collection", "app.bsky.feed.like"),
1056 ("rkey", like_rkey),
1057 ])
1058 .send()
1059 .await
1060 .expect("Failed to get like");
1061
1062 assert_eq!(get_like_res.status(), StatusCode::OK);
1063 let like_body: Value = get_like_res.json().await.unwrap();
1064 assert_eq!(like_body["value"]["subject"]["uri"], post_uri);
1065
1066 let delete_payload = json!({
1067 "repo": bob_did,
1068 "collection": "app.bsky.feed.like",
1069 "rkey": like_rkey
1070 });
1071
1072 let delete_res = client
1073 .post(format!(
1074 "{}/xrpc/com.atproto.repo.deleteRecord",
1075 base_url().await
1076 ))
1077 .bearer_auth(&bob_jwt)
1078 .json(&delete_payload)
1079 .send()
1080 .await
1081 .expect("Failed to delete like");
1082
1083 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete like");
1084
1085 let get_deleted_res = client
1086 .get(format!(
1087 "{}/xrpc/com.atproto.repo.getRecord",
1088 base_url().await
1089 ))
1090 .query(&[
1091 ("repo", bob_did.as_str()),
1092 ("collection", "app.bsky.feed.like"),
1093 ("rkey", like_rkey),
1094 ])
1095 .send()
1096 .await
1097 .expect("Failed to check deleted like");
1098
1099 assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Like should be deleted");
1100}
1101
1102#[tokio::test]
1103async fn test_repost_lifecycle() {
1104 let client = client();
1105
1106 let (alice_did, alice_jwt) = setup_new_user("alice-repost").await;
1107 let (bob_did, bob_jwt) = setup_new_user("bob-repost").await;
1108
1109 let (post_uri, post_cid) = create_post(&client, &alice_did, &alice_jwt, "Repost this!").await;
1110
1111 let (repost_uri, _) = create_repost(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await;
1112
1113 let repost_rkey = repost_uri.split('/').last().unwrap();
1114 let get_repost_res = client
1115 .get(format!(
1116 "{}/xrpc/com.atproto.repo.getRecord",
1117 base_url().await
1118 ))
1119 .query(&[
1120 ("repo", bob_did.as_str()),
1121 ("collection", "app.bsky.feed.repost"),
1122 ("rkey", repost_rkey),
1123 ])
1124 .send()
1125 .await
1126 .expect("Failed to get repost");
1127
1128 assert_eq!(get_repost_res.status(), StatusCode::OK);
1129 let repost_body: Value = get_repost_res.json().await.unwrap();
1130 assert_eq!(repost_body["value"]["subject"]["uri"], post_uri);
1131
1132 let delete_payload = json!({
1133 "repo": bob_did,
1134 "collection": "app.bsky.feed.repost",
1135 "rkey": repost_rkey
1136 });
1137
1138 let delete_res = client
1139 .post(format!(
1140 "{}/xrpc/com.atproto.repo.deleteRecord",
1141 base_url().await
1142 ))
1143 .bearer_auth(&bob_jwt)
1144 .json(&delete_payload)
1145 .send()
1146 .await
1147 .expect("Failed to delete repost");
1148
1149 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete repost");
1150}
1151
1152#[tokio::test]
1153async fn test_unfollow_lifecycle() {
1154 let client = client();
1155
1156 let (alice_did, _alice_jwt) = setup_new_user("alice-unfollow").await;
1157 let (bob_did, bob_jwt) = setup_new_user("bob-unfollow").await;
1158
1159 let (follow_uri, _) = create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
1160
1161 let follow_rkey = follow_uri.split('/').last().unwrap();
1162 let get_follow_res = client
1163 .get(format!(
1164 "{}/xrpc/com.atproto.repo.getRecord",
1165 base_url().await
1166 ))
1167 .query(&[
1168 ("repo", bob_did.as_str()),
1169 ("collection", "app.bsky.graph.follow"),
1170 ("rkey", follow_rkey),
1171 ])
1172 .send()
1173 .await
1174 .expect("Failed to get follow");
1175
1176 assert_eq!(get_follow_res.status(), StatusCode::OK);
1177
1178 let unfollow_payload = json!({
1179 "repo": bob_did,
1180 "collection": "app.bsky.graph.follow",
1181 "rkey": follow_rkey
1182 });
1183
1184 let unfollow_res = client
1185 .post(format!(
1186 "{}/xrpc/com.atproto.repo.deleteRecord",
1187 base_url().await
1188 ))
1189 .bearer_auth(&bob_jwt)
1190 .json(&unfollow_payload)
1191 .send()
1192 .await
1193 .expect("Failed to unfollow");
1194
1195 assert_eq!(unfollow_res.status(), StatusCode::OK, "Failed to unfollow");
1196
1197 let get_deleted_res = client
1198 .get(format!(
1199 "{}/xrpc/com.atproto.repo.getRecord",
1200 base_url().await
1201 ))
1202 .query(&[
1203 ("repo", bob_did.as_str()),
1204 ("collection", "app.bsky.graph.follow"),
1205 ("rkey", follow_rkey),
1206 ])
1207 .send()
1208 .await
1209 .expect("Failed to check deleted follow");
1210
1211 assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Follow should be deleted");
1212}
1213
1214#[tokio::test]
1215async fn test_timeline_after_unfollow() {
1216 let client = client();
1217
1218 let (alice_did, alice_jwt) = setup_new_user("alice-tl-unfollow").await;
1219 let (bob_did, bob_jwt) = setup_new_user("bob-tl-unfollow").await;
1220
1221 let (follow_uri, _) = create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
1222
1223 create_post(&client, &alice_did, &alice_jwt, "Post while following").await;
1224
1225 tokio::time::sleep(Duration::from_secs(1)).await;
1226
1227 let timeline_res = client
1228 .get(format!(
1229 "{}/xrpc/app.bsky.feed.getTimeline",
1230 base_url().await
1231 ))
1232 .bearer_auth(&bob_jwt)
1233 .send()
1234 .await
1235 .expect("Failed to get timeline");
1236
1237 assert_eq!(timeline_res.status(), StatusCode::OK);
1238 let timeline_body: Value = timeline_res.json().await.unwrap();
1239 let feed = timeline_body["feed"].as_array().unwrap();
1240 assert_eq!(feed.len(), 1, "Should see 1 post from Alice");
1241
1242 let follow_rkey = follow_uri.split('/').last().unwrap();
1243 let unfollow_payload = json!({
1244 "repo": bob_did,
1245 "collection": "app.bsky.graph.follow",
1246 "rkey": follow_rkey
1247 });
1248 client
1249 .post(format!(
1250 "{}/xrpc/com.atproto.repo.deleteRecord",
1251 base_url().await
1252 ))
1253 .bearer_auth(&bob_jwt)
1254 .json(&unfollow_payload)
1255 .send()
1256 .await
1257 .expect("Failed to unfollow");
1258
1259 tokio::time::sleep(Duration::from_secs(1)).await;
1260
1261 let timeline_after_res = client
1262 .get(format!(
1263 "{}/xrpc/app.bsky.feed.getTimeline",
1264 base_url().await
1265 ))
1266 .bearer_auth(&bob_jwt)
1267 .send()
1268 .await
1269 .expect("Failed to get timeline after unfollow");
1270
1271 assert_eq!(timeline_after_res.status(), StatusCode::OK);
1272 let timeline_after: Value = timeline_after_res.json().await.unwrap();
1273 let feed_after = timeline_after["feed"].as_array().unwrap();
1274 assert_eq!(feed_after.len(), 0, "Should see 0 posts after unfollowing");
1275}
1276
1277#[tokio::test]
1278async fn test_blob_in_record_lifecycle() {
1279 let client = client();
1280 let (did, jwt) = setup_new_user("blob-record").await;
1281
1282 let blob_data = b"This is test blob data for a profile avatar";
1283 let upload_res = client
1284 .post(format!(
1285 "{}/xrpc/com.atproto.repo.uploadBlob",
1286 base_url().await
1287 ))
1288 .header(header::CONTENT_TYPE, "text/plain")
1289 .bearer_auth(&jwt)
1290 .body(blob_data.to_vec())
1291 .send()
1292 .await
1293 .expect("Failed to upload blob");
1294
1295 assert_eq!(upload_res.status(), StatusCode::OK);
1296 let upload_body: Value = upload_res.json().await.unwrap();
1297 let blob_ref = upload_body["blob"].clone();
1298
1299 let profile_payload = json!({
1300 "repo": did,
1301 "collection": "app.bsky.actor.profile",
1302 "rkey": "self",
1303 "record": {
1304 "$type": "app.bsky.actor.profile",
1305 "displayName": "User With Avatar",
1306 "avatar": blob_ref
1307 }
1308 });
1309
1310 let create_res = client
1311 .post(format!(
1312 "{}/xrpc/com.atproto.repo.putRecord",
1313 base_url().await
1314 ))
1315 .bearer_auth(&jwt)
1316 .json(&profile_payload)
1317 .send()
1318 .await
1319 .expect("Failed to create profile with blob");
1320
1321 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile with blob");
1322
1323 let get_res = client
1324 .get(format!(
1325 "{}/xrpc/com.atproto.repo.getRecord",
1326 base_url().await
1327 ))
1328 .query(&[
1329 ("repo", did.as_str()),
1330 ("collection", "app.bsky.actor.profile"),
1331 ("rkey", "self"),
1332 ])
1333 .send()
1334 .await
1335 .expect("Failed to get profile");
1336
1337 assert_eq!(get_res.status(), StatusCode::OK);
1338 let profile: Value = get_res.json().await.unwrap();
1339 assert!(profile["value"]["avatar"]["ref"]["$link"].is_string());
1340}
1341
1342#[tokio::test]
1343async fn test_authorization_cannot_modify_other_repo() {
1344 let client = client();
1345
1346 let (alice_did, _alice_jwt) = setup_new_user("alice-auth").await;
1347 let (_bob_did, bob_jwt) = setup_new_user("bob-auth").await;
1348
1349 let post_payload = json!({
1350 "repo": alice_did,
1351 "collection": "app.bsky.feed.post",
1352 "rkey": "unauthorized-post",
1353 "record": {
1354 "$type": "app.bsky.feed.post",
1355 "text": "Bob trying to post as Alice",
1356 "createdAt": Utc::now().to_rfc3339()
1357 }
1358 });
1359
1360 let res = client
1361 .post(format!(
1362 "{}/xrpc/com.atproto.repo.putRecord",
1363 base_url().await
1364 ))
1365 .bearer_auth(&bob_jwt)
1366 .json(&post_payload)
1367 .send()
1368 .await
1369 .expect("Failed to send request");
1370
1371 assert!(
1372 res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED,
1373 "Expected 403 or 401 when writing to another user's repo, got {}",
1374 res.status()
1375 );
1376}
1377
1378#[tokio::test]
1379async fn test_authorization_cannot_delete_other_record() {
1380 let client = client();
1381
1382 let (alice_did, alice_jwt) = setup_new_user("alice-del-auth").await;
1383 let (_bob_did, bob_jwt) = setup_new_user("bob-del-auth").await;
1384
1385 let (post_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's post").await;
1386 let post_rkey = post_uri.split('/').last().unwrap();
1387
1388 let delete_payload = json!({
1389 "repo": alice_did,
1390 "collection": "app.bsky.feed.post",
1391 "rkey": post_rkey
1392 });
1393
1394 let res = client
1395 .post(format!(
1396 "{}/xrpc/com.atproto.repo.deleteRecord",
1397 base_url().await
1398 ))
1399 .bearer_auth(&bob_jwt)
1400 .json(&delete_payload)
1401 .send()
1402 .await
1403 .expect("Failed to send request");
1404
1405 assert!(
1406 res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED,
1407 "Expected 403 or 401 when deleting another user's record, got {}",
1408 res.status()
1409 );
1410
1411 let get_res = client
1412 .get(format!(
1413 "{}/xrpc/com.atproto.repo.getRecord",
1414 base_url().await
1415 ))
1416 .query(&[
1417 ("repo", alice_did.as_str()),
1418 ("collection", "app.bsky.feed.post"),
1419 ("rkey", post_rkey),
1420 ])
1421 .send()
1422 .await
1423 .expect("Failed to verify record exists");
1424
1425 assert_eq!(get_res.status(), StatusCode::OK, "Record should still exist");
1426}
1427
1428#[tokio::test]
1429async fn test_list_records_pagination() {
1430 let client = client();
1431 let (did, jwt) = setup_new_user("list-pagination").await;
1432
1433 for i in 0..5 {
1434 tokio::time::sleep(Duration::from_millis(50)).await;
1435 create_post(&client, &did, &jwt, &format!("Post number {}", i)).await;
1436 }
1437
1438 let list_res = client
1439 .get(format!(
1440 "{}/xrpc/com.atproto.repo.listRecords",
1441 base_url().await
1442 ))
1443 .query(&[
1444 ("repo", did.as_str()),
1445 ("collection", "app.bsky.feed.post"),
1446 ("limit", "2"),
1447 ])
1448 .send()
1449 .await
1450 .expect("Failed to list records");
1451
1452 assert_eq!(list_res.status(), StatusCode::OK);
1453 let list_body: Value = list_res.json().await.unwrap();
1454 let records = list_body["records"].as_array().unwrap();
1455 assert_eq!(records.len(), 2, "Should return 2 records with limit=2");
1456
1457 if let Some(cursor) = list_body["cursor"].as_str() {
1458 let list_page2_res = client
1459 .get(format!(
1460 "{}/xrpc/com.atproto.repo.listRecords",
1461 base_url().await
1462 ))
1463 .query(&[
1464 ("repo", did.as_str()),
1465 ("collection", "app.bsky.feed.post"),
1466 ("limit", "2"),
1467 ("cursor", cursor),
1468 ])
1469 .send()
1470 .await
1471 .expect("Failed to list records page 2");
1472
1473 assert_eq!(list_page2_res.status(), StatusCode::OK);
1474 let page2_body: Value = list_page2_res.json().await.unwrap();
1475 let page2_records = page2_body["records"].as_array().unwrap();
1476 assert_eq!(page2_records.len(), 2, "Page 2 should have 2 more records");
1477 }
1478}
1479
1480#[tokio::test]
1481async fn test_mutual_follow_lifecycle() {
1482 let client = client();
1483
1484 let (alice_did, alice_jwt) = setup_new_user("alice-mutual").await;
1485 let (bob_did, bob_jwt) = setup_new_user("bob-mutual").await;
1486
1487 create_follow(&client, &alice_did, &alice_jwt, &bob_did).await;
1488 create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
1489
1490 create_post(&client, &alice_did, &alice_jwt, "Alice's post for mutual").await;
1491 create_post(&client, &bob_did, &bob_jwt, "Bob's post for mutual").await;
1492
1493 tokio::time::sleep(Duration::from_secs(1)).await;
1494
1495 let alice_timeline_res = client
1496 .get(format!(
1497 "{}/xrpc/app.bsky.feed.getTimeline",
1498 base_url().await
1499 ))
1500 .bearer_auth(&alice_jwt)
1501 .send()
1502 .await
1503 .expect("Failed to get Alice's timeline");
1504
1505 assert_eq!(alice_timeline_res.status(), StatusCode::OK);
1506 let alice_tl: Value = alice_timeline_res.json().await.unwrap();
1507 let alice_feed = alice_tl["feed"].as_array().unwrap();
1508 assert_eq!(alice_feed.len(), 1, "Alice should see Bob's 1 post");
1509
1510 let bob_timeline_res = client
1511 .get(format!(
1512 "{}/xrpc/app.bsky.feed.getTimeline",
1513 base_url().await
1514 ))
1515 .bearer_auth(&bob_jwt)
1516 .send()
1517 .await
1518 .expect("Failed to get Bob's timeline");
1519
1520 assert_eq!(bob_timeline_res.status(), StatusCode::OK);
1521 let bob_tl: Value = bob_timeline_res.json().await.unwrap();
1522 let bob_feed = bob_tl["feed"].as_array().unwrap();
1523 assert_eq!(bob_feed.len(), 1, "Bob should see Alice's 1 post");
1524}
1525
1526#[tokio::test]
1527async fn test_account_to_post_full_lifecycle() {
1528 let client = client();
1529 let ts = Utc::now().timestamp_millis();
1530 let handle = format!("fullcycle-{}.test", ts);
1531 let email = format!("fullcycle-{}@test.com", ts);
1532 let password = "fullcycle-password";
1533
1534 let create_account_res = client
1535 .post(format!(
1536 "{}/xrpc/com.atproto.server.createAccount",
1537 base_url().await
1538 ))
1539 .json(&json!({
1540 "handle": handle,
1541 "email": email,
1542 "password": password
1543 }))
1544 .send()
1545 .await
1546 .expect("Failed to create account");
1547
1548 assert_eq!(create_account_res.status(), StatusCode::OK);
1549 let account_body: Value = create_account_res.json().await.unwrap();
1550 let did = account_body["did"].as_str().unwrap().to_string();
1551 let access_jwt = account_body["accessJwt"].as_str().unwrap().to_string();
1552
1553 let get_session_res = client
1554 .get(format!(
1555 "{}/xrpc/com.atproto.server.getSession",
1556 base_url().await
1557 ))
1558 .bearer_auth(&access_jwt)
1559 .send()
1560 .await
1561 .expect("Failed to get session");
1562
1563 assert_eq!(get_session_res.status(), StatusCode::OK);
1564 let session_body: Value = get_session_res.json().await.unwrap();
1565 assert_eq!(session_body["did"], did);
1566 assert_eq!(session_body["handle"], handle);
1567
1568 let profile_res = client
1569 .post(format!(
1570 "{}/xrpc/com.atproto.repo.putRecord",
1571 base_url().await
1572 ))
1573 .bearer_auth(&access_jwt)
1574 .json(&json!({
1575 "repo": did,
1576 "collection": "app.bsky.actor.profile",
1577 "rkey": "self",
1578 "record": {
1579 "$type": "app.bsky.actor.profile",
1580 "displayName": "Full Cycle User"
1581 }
1582 }))
1583 .send()
1584 .await
1585 .expect("Failed to create profile");
1586
1587 assert_eq!(profile_res.status(), StatusCode::OK);
1588
1589 let (post_uri, post_cid) = create_post(&client, &did, &access_jwt, "My first post!").await;
1590
1591 let get_post_res = client
1592 .get(format!(
1593 "{}/xrpc/com.atproto.repo.getRecord",
1594 base_url().await
1595 ))
1596 .query(&[
1597 ("repo", did.as_str()),
1598 ("collection", "app.bsky.feed.post"),
1599 ("rkey", post_uri.split('/').last().unwrap()),
1600 ])
1601 .send()
1602 .await
1603 .expect("Failed to get post");
1604
1605 assert_eq!(get_post_res.status(), StatusCode::OK);
1606
1607 create_like(&client, &did, &access_jwt, &post_uri, &post_cid).await;
1608
1609 let describe_res = client
1610 .get(format!(
1611 "{}/xrpc/com.atproto.repo.describeRepo",
1612 base_url().await
1613 ))
1614 .query(&[("repo", did.as_str())])
1615 .send()
1616 .await
1617 .expect("Failed to describe repo");
1618
1619 assert_eq!(describe_res.status(), StatusCode::OK);
1620 let describe_body: Value = describe_res.json().await.unwrap();
1621 assert_eq!(describe_body["did"], did);
1622 assert_eq!(describe_body["handle"], handle);
1623}
1624
1625#[tokio::test]
1626async fn test_app_password_lifecycle() {
1627 let client = client();
1628 let ts = Utc::now().timestamp_millis();
1629 let handle = format!("apppass-{}.test", ts);
1630 let email = format!("apppass-{}@test.com", ts);
1631 let password = "apppass-password";
1632
1633 let create_res = client
1634 .post(format!(
1635 "{}/xrpc/com.atproto.server.createAccount",
1636 base_url().await
1637 ))
1638 .json(&json!({
1639 "handle": handle,
1640 "email": email,
1641 "password": password
1642 }))
1643 .send()
1644 .await
1645 .expect("Failed to create account");
1646
1647 assert_eq!(create_res.status(), StatusCode::OK);
1648 let account: Value = create_res.json().await.unwrap();
1649 let jwt = account["accessJwt"].as_str().unwrap();
1650
1651 let create_app_pass_res = client
1652 .post(format!(
1653 "{}/xrpc/com.atproto.server.createAppPassword",
1654 base_url().await
1655 ))
1656 .bearer_auth(jwt)
1657 .json(&json!({ "name": "Test App" }))
1658 .send()
1659 .await
1660 .expect("Failed to create app password");
1661
1662 assert_eq!(create_app_pass_res.status(), StatusCode::OK);
1663 let app_pass: Value = create_app_pass_res.json().await.unwrap();
1664 let app_password = app_pass["password"].as_str().unwrap().to_string();
1665 assert_eq!(app_pass["name"], "Test App");
1666
1667 let list_res = client
1668 .get(format!(
1669 "{}/xrpc/com.atproto.server.listAppPasswords",
1670 base_url().await
1671 ))
1672 .bearer_auth(jwt)
1673 .send()
1674 .await
1675 .expect("Failed to list app passwords");
1676
1677 assert_eq!(list_res.status(), StatusCode::OK);
1678 let list_body: Value = list_res.json().await.unwrap();
1679 let passwords = list_body["passwords"].as_array().unwrap();
1680 assert_eq!(passwords.len(), 1);
1681 assert_eq!(passwords[0]["name"], "Test App");
1682
1683 let login_res = client
1684 .post(format!(
1685 "{}/xrpc/com.atproto.server.createSession",
1686 base_url().await
1687 ))
1688 .json(&json!({
1689 "identifier": handle,
1690 "password": app_password
1691 }))
1692 .send()
1693 .await
1694 .expect("Failed to login with app password");
1695
1696 assert_eq!(login_res.status(), StatusCode::OK, "App password login should work");
1697
1698 let revoke_res = client
1699 .post(format!(
1700 "{}/xrpc/com.atproto.server.revokeAppPassword",
1701 base_url().await
1702 ))
1703 .bearer_auth(jwt)
1704 .json(&json!({ "name": "Test App" }))
1705 .send()
1706 .await
1707 .expect("Failed to revoke app password");
1708
1709 assert_eq!(revoke_res.status(), StatusCode::OK);
1710
1711 let login_after_revoke = client
1712 .post(format!(
1713 "{}/xrpc/com.atproto.server.createSession",
1714 base_url().await
1715 ))
1716 .json(&json!({
1717 "identifier": handle,
1718 "password": app_password
1719 }))
1720 .send()
1721 .await
1722 .expect("Failed to attempt login after revoke");
1723
1724 assert!(
1725 login_after_revoke.status() == StatusCode::UNAUTHORIZED
1726 || login_after_revoke.status() == StatusCode::BAD_REQUEST,
1727 "Revoked app password should not work"
1728 );
1729
1730 let list_after_revoke = client
1731 .get(format!(
1732 "{}/xrpc/com.atproto.server.listAppPasswords",
1733 base_url().await
1734 ))
1735 .bearer_auth(jwt)
1736 .send()
1737 .await
1738 .expect("Failed to list after revoke");
1739
1740 let list_after: Value = list_after_revoke.json().await.unwrap();
1741 let passwords_after = list_after["passwords"].as_array().unwrap();
1742 assert_eq!(passwords_after.len(), 0, "No app passwords should remain");
1743}
1744
1745#[tokio::test]
1746async fn test_account_deactivation_lifecycle() {
1747 let client = client();
1748 let ts = Utc::now().timestamp_millis();
1749 let handle = format!("deactivate-{}.test", ts);
1750 let email = format!("deactivate-{}@test.com", ts);
1751 let password = "deactivate-password";
1752
1753 let create_res = client
1754 .post(format!(
1755 "{}/xrpc/com.atproto.server.createAccount",
1756 base_url().await
1757 ))
1758 .json(&json!({
1759 "handle": handle,
1760 "email": email,
1761 "password": password
1762 }))
1763 .send()
1764 .await
1765 .expect("Failed to create account");
1766
1767 assert_eq!(create_res.status(), StatusCode::OK);
1768 let account: Value = create_res.json().await.unwrap();
1769 let did = account["did"].as_str().unwrap().to_string();
1770 let jwt = account["accessJwt"].as_str().unwrap().to_string();
1771
1772 let (post_uri, _) = create_post(&client, &did, &jwt, "Post before deactivation").await;
1773 let post_rkey = post_uri.split('/').last().unwrap();
1774
1775 let status_before = client
1776 .get(format!(
1777 "{}/xrpc/com.atproto.server.checkAccountStatus",
1778 base_url().await
1779 ))
1780 .bearer_auth(&jwt)
1781 .send()
1782 .await
1783 .expect("Failed to check status");
1784
1785 assert_eq!(status_before.status(), StatusCode::OK);
1786 let status_body: Value = status_before.json().await.unwrap();
1787 assert_eq!(status_body["activated"], true);
1788
1789 let deactivate_res = client
1790 .post(format!(
1791 "{}/xrpc/com.atproto.server.deactivateAccount",
1792 base_url().await
1793 ))
1794 .bearer_auth(&jwt)
1795 .json(&json!({}))
1796 .send()
1797 .await
1798 .expect("Failed to deactivate");
1799
1800 assert_eq!(deactivate_res.status(), StatusCode::OK);
1801
1802 let get_post_res = client
1803 .get(format!(
1804 "{}/xrpc/com.atproto.repo.getRecord",
1805 base_url().await
1806 ))
1807 .query(&[
1808 ("repo", did.as_str()),
1809 ("collection", "app.bsky.feed.post"),
1810 ("rkey", post_rkey),
1811 ])
1812 .send()
1813 .await
1814 .expect("Failed to get post while deactivated");
1815
1816 assert_eq!(get_post_res.status(), StatusCode::OK, "Records should still be readable");
1817
1818 let activate_res = client
1819 .post(format!(
1820 "{}/xrpc/com.atproto.server.activateAccount",
1821 base_url().await
1822 ))
1823 .bearer_auth(&jwt)
1824 .json(&json!({}))
1825 .send()
1826 .await
1827 .expect("Failed to reactivate");
1828
1829 assert_eq!(activate_res.status(), StatusCode::OK);
1830
1831 let status_after_activate = client
1832 .get(format!(
1833 "{}/xrpc/com.atproto.server.checkAccountStatus",
1834 base_url().await
1835 ))
1836 .bearer_auth(&jwt)
1837 .send()
1838 .await
1839 .expect("Failed to check status after activate");
1840
1841 assert_eq!(status_after_activate.status(), StatusCode::OK);
1842
1843 let (new_post_uri, _) = create_post(&client, &did, &jwt, "Post after reactivation").await;
1844 assert!(!new_post_uri.is_empty(), "Should be able to post after reactivation");
1845}
1846
1847#[tokio::test]
1848async fn test_sync_record_lifecycle() {
1849 let client = client();
1850 let (did, jwt) = setup_new_user("sync-record-lifecycle").await;
1851
1852 let (post_uri, _post_cid) =
1853 create_post(&client, &did, &jwt, "Post for sync record test").await;
1854 let post_rkey = post_uri.split('/').last().unwrap();
1855
1856 let sync_record_res = client
1857 .get(format!(
1858 "{}/xrpc/com.atproto.sync.getRecord",
1859 base_url().await
1860 ))
1861 .query(&[
1862 ("did", did.as_str()),
1863 ("collection", "app.bsky.feed.post"),
1864 ("rkey", post_rkey),
1865 ])
1866 .send()
1867 .await
1868 .expect("Failed to get sync record");
1869
1870 assert_eq!(sync_record_res.status(), StatusCode::OK);
1871 assert_eq!(
1872 sync_record_res
1873 .headers()
1874 .get("content-type")
1875 .and_then(|h| h.to_str().ok()),
1876 Some("application/vnd.ipld.car")
1877 );
1878 let car_bytes = sync_record_res.bytes().await.unwrap();
1879 assert!(!car_bytes.is_empty(), "CAR data should not be empty");
1880
1881 let latest_before = client
1882 .get(format!(
1883 "{}/xrpc/com.atproto.sync.getLatestCommit",
1884 base_url().await
1885 ))
1886 .query(&[("did", did.as_str())])
1887 .send()
1888 .await
1889 .expect("Failed to get latest commit");
1890 let latest_before_body: Value = latest_before.json().await.unwrap();
1891 let rev_before = latest_before_body["rev"].as_str().unwrap().to_string();
1892
1893 let (post2_uri, _) = create_post(&client, &did, &jwt, "Second post for sync test").await;
1894
1895 let latest_after = client
1896 .get(format!(
1897 "{}/xrpc/com.atproto.sync.getLatestCommit",
1898 base_url().await
1899 ))
1900 .query(&[("did", did.as_str())])
1901 .send()
1902 .await
1903 .expect("Failed to get latest commit after");
1904 let latest_after_body: Value = latest_after.json().await.unwrap();
1905 let rev_after = latest_after_body["rev"].as_str().unwrap().to_string();
1906 assert_ne!(rev_before, rev_after, "Revision should change after new record");
1907
1908 let delete_payload = json!({
1909 "repo": did,
1910 "collection": "app.bsky.feed.post",
1911 "rkey": post_rkey
1912 });
1913 let delete_res = client
1914 .post(format!(
1915 "{}/xrpc/com.atproto.repo.deleteRecord",
1916 base_url().await
1917 ))
1918 .bearer_auth(&jwt)
1919 .json(&delete_payload)
1920 .send()
1921 .await
1922 .expect("Failed to delete record");
1923 assert_eq!(delete_res.status(), StatusCode::OK);
1924
1925 let sync_deleted_res = client
1926 .get(format!(
1927 "{}/xrpc/com.atproto.sync.getRecord",
1928 base_url().await
1929 ))
1930 .query(&[
1931 ("did", did.as_str()),
1932 ("collection", "app.bsky.feed.post"),
1933 ("rkey", post_rkey),
1934 ])
1935 .send()
1936 .await
1937 .expect("Failed to check deleted record via sync");
1938 assert_eq!(
1939 sync_deleted_res.status(),
1940 StatusCode::NOT_FOUND,
1941 "Deleted record should return 404 via sync.getRecord"
1942 );
1943
1944 let post2_rkey = post2_uri.split('/').last().unwrap();
1945 let sync_post2_res = client
1946 .get(format!(
1947 "{}/xrpc/com.atproto.sync.getRecord",
1948 base_url().await
1949 ))
1950 .query(&[
1951 ("did", did.as_str()),
1952 ("collection", "app.bsky.feed.post"),
1953 ("rkey", post2_rkey),
1954 ])
1955 .send()
1956 .await
1957 .expect("Failed to get second post via sync");
1958 assert_eq!(
1959 sync_post2_res.status(),
1960 StatusCode::OK,
1961 "Second post should still be accessible"
1962 );
1963}
1964
1965#[tokio::test]
1966async fn test_sync_repo_export_lifecycle() {
1967 let client = client();
1968 let (did, jwt) = setup_new_user("sync-repo-export").await;
1969
1970 let profile_payload = json!({
1971 "repo": did,
1972 "collection": "app.bsky.actor.profile",
1973 "rkey": "self",
1974 "record": {
1975 "$type": "app.bsky.actor.profile",
1976 "displayName": "Sync Export User"
1977 }
1978 });
1979 let profile_res = client
1980 .post(format!(
1981 "{}/xrpc/com.atproto.repo.putRecord",
1982 base_url().await
1983 ))
1984 .bearer_auth(&jwt)
1985 .json(&profile_payload)
1986 .send()
1987 .await
1988 .expect("Failed to create profile");
1989 assert_eq!(profile_res.status(), StatusCode::OK);
1990
1991 for i in 0..3 {
1992 tokio::time::sleep(Duration::from_millis(50)).await;
1993 create_post(&client, &did, &jwt, &format!("Export test post {}", i)).await;
1994 }
1995
1996 let blob_data = b"blob data for sync export test";
1997 let upload_res = client
1998 .post(format!(
1999 "{}/xrpc/com.atproto.repo.uploadBlob",
2000 base_url().await
2001 ))
2002 .header(header::CONTENT_TYPE, "application/octet-stream")
2003 .bearer_auth(&jwt)
2004 .body(blob_data.to_vec())
2005 .send()
2006 .await
2007 .expect("Failed to upload blob");
2008 assert_eq!(upload_res.status(), StatusCode::OK);
2009 let blob_body: Value = upload_res.json().await.unwrap();
2010 let blob_cid = blob_body["blob"]["ref"]["$link"].as_str().unwrap().to_string();
2011
2012 let repo_status_res = client
2013 .get(format!(
2014 "{}/xrpc/com.atproto.sync.getRepoStatus",
2015 base_url().await
2016 ))
2017 .query(&[("did", did.as_str())])
2018 .send()
2019 .await
2020 .expect("Failed to get repo status");
2021 assert_eq!(repo_status_res.status(), StatusCode::OK);
2022 let status_body: Value = repo_status_res.json().await.unwrap();
2023 assert_eq!(status_body["did"], did);
2024 assert_eq!(status_body["active"], true);
2025
2026 let get_repo_res = client
2027 .get(format!(
2028 "{}/xrpc/com.atproto.sync.getRepo",
2029 base_url().await
2030 ))
2031 .query(&[("did", did.as_str())])
2032 .send()
2033 .await
2034 .expect("Failed to get full repo");
2035 assert_eq!(get_repo_res.status(), StatusCode::OK);
2036 assert_eq!(
2037 get_repo_res
2038 .headers()
2039 .get("content-type")
2040 .and_then(|h| h.to_str().ok()),
2041 Some("application/vnd.ipld.car")
2042 );
2043 let repo_car = get_repo_res.bytes().await.unwrap();
2044 assert!(repo_car.len() > 100, "Repo CAR should have substantial data");
2045
2046 let list_blobs_res = client
2047 .get(format!(
2048 "{}/xrpc/com.atproto.sync.listBlobs",
2049 base_url().await
2050 ))
2051 .query(&[("did", did.as_str())])
2052 .send()
2053 .await
2054 .expect("Failed to list blobs");
2055 assert_eq!(list_blobs_res.status(), StatusCode::OK);
2056 let blobs_body: Value = list_blobs_res.json().await.unwrap();
2057 let cids = blobs_body["cids"].as_array().unwrap();
2058 assert!(!cids.is_empty(), "Should have at least one blob");
2059
2060 let get_blob_res = client
2061 .get(format!(
2062 "{}/xrpc/com.atproto.sync.getBlob",
2063 base_url().await
2064 ))
2065 .query(&[("did", did.as_str()), ("cid", &blob_cid)])
2066 .send()
2067 .await
2068 .expect("Failed to get blob");
2069 assert_eq!(get_blob_res.status(), StatusCode::OK);
2070 let retrieved_blob = get_blob_res.bytes().await.unwrap();
2071 assert_eq!(
2072 retrieved_blob.as_ref(),
2073 blob_data,
2074 "Retrieved blob should match uploaded data"
2075 );
2076
2077 let latest_commit_res = client
2078 .get(format!(
2079 "{}/xrpc/com.atproto.sync.getLatestCommit",
2080 base_url().await
2081 ))
2082 .query(&[("did", did.as_str())])
2083 .send()
2084 .await
2085 .expect("Failed to get latest commit");
2086 assert_eq!(latest_commit_res.status(), StatusCode::OK);
2087 let commit_body: Value = latest_commit_res.json().await.unwrap();
2088 let root_cid = commit_body["cid"].as_str().unwrap();
2089
2090 let get_blocks_url = format!(
2091 "{}/xrpc/com.atproto.sync.getBlocks?did={}&cids={}",
2092 base_url().await,
2093 did,
2094 root_cid
2095 );
2096 let get_blocks_res = client
2097 .get(&get_blocks_url)
2098 .send()
2099 .await
2100 .expect("Failed to get blocks");
2101 assert_eq!(get_blocks_res.status(), StatusCode::OK);
2102 assert_eq!(
2103 get_blocks_res
2104 .headers()
2105 .get("content-type")
2106 .and_then(|h| h.to_str().ok()),
2107 Some("application/vnd.ipld.car")
2108 );
2109}
2110
2111#[tokio::test]
2112async fn test_apply_writes_batch_lifecycle() {
2113 let client = client();
2114 let (did, jwt) = setup_new_user("apply-writes-batch").await;
2115
2116 let now = Utc::now().to_rfc3339();
2117 let writes_payload = json!({
2118 "repo": did,
2119 "writes": [
2120 {
2121 "$type": "com.atproto.repo.applyWrites#create",
2122 "collection": "app.bsky.feed.post",
2123 "rkey": "batch-post-1",
2124 "value": {
2125 "$type": "app.bsky.feed.post",
2126 "text": "First batch post",
2127 "createdAt": now
2128 }
2129 },
2130 {
2131 "$type": "com.atproto.repo.applyWrites#create",
2132 "collection": "app.bsky.feed.post",
2133 "rkey": "batch-post-2",
2134 "value": {
2135 "$type": "app.bsky.feed.post",
2136 "text": "Second batch post",
2137 "createdAt": now
2138 }
2139 },
2140 {
2141 "$type": "com.atproto.repo.applyWrites#create",
2142 "collection": "app.bsky.actor.profile",
2143 "rkey": "self",
2144 "value": {
2145 "$type": "app.bsky.actor.profile",
2146 "displayName": "Batch User"
2147 }
2148 }
2149 ]
2150 });
2151
2152 let apply_res = client
2153 .post(format!(
2154 "{}/xrpc/com.atproto.repo.applyWrites",
2155 base_url().await
2156 ))
2157 .bearer_auth(&jwt)
2158 .json(&writes_payload)
2159 .send()
2160 .await
2161 .expect("Failed to apply writes");
2162
2163 assert_eq!(apply_res.status(), StatusCode::OK);
2164
2165 let get_post1 = client
2166 .get(format!(
2167 "{}/xrpc/com.atproto.repo.getRecord",
2168 base_url().await
2169 ))
2170 .query(&[
2171 ("repo", did.as_str()),
2172 ("collection", "app.bsky.feed.post"),
2173 ("rkey", "batch-post-1"),
2174 ])
2175 .send()
2176 .await
2177 .expect("Failed to get post 1");
2178 assert_eq!(get_post1.status(), StatusCode::OK);
2179 let post1_body: Value = get_post1.json().await.unwrap();
2180 assert_eq!(post1_body["value"]["text"], "First batch post");
2181
2182 let get_post2 = client
2183 .get(format!(
2184 "{}/xrpc/com.atproto.repo.getRecord",
2185 base_url().await
2186 ))
2187 .query(&[
2188 ("repo", did.as_str()),
2189 ("collection", "app.bsky.feed.post"),
2190 ("rkey", "batch-post-2"),
2191 ])
2192 .send()
2193 .await
2194 .expect("Failed to get post 2");
2195 assert_eq!(get_post2.status(), StatusCode::OK);
2196
2197 let get_profile = client
2198 .get(format!(
2199 "{}/xrpc/com.atproto.repo.getRecord",
2200 base_url().await
2201 ))
2202 .query(&[
2203 ("repo", did.as_str()),
2204 ("collection", "app.bsky.actor.profile"),
2205 ("rkey", "self"),
2206 ])
2207 .send()
2208 .await
2209 .expect("Failed to get profile");
2210 assert_eq!(get_profile.status(), StatusCode::OK);
2211 let profile_body: Value = get_profile.json().await.unwrap();
2212 assert_eq!(profile_body["value"]["displayName"], "Batch User");
2213
2214 let update_writes = json!({
2215 "repo": did,
2216 "writes": [
2217 {
2218 "$type": "com.atproto.repo.applyWrites#update",
2219 "collection": "app.bsky.actor.profile",
2220 "rkey": "self",
2221 "value": {
2222 "$type": "app.bsky.actor.profile",
2223 "displayName": "Updated Batch User"
2224 }
2225 },
2226 {
2227 "$type": "com.atproto.repo.applyWrites#delete",
2228 "collection": "app.bsky.feed.post",
2229 "rkey": "batch-post-1"
2230 }
2231 ]
2232 });
2233
2234 let update_res = client
2235 .post(format!(
2236 "{}/xrpc/com.atproto.repo.applyWrites",
2237 base_url().await
2238 ))
2239 .bearer_auth(&jwt)
2240 .json(&update_writes)
2241 .send()
2242 .await
2243 .expect("Failed to apply update writes");
2244 assert_eq!(update_res.status(), StatusCode::OK);
2245
2246 let get_updated_profile = client
2247 .get(format!(
2248 "{}/xrpc/com.atproto.repo.getRecord",
2249 base_url().await
2250 ))
2251 .query(&[
2252 ("repo", did.as_str()),
2253 ("collection", "app.bsky.actor.profile"),
2254 ("rkey", "self"),
2255 ])
2256 .send()
2257 .await
2258 .expect("Failed to get updated profile");
2259 let updated_profile: Value = get_updated_profile.json().await.unwrap();
2260 assert_eq!(updated_profile["value"]["displayName"], "Updated Batch User");
2261
2262 let get_deleted_post = client
2263 .get(format!(
2264 "{}/xrpc/com.atproto.repo.getRecord",
2265 base_url().await
2266 ))
2267 .query(&[
2268 ("repo", did.as_str()),
2269 ("collection", "app.bsky.feed.post"),
2270 ("rkey", "batch-post-1"),
2271 ])
2272 .send()
2273 .await
2274 .expect("Failed to check deleted post");
2275 assert_eq!(
2276 get_deleted_post.status(),
2277 StatusCode::NOT_FOUND,
2278 "Batch-deleted post should be gone"
2279 );
2280}
2281
2282#[tokio::test]
2283async fn test_resolve_handle_lifecycle() {
2284 let client = client();
2285 let ts = Utc::now().timestamp_millis();
2286 let handle = format!("resolve-test-{}.test", ts);
2287 let email = format!("resolve-test-{}@test.com", ts);
2288
2289 let create_res = client
2290 .post(format!(
2291 "{}/xrpc/com.atproto.server.createAccount",
2292 base_url().await
2293 ))
2294 .json(&json!({
2295 "handle": handle,
2296 "email": email,
2297 "password": "resolve-test-pw"
2298 }))
2299 .send()
2300 .await
2301 .expect("Failed to create account");
2302 assert_eq!(create_res.status(), StatusCode::OK);
2303 let account: Value = create_res.json().await.unwrap();
2304 let did = account["did"].as_str().unwrap();
2305
2306 let resolve_res = client
2307 .get(format!(
2308 "{}/xrpc/com.atproto.identity.resolveHandle",
2309 base_url().await
2310 ))
2311 .query(&[("handle", handle.as_str())])
2312 .send()
2313 .await
2314 .expect("Failed to resolve handle");
2315
2316 assert_eq!(resolve_res.status(), StatusCode::OK);
2317 let resolve_body: Value = resolve_res.json().await.unwrap();
2318 assert_eq!(resolve_body["did"], did);
2319}
2320
2321#[tokio::test]
2322async fn test_service_auth_lifecycle() {
2323 let client = client();
2324 let (did, jwt) = setup_new_user("service-auth-test").await;
2325
2326 let service_auth_res = client
2327 .get(format!(
2328 "{}/xrpc/com.atproto.server.getServiceAuth",
2329 base_url().await
2330 ))
2331 .query(&[
2332 ("aud", "did:web:api.bsky.app"),
2333 ("lxm", "com.atproto.repo.uploadBlob"),
2334 ])
2335 .bearer_auth(&jwt)
2336 .send()
2337 .await
2338 .expect("Failed to get service auth");
2339
2340 assert_eq!(service_auth_res.status(), StatusCode::OK);
2341 let auth_body: Value = service_auth_res.json().await.unwrap();
2342 let service_token = auth_body["token"].as_str().expect("No token in response");
2343
2344 let parts: Vec<&str> = service_token.split('.').collect();
2345 assert_eq!(parts.len(), 3, "Service token should be a valid JWT");
2346
2347 let payload_bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD
2348 .decode(parts[1])
2349 .expect("Failed to decode JWT payload");
2350 let claims: Value = serde_json::from_slice(&payload_bytes).expect("Invalid JWT payload");
2351
2352 assert_eq!(claims["iss"], did);
2353 assert_eq!(claims["aud"], "did:web:api.bsky.app");
2354 assert_eq!(claims["lxm"], "com.atproto.repo.uploadBlob");
2355}
2356
2357#[tokio::test]
2358async fn test_moderation_report_lifecycle() {
2359 let client = client();
2360 let (alice_did, alice_jwt) = setup_new_user("alice-report").await;
2361 let (bob_did, bob_jwt) = setup_new_user("bob-report").await;
2362
2363 let (post_uri, post_cid) =
2364 create_post(&client, &bob_did, &bob_jwt, "This is a reportable post").await;
2365
2366 let report_payload = json!({
2367 "reasonType": "com.atproto.moderation.defs#reasonSpam",
2368 "reason": "This looks like spam to me",
2369 "subject": {
2370 "$type": "com.atproto.repo.strongRef",
2371 "uri": post_uri,
2372 "cid": post_cid
2373 }
2374 });
2375
2376 let report_res = client
2377 .post(format!(
2378 "{}/xrpc/com.atproto.moderation.createReport",
2379 base_url().await
2380 ))
2381 .bearer_auth(&alice_jwt)
2382 .json(&report_payload)
2383 .send()
2384 .await
2385 .expect("Failed to create report");
2386
2387 assert_eq!(report_res.status(), StatusCode::OK);
2388 let report_body: Value = report_res.json().await.unwrap();
2389 assert!(report_body["id"].is_number(), "Report should have an ID");
2390 assert_eq!(report_body["reasonType"], "com.atproto.moderation.defs#reasonSpam");
2391 assert_eq!(report_body["reportedBy"], alice_did);
2392
2393 let account_report_payload = json!({
2394 "reasonType": "com.atproto.moderation.defs#reasonOther",
2395 "reason": "Suspicious account activity",
2396 "subject": {
2397 "$type": "com.atproto.admin.defs#repoRef",
2398 "did": bob_did
2399 }
2400 });
2401
2402 let account_report_res = client
2403 .post(format!(
2404 "{}/xrpc/com.atproto.moderation.createReport",
2405 base_url().await
2406 ))
2407 .bearer_auth(&alice_jwt)
2408 .json(&account_report_payload)
2409 .send()
2410 .await
2411 .expect("Failed to create account report");
2412
2413 assert_eq!(account_report_res.status(), StatusCode::OK);
2414}