this repo has no description
1mod common;
2use common::*;
3
4use chrono::Utc;
5use reqwest::{self, StatusCode, header};
6use serde_json::{Value, json};
7use std::time::Duration;
8
9async fn setup_new_user(handle_prefix: &str) -> (String, String) {
10 let client = client();
11 let ts = Utc::now().timestamp_millis();
12 let handle = format!("{}-{}.test", handle_prefix, ts);
13 let email = format!("{}-{}@test.com", handle_prefix, ts);
14 let password = "e2e-password-123";
15
16 let create_account_payload = json!({
17 "handle": handle,
18 "email": email,
19 "password": password
20 });
21 let create_res = client
22 .post(format!(
23 "{}/xrpc/com.atproto.server.createAccount",
24 base_url().await
25 ))
26 .json(&create_account_payload)
27 .send()
28 .await
29 .expect("setup_new_user: Failed to send createAccount");
30
31 if create_res.status() != reqwest::StatusCode::OK {
32 panic!(
33 "setup_new_user: Failed to create account: {:?}",
34 create_res.text().await
35 );
36 }
37
38 let create_body: Value = create_res
39 .json()
40 .await
41 .expect("setup_new_user: createAccount response was not JSON");
42
43 let new_did = create_body["did"]
44 .as_str()
45 .expect("setup_new_user: Response had no DID")
46 .to_string();
47 let new_jwt = create_body["accessJwt"]
48 .as_str()
49 .expect("setup_new_user: Response had no accessJwt")
50 .to_string();
51
52 (new_did, new_jwt)
53}
54
55#[tokio::test]
56async fn test_post_crud_lifecycle() {
57 let client = client();
58 let (did, jwt) = setup_new_user("lifecycle-crud").await;
59 let collection = "app.bsky.feed.post";
60
61 let rkey = format!("e2e_lifecycle_{}", Utc::now().timestamp_millis());
62 let now = Utc::now().to_rfc3339();
63
64 let original_text = "Hello from the lifecycle test!";
65 let create_payload = json!({
66 "repo": did,
67 "collection": collection,
68 "rkey": rkey,
69 "record": {
70 "$type": collection,
71 "text": original_text,
72 "createdAt": now
73 }
74 });
75
76 let create_res = client
77 .post(format!(
78 "{}/xrpc/com.atproto.repo.putRecord",
79 base_url().await
80 ))
81 .bearer_auth(&jwt)
82 .json(&create_payload)
83 .send()
84 .await
85 .expect("Failed to send create request");
86
87 if create_res.status() != reqwest::StatusCode::OK {
88 let status = create_res.status();
89 let body = create_res
90 .text()
91 .await
92 .unwrap_or_else(|_| "Could not get body".to_string());
93 panic!(
94 "Failed to create record. Status: {}, Body: {}",
95 status, body
96 );
97 }
98
99 let create_body: Value = create_res
100 .json()
101 .await
102 .expect("create response was not JSON");
103 let uri = create_body["uri"].as_str().unwrap();
104
105 let params = [
106 ("repo", did.as_str()),
107 ("collection", collection),
108 ("rkey", &rkey),
109 ];
110 let get_res = client
111 .get(format!(
112 "{}/xrpc/com.atproto.repo.getRecord",
113 base_url().await
114 ))
115 .query(¶ms)
116 .send()
117 .await
118 .expect("Failed to send get request");
119
120 assert_eq!(
121 get_res.status(),
122 reqwest::StatusCode::OK,
123 "Failed to get record after create"
124 );
125 let get_body: Value = get_res.json().await.expect("get response was not JSON");
126 assert_eq!(get_body["uri"], uri);
127 assert_eq!(get_body["value"]["text"], original_text);
128
129 let updated_text = "This post has been updated.";
130 let update_payload = json!({
131 "repo": did,
132 "collection": collection,
133 "rkey": rkey,
134 "record": {
135 "$type": collection,
136 "text": updated_text,
137 "createdAt": now
138 }
139 });
140
141 let update_res = client
142 .post(format!(
143 "{}/xrpc/com.atproto.repo.putRecord",
144 base_url().await
145 ))
146 .bearer_auth(&jwt)
147 .json(&update_payload)
148 .send()
149 .await
150 .expect("Failed to send update request");
151
152 assert_eq!(
153 update_res.status(),
154 reqwest::StatusCode::OK,
155 "Failed to update record"
156 );
157
158 let get_updated_res = client
159 .get(format!(
160 "{}/xrpc/com.atproto.repo.getRecord",
161 base_url().await
162 ))
163 .query(¶ms)
164 .send()
165 .await
166 .expect("Failed to send get-after-update request");
167
168 assert_eq!(
169 get_updated_res.status(),
170 reqwest::StatusCode::OK,
171 "Failed to get record after update"
172 );
173 let get_updated_body: Value = get_updated_res
174 .json()
175 .await
176 .expect("get-updated response was not JSON");
177 assert_eq!(
178 get_updated_body["value"]["text"], updated_text,
179 "Text was not updated"
180 );
181
182 let delete_payload = json!({
183 "repo": did,
184 "collection": collection,
185 "rkey": rkey
186 });
187
188 let delete_res = client
189 .post(format!(
190 "{}/xrpc/com.atproto.repo.deleteRecord",
191 base_url().await
192 ))
193 .bearer_auth(&jwt)
194 .json(&delete_payload)
195 .send()
196 .await
197 .expect("Failed to send delete request");
198
199 assert_eq!(
200 delete_res.status(),
201 reqwest::StatusCode::OK,
202 "Failed to delete record"
203 );
204
205 let get_deleted_res = client
206 .get(format!(
207 "{}/xrpc/com.atproto.repo.getRecord",
208 base_url().await
209 ))
210 .query(¶ms)
211 .send()
212 .await
213 .expect("Failed to send get-after-delete request");
214
215 assert_eq!(
216 get_deleted_res.status(),
217 reqwest::StatusCode::NOT_FOUND,
218 "Record was found, but it should be deleted"
219 );
220}
221
222#[tokio::test]
223async fn test_record_update_conflict_lifecycle() {
224 let client = client();
225 let (user_did, user_jwt) = setup_new_user("user-conflict").await;
226
227 let profile_payload = json!({
228 "repo": user_did,
229 "collection": "app.bsky.actor.profile",
230 "rkey": "self",
231 "record": {
232 "$type": "app.bsky.actor.profile",
233 "displayName": "Original Name"
234 }
235 });
236 let create_res = client
237 .post(format!(
238 "{}/xrpc/com.atproto.repo.putRecord",
239 base_url().await
240 ))
241 .bearer_auth(&user_jwt)
242 .json(&profile_payload)
243 .send()
244 .await
245 .expect("create profile failed");
246
247 if create_res.status() != reqwest::StatusCode::OK {
248 return;
249 }
250
251 let get_res = client
252 .get(format!(
253 "{}/xrpc/com.atproto.repo.getRecord",
254 base_url().await
255 ))
256 .query(&[
257 ("repo", &user_did),
258 ("collection", &"app.bsky.actor.profile".to_string()),
259 ("rkey", &"self".to_string()),
260 ])
261 .send()
262 .await
263 .expect("getRecord failed");
264 let get_body: Value = get_res.json().await.expect("getRecord not json");
265 let cid_v1 = get_body["cid"]
266 .as_str()
267 .expect("Profile v1 had no CID")
268 .to_string();
269
270 let update_payload_v2 = json!({
271 "repo": user_did,
272 "collection": "app.bsky.actor.profile",
273 "rkey": "self",
274 "record": {
275 "$type": "app.bsky.actor.profile",
276 "displayName": "Updated Name (v2)"
277 },
278 "swapRecord": cid_v1
279 });
280 let update_res_v2 = client
281 .post(format!(
282 "{}/xrpc/com.atproto.repo.putRecord",
283 base_url().await
284 ))
285 .bearer_auth(&user_jwt)
286 .json(&update_payload_v2)
287 .send()
288 .await
289 .expect("putRecord v2 failed");
290 assert_eq!(
291 update_res_v2.status(),
292 reqwest::StatusCode::OK,
293 "v2 update failed"
294 );
295 let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json");
296 let cid_v2 = update_body_v2["cid"]
297 .as_str()
298 .expect("v2 response had no CID")
299 .to_string();
300
301 let update_payload_v3_stale = json!({
302 "repo": user_did,
303 "collection": "app.bsky.actor.profile",
304 "rkey": "self",
305 "record": {
306 "$type": "app.bsky.actor.profile",
307 "displayName": "Stale Update (v3)"
308 },
309 "swapRecord": cid_v1
310 });
311 let update_res_v3_stale = client
312 .post(format!(
313 "{}/xrpc/com.atproto.repo.putRecord",
314 base_url().await
315 ))
316 .bearer_auth(&user_jwt)
317 .json(&update_payload_v3_stale)
318 .send()
319 .await
320 .expect("putRecord v3 (stale) failed");
321
322 assert_eq!(
323 update_res_v3_stale.status(),
324 reqwest::StatusCode::CONFLICT,
325 "Stale update did not cause a 409 Conflict"
326 );
327
328 let update_payload_v3_good = json!({
329 "repo": user_did,
330 "collection": "app.bsky.actor.profile",
331 "rkey": "self",
332 "record": {
333 "$type": "app.bsky.actor.profile",
334 "displayName": "Good Update (v3)"
335 },
336 "swapRecord": cid_v2
337 });
338 let update_res_v3_good = client
339 .post(format!(
340 "{}/xrpc/com.atproto.repo.putRecord",
341 base_url().await
342 ))
343 .bearer_auth(&user_jwt)
344 .json(&update_payload_v3_good)
345 .send()
346 .await
347 .expect("putRecord v3 (good) failed");
348
349 assert_eq!(
350 update_res_v3_good.status(),
351 reqwest::StatusCode::OK,
352 "v3 (good) update failed"
353 );
354}
355
356async fn create_post(
357 client: &reqwest::Client,
358 did: &str,
359 jwt: &str,
360 text: &str,
361) -> (String, String) {
362 let collection = "app.bsky.feed.post";
363 let rkey = format!("e2e_social_{}", Utc::now().timestamp_millis());
364 let now = Utc::now().to_rfc3339();
365
366 let create_payload = json!({
367 "repo": did,
368 "collection": collection,
369 "rkey": rkey,
370 "record": {
371 "$type": collection,
372 "text": text,
373 "createdAt": now
374 }
375 });
376
377 let create_res = client
378 .post(format!(
379 "{}/xrpc/com.atproto.repo.putRecord",
380 base_url().await
381 ))
382 .bearer_auth(jwt)
383 .json(&create_payload)
384 .send()
385 .await
386 .expect("Failed to send create post request");
387
388 assert_eq!(
389 create_res.status(),
390 reqwest::StatusCode::OK,
391 "Failed to create post record"
392 );
393 let create_body: Value = create_res
394 .json()
395 .await
396 .expect("create post response was not JSON");
397 let uri = create_body["uri"].as_str().unwrap().to_string();
398 let cid = create_body["cid"].as_str().unwrap().to_string();
399 (uri, cid)
400}
401
402async fn create_follow(
403 client: &reqwest::Client,
404 follower_did: &str,
405 follower_jwt: &str,
406 followee_did: &str,
407) -> (String, String) {
408 let collection = "app.bsky.graph.follow";
409 let rkey = format!("e2e_follow_{}", Utc::now().timestamp_millis());
410 let now = Utc::now().to_rfc3339();
411
412 let create_payload = json!({
413 "repo": follower_did,
414 "collection": collection,
415 "rkey": rkey,
416 "record": {
417 "$type": collection,
418 "subject": followee_did,
419 "createdAt": now
420 }
421 });
422
423 let create_res = client
424 .post(format!(
425 "{}/xrpc/com.atproto.repo.putRecord",
426 base_url().await
427 ))
428 .bearer_auth(follower_jwt)
429 .json(&create_payload)
430 .send()
431 .await
432 .expect("Failed to send create follow request");
433
434 assert_eq!(
435 create_res.status(),
436 reqwest::StatusCode::OK,
437 "Failed to create follow record"
438 );
439 let create_body: Value = create_res
440 .json()
441 .await
442 .expect("create follow response was not JSON");
443 let uri = create_body["uri"].as_str().unwrap().to_string();
444 let cid = create_body["cid"].as_str().unwrap().to_string();
445 (uri, cid)
446}
447
448#[tokio::test]
449async fn test_social_flow_lifecycle() {
450 let client = client();
451
452 let (alice_did, alice_jwt) = setup_new_user("alice-social").await;
453 let (bob_did, bob_jwt) = setup_new_user("bob-social").await;
454
455 let (post1_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's first post!").await;
456
457 create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
458
459 tokio::time::sleep(Duration::from_secs(1)).await;
460
461 let timeline_res_1 = client
462 .get(format!(
463 "{}/xrpc/app.bsky.feed.getTimeline",
464 base_url().await
465 ))
466 .bearer_auth(&bob_jwt)
467 .send()
468 .await
469 .expect("Failed to get timeline (1)");
470
471 assert_eq!(
472 timeline_res_1.status(),
473 reqwest::StatusCode::OK,
474 "Failed to get timeline (1)"
475 );
476 let timeline_body_1: Value = timeline_res_1.json().await.expect("Timeline (1) not JSON");
477 let feed_1 = timeline_body_1["feed"].as_array().unwrap();
478 assert_eq!(feed_1.len(), 1, "Timeline should have 1 post");
479 assert_eq!(
480 feed_1[0]["post"]["uri"], post1_uri,
481 "Post URI mismatch in timeline (1)"
482 );
483
484 let (post2_uri, _) = create_post(
485 &client,
486 &alice_did,
487 &alice_jwt,
488 "Alice's second post, so exciting!",
489 )
490 .await;
491
492 tokio::time::sleep(Duration::from_secs(1)).await;
493
494 let timeline_res_2 = client
495 .get(format!(
496 "{}/xrpc/app.bsky.feed.getTimeline",
497 base_url().await
498 ))
499 .bearer_auth(&bob_jwt)
500 .send()
501 .await
502 .expect("Failed to get timeline (2)");
503
504 assert_eq!(
505 timeline_res_2.status(),
506 reqwest::StatusCode::OK,
507 "Failed to get timeline (2)"
508 );
509 let timeline_body_2: Value = timeline_res_2.json().await.expect("Timeline (2) not JSON");
510 let feed_2 = timeline_body_2["feed"].as_array().unwrap();
511 assert_eq!(feed_2.len(), 2, "Timeline should have 2 posts");
512 assert_eq!(
513 feed_2[0]["post"]["uri"], post2_uri,
514 "Post 2 should be first"
515 );
516 assert_eq!(
517 feed_2[1]["post"]["uri"], post1_uri,
518 "Post 1 should be second"
519 );
520
521 let delete_payload = json!({
522 "repo": alice_did,
523 "collection": "app.bsky.feed.post",
524 "rkey": post1_uri.split('/').last().unwrap()
525 });
526 let delete_res = client
527 .post(format!(
528 "{}/xrpc/com.atproto.repo.deleteRecord",
529 base_url().await
530 ))
531 .bearer_auth(&alice_jwt)
532 .json(&delete_payload)
533 .send()
534 .await
535 .expect("Failed to send delete request");
536 assert_eq!(
537 delete_res.status(),
538 reqwest::StatusCode::OK,
539 "Failed to delete record"
540 );
541
542 tokio::time::sleep(Duration::from_secs(1)).await;
543
544 let timeline_res_3 = client
545 .get(format!(
546 "{}/xrpc/app.bsky.feed.getTimeline",
547 base_url().await
548 ))
549 .bearer_auth(&bob_jwt)
550 .send()
551 .await
552 .expect("Failed to get timeline (3)");
553
554 assert_eq!(
555 timeline_res_3.status(),
556 reqwest::StatusCode::OK,
557 "Failed to get timeline (3)"
558 );
559 let timeline_body_3: Value = timeline_res_3.json().await.expect("Timeline (3) not JSON");
560 let feed_3 = timeline_body_3["feed"].as_array().unwrap();
561 assert_eq!(feed_3.len(), 1, "Timeline should have 1 post after delete");
562 assert_eq!(
563 feed_3[0]["post"]["uri"], post2_uri,
564 "Only post 2 should remain"
565 );
566}
567
568#[tokio::test]
569async fn test_session_lifecycle_wrong_password() {
570 let client = client();
571 let (_, _) = setup_new_user("session-wrong-pw").await;
572
573 let login_payload = json!({
574 "identifier": format!("session-wrong-pw-{}.test", Utc::now().timestamp_millis()),
575 "password": "wrong-password"
576 });
577
578 let res = client
579 .post(format!(
580 "{}/xrpc/com.atproto.server.createSession",
581 base_url().await
582 ))
583 .json(&login_payload)
584 .send()
585 .await
586 .expect("Failed to send request");
587
588 assert!(
589 res.status() == StatusCode::UNAUTHORIZED || res.status() == StatusCode::BAD_REQUEST,
590 "Expected 401 or 400 for wrong password, got {}",
591 res.status()
592 );
593}
594
595#[tokio::test]
596async fn test_session_lifecycle_multiple_sessions() {
597 let client = client();
598 let ts = Utc::now().timestamp_millis();
599 let handle = format!("multi-session-{}.test", ts);
600 let email = format!("multi-session-{}@test.com", ts);
601 let password = "multi-session-pw";
602
603 let create_payload = json!({
604 "handle": handle,
605 "email": email,
606 "password": password
607 });
608 let create_res = client
609 .post(format!(
610 "{}/xrpc/com.atproto.server.createAccount",
611 base_url().await
612 ))
613 .json(&create_payload)
614 .send()
615 .await
616 .expect("Failed to create account");
617 assert_eq!(create_res.status(), StatusCode::OK);
618
619 let login_payload = json!({
620 "identifier": handle,
621 "password": password
622 });
623
624 let session1_res = client
625 .post(format!(
626 "{}/xrpc/com.atproto.server.createSession",
627 base_url().await
628 ))
629 .json(&login_payload)
630 .send()
631 .await
632 .expect("Failed session 1");
633 assert_eq!(session1_res.status(), StatusCode::OK);
634 let session1: Value = session1_res.json().await.unwrap();
635 let jwt1 = session1["accessJwt"].as_str().unwrap();
636
637 let session2_res = client
638 .post(format!(
639 "{}/xrpc/com.atproto.server.createSession",
640 base_url().await
641 ))
642 .json(&login_payload)
643 .send()
644 .await
645 .expect("Failed session 2");
646 assert_eq!(session2_res.status(), StatusCode::OK);
647 let session2: Value = session2_res.json().await.unwrap();
648 let jwt2 = session2["accessJwt"].as_str().unwrap();
649
650 assert_ne!(jwt1, jwt2, "Sessions should have different tokens");
651
652 let get1 = client
653 .get(format!(
654 "{}/xrpc/com.atproto.server.getSession",
655 base_url().await
656 ))
657 .bearer_auth(jwt1)
658 .send()
659 .await
660 .expect("Failed getSession 1");
661 assert_eq!(get1.status(), StatusCode::OK);
662
663 let get2 = client
664 .get(format!(
665 "{}/xrpc/com.atproto.server.getSession",
666 base_url().await
667 ))
668 .bearer_auth(jwt2)
669 .send()
670 .await
671 .expect("Failed getSession 2");
672 assert_eq!(get2.status(), StatusCode::OK);
673}
674
675#[tokio::test]
676async fn test_session_lifecycle_refresh_invalidates_old() {
677 let client = client();
678 let ts = Utc::now().timestamp_millis();
679 let handle = format!("refresh-inv-{}.test", ts);
680 let email = format!("refresh-inv-{}@test.com", ts);
681 let password = "refresh-inv-pw";
682
683 let create_payload = json!({
684 "handle": handle,
685 "email": email,
686 "password": password
687 });
688 client
689 .post(format!(
690 "{}/xrpc/com.atproto.server.createAccount",
691 base_url().await
692 ))
693 .json(&create_payload)
694 .send()
695 .await
696 .expect("Failed to create account");
697
698 let login_payload = json!({
699 "identifier": handle,
700 "password": password
701 });
702 let login_res = client
703 .post(format!(
704 "{}/xrpc/com.atproto.server.createSession",
705 base_url().await
706 ))
707 .json(&login_payload)
708 .send()
709 .await
710 .expect("Failed login");
711 let login_body: Value = login_res.json().await.unwrap();
712 let refresh_jwt = login_body["refreshJwt"].as_str().unwrap().to_string();
713
714 let refresh_res = client
715 .post(format!(
716 "{}/xrpc/com.atproto.server.refreshSession",
717 base_url().await
718 ))
719 .bearer_auth(&refresh_jwt)
720 .send()
721 .await
722 .expect("Failed first refresh");
723 assert_eq!(refresh_res.status(), StatusCode::OK);
724 let refresh_body: Value = refresh_res.json().await.unwrap();
725 let new_refresh_jwt = refresh_body["refreshJwt"].as_str().unwrap();
726
727 assert_ne!(refresh_jwt, new_refresh_jwt, "Refresh tokens should differ");
728
729 let reuse_res = client
730 .post(format!(
731 "{}/xrpc/com.atproto.server.refreshSession",
732 base_url().await
733 ))
734 .bearer_auth(&refresh_jwt)
735 .send()
736 .await
737 .expect("Failed reuse attempt");
738
739 assert!(
740 reuse_res.status() == StatusCode::UNAUTHORIZED || reuse_res.status() == StatusCode::BAD_REQUEST,
741 "Old refresh token should be invalid after use"
742 );
743}
744
745async fn create_like(
746 client: &reqwest::Client,
747 liker_did: &str,
748 liker_jwt: &str,
749 subject_uri: &str,
750 subject_cid: &str,
751) -> (String, String) {
752 let collection = "app.bsky.feed.like";
753 let rkey = format!("e2e_like_{}", Utc::now().timestamp_millis());
754 let now = Utc::now().to_rfc3339();
755
756 let payload = json!({
757 "repo": liker_did,
758 "collection": collection,
759 "rkey": rkey,
760 "record": {
761 "$type": collection,
762 "subject": {
763 "uri": subject_uri,
764 "cid": subject_cid
765 },
766 "createdAt": now
767 }
768 });
769
770 let res = client
771 .post(format!(
772 "{}/xrpc/com.atproto.repo.putRecord",
773 base_url().await
774 ))
775 .bearer_auth(liker_jwt)
776 .json(&payload)
777 .send()
778 .await
779 .expect("Failed to create like");
780
781 assert_eq!(res.status(), StatusCode::OK, "Failed to create like");
782 let body: Value = res.json().await.expect("Like response not JSON");
783 (
784 body["uri"].as_str().unwrap().to_string(),
785 body["cid"].as_str().unwrap().to_string(),
786 )
787}
788
789async fn create_repost(
790 client: &reqwest::Client,
791 reposter_did: &str,
792 reposter_jwt: &str,
793 subject_uri: &str,
794 subject_cid: &str,
795) -> (String, String) {
796 let collection = "app.bsky.feed.repost";
797 let rkey = format!("e2e_repost_{}", Utc::now().timestamp_millis());
798 let now = Utc::now().to_rfc3339();
799
800 let payload = json!({
801 "repo": reposter_did,
802 "collection": collection,
803 "rkey": rkey,
804 "record": {
805 "$type": collection,
806 "subject": {
807 "uri": subject_uri,
808 "cid": subject_cid
809 },
810 "createdAt": now
811 }
812 });
813
814 let res = client
815 .post(format!(
816 "{}/xrpc/com.atproto.repo.putRecord",
817 base_url().await
818 ))
819 .bearer_auth(reposter_jwt)
820 .json(&payload)
821 .send()
822 .await
823 .expect("Failed to create repost");
824
825 assert_eq!(res.status(), StatusCode::OK, "Failed to create repost");
826 let body: Value = res.json().await.expect("Repost response not JSON");
827 (
828 body["uri"].as_str().unwrap().to_string(),
829 body["cid"].as_str().unwrap().to_string(),
830 )
831}
832
833#[tokio::test]
834async fn test_profile_lifecycle() {
835 let client = client();
836 let (did, jwt) = setup_new_user("profile-lifecycle").await;
837
838 let profile_payload = json!({
839 "repo": did,
840 "collection": "app.bsky.actor.profile",
841 "rkey": "self",
842 "record": {
843 "$type": "app.bsky.actor.profile",
844 "displayName": "Test User",
845 "description": "A test profile for lifecycle testing"
846 }
847 });
848
849 let create_res = client
850 .post(format!(
851 "{}/xrpc/com.atproto.repo.putRecord",
852 base_url().await
853 ))
854 .bearer_auth(&jwt)
855 .json(&profile_payload)
856 .send()
857 .await
858 .expect("Failed to create profile");
859
860 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile");
861 let create_body: Value = create_res.json().await.unwrap();
862 let initial_cid = create_body["cid"].as_str().unwrap().to_string();
863
864 let get_res = client
865 .get(format!(
866 "{}/xrpc/com.atproto.repo.getRecord",
867 base_url().await
868 ))
869 .query(&[
870 ("repo", did.as_str()),
871 ("collection", "app.bsky.actor.profile"),
872 ("rkey", "self"),
873 ])
874 .send()
875 .await
876 .expect("Failed to get profile");
877
878 assert_eq!(get_res.status(), StatusCode::OK);
879 let get_body: Value = get_res.json().await.unwrap();
880 assert_eq!(get_body["value"]["displayName"], "Test User");
881 assert_eq!(get_body["value"]["description"], "A test profile for lifecycle testing");
882
883 let update_payload = json!({
884 "repo": did,
885 "collection": "app.bsky.actor.profile",
886 "rkey": "self",
887 "record": {
888 "$type": "app.bsky.actor.profile",
889 "displayName": "Updated User",
890 "description": "Profile has been updated"
891 },
892 "swapRecord": initial_cid
893 });
894
895 let update_res = client
896 .post(format!(
897 "{}/xrpc/com.atproto.repo.putRecord",
898 base_url().await
899 ))
900 .bearer_auth(&jwt)
901 .json(&update_payload)
902 .send()
903 .await
904 .expect("Failed to update profile");
905
906 assert_eq!(update_res.status(), StatusCode::OK, "Failed to update profile");
907
908 let get_updated_res = client
909 .get(format!(
910 "{}/xrpc/com.atproto.repo.getRecord",
911 base_url().await
912 ))
913 .query(&[
914 ("repo", did.as_str()),
915 ("collection", "app.bsky.actor.profile"),
916 ("rkey", "self"),
917 ])
918 .send()
919 .await
920 .expect("Failed to get updated profile");
921
922 let updated_body: Value = get_updated_res.json().await.unwrap();
923 assert_eq!(updated_body["value"]["displayName"], "Updated User");
924}
925
926#[tokio::test]
927async fn test_reply_thread_lifecycle() {
928 let client = client();
929
930 let (alice_did, alice_jwt) = setup_new_user("alice-thread").await;
931 let (bob_did, bob_jwt) = setup_new_user("bob-thread").await;
932
933 let (root_uri, root_cid) = create_post(&client, &alice_did, &alice_jwt, "This is the root post").await;
934
935 tokio::time::sleep(Duration::from_millis(100)).await;
936
937 let reply_collection = "app.bsky.feed.post";
938 let reply_rkey = format!("e2e_reply_{}", Utc::now().timestamp_millis());
939 let now = Utc::now().to_rfc3339();
940
941 let reply_payload = json!({
942 "repo": bob_did,
943 "collection": reply_collection,
944 "rkey": reply_rkey,
945 "record": {
946 "$type": reply_collection,
947 "text": "This is Bob's reply to Alice",
948 "createdAt": now,
949 "reply": {
950 "root": {
951 "uri": root_uri,
952 "cid": root_cid
953 },
954 "parent": {
955 "uri": root_uri,
956 "cid": root_cid
957 }
958 }
959 }
960 });
961
962 let reply_res = client
963 .post(format!(
964 "{}/xrpc/com.atproto.repo.putRecord",
965 base_url().await
966 ))
967 .bearer_auth(&bob_jwt)
968 .json(&reply_payload)
969 .send()
970 .await
971 .expect("Failed to create reply");
972
973 assert_eq!(reply_res.status(), StatusCode::OK, "Failed to create reply");
974 let reply_body: Value = reply_res.json().await.unwrap();
975 let reply_uri = reply_body["uri"].as_str().unwrap();
976 let reply_cid = reply_body["cid"].as_str().unwrap();
977
978 let get_reply_res = client
979 .get(format!(
980 "{}/xrpc/com.atproto.repo.getRecord",
981 base_url().await
982 ))
983 .query(&[
984 ("repo", bob_did.as_str()),
985 ("collection", reply_collection),
986 ("rkey", reply_rkey.as_str()),
987 ])
988 .send()
989 .await
990 .expect("Failed to get reply");
991
992 assert_eq!(get_reply_res.status(), StatusCode::OK);
993 let reply_record: Value = get_reply_res.json().await.unwrap();
994 assert_eq!(reply_record["value"]["reply"]["root"]["uri"], root_uri);
995 assert_eq!(reply_record["value"]["reply"]["parent"]["uri"], root_uri);
996
997 tokio::time::sleep(Duration::from_millis(100)).await;
998
999 let nested_reply_rkey = format!("e2e_nested_reply_{}", Utc::now().timestamp_millis());
1000 let nested_payload = json!({
1001 "repo": alice_did,
1002 "collection": reply_collection,
1003 "rkey": nested_reply_rkey,
1004 "record": {
1005 "$type": reply_collection,
1006 "text": "Alice replies to Bob's reply",
1007 "createdAt": Utc::now().to_rfc3339(),
1008 "reply": {
1009 "root": {
1010 "uri": root_uri,
1011 "cid": root_cid
1012 },
1013 "parent": {
1014 "uri": reply_uri,
1015 "cid": reply_cid
1016 }
1017 }
1018 }
1019 });
1020
1021 let nested_res = client
1022 .post(format!(
1023 "{}/xrpc/com.atproto.repo.putRecord",
1024 base_url().await
1025 ))
1026 .bearer_auth(&alice_jwt)
1027 .json(&nested_payload)
1028 .send()
1029 .await
1030 .expect("Failed to create nested reply");
1031
1032 assert_eq!(nested_res.status(), StatusCode::OK, "Failed to create nested reply");
1033}
1034
1035#[tokio::test]
1036async fn test_like_lifecycle() {
1037 let client = client();
1038
1039 let (alice_did, alice_jwt) = setup_new_user("alice-like").await;
1040 let (bob_did, bob_jwt) = setup_new_user("bob-like").await;
1041
1042 let (post_uri, post_cid) = create_post(&client, &alice_did, &alice_jwt, "Like this post!").await;
1043
1044 let (like_uri, _) = create_like(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await;
1045
1046 let like_rkey = like_uri.split('/').last().unwrap();
1047 let get_like_res = client
1048 .get(format!(
1049 "{}/xrpc/com.atproto.repo.getRecord",
1050 base_url().await
1051 ))
1052 .query(&[
1053 ("repo", bob_did.as_str()),
1054 ("collection", "app.bsky.feed.like"),
1055 ("rkey", like_rkey),
1056 ])
1057 .send()
1058 .await
1059 .expect("Failed to get like");
1060
1061 assert_eq!(get_like_res.status(), StatusCode::OK);
1062 let like_body: Value = get_like_res.json().await.unwrap();
1063 assert_eq!(like_body["value"]["subject"]["uri"], post_uri);
1064
1065 let delete_payload = json!({
1066 "repo": bob_did,
1067 "collection": "app.bsky.feed.like",
1068 "rkey": like_rkey
1069 });
1070
1071 let delete_res = client
1072 .post(format!(
1073 "{}/xrpc/com.atproto.repo.deleteRecord",
1074 base_url().await
1075 ))
1076 .bearer_auth(&bob_jwt)
1077 .json(&delete_payload)
1078 .send()
1079 .await
1080 .expect("Failed to delete like");
1081
1082 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete like");
1083
1084 let get_deleted_res = client
1085 .get(format!(
1086 "{}/xrpc/com.atproto.repo.getRecord",
1087 base_url().await
1088 ))
1089 .query(&[
1090 ("repo", bob_did.as_str()),
1091 ("collection", "app.bsky.feed.like"),
1092 ("rkey", like_rkey),
1093 ])
1094 .send()
1095 .await
1096 .expect("Failed to check deleted like");
1097
1098 assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Like should be deleted");
1099}
1100
1101#[tokio::test]
1102async fn test_repost_lifecycle() {
1103 let client = client();
1104
1105 let (alice_did, alice_jwt) = setup_new_user("alice-repost").await;
1106 let (bob_did, bob_jwt) = setup_new_user("bob-repost").await;
1107
1108 let (post_uri, post_cid) = create_post(&client, &alice_did, &alice_jwt, "Repost this!").await;
1109
1110 let (repost_uri, _) = create_repost(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await;
1111
1112 let repost_rkey = repost_uri.split('/').last().unwrap();
1113 let get_repost_res = client
1114 .get(format!(
1115 "{}/xrpc/com.atproto.repo.getRecord",
1116 base_url().await
1117 ))
1118 .query(&[
1119 ("repo", bob_did.as_str()),
1120 ("collection", "app.bsky.feed.repost"),
1121 ("rkey", repost_rkey),
1122 ])
1123 .send()
1124 .await
1125 .expect("Failed to get repost");
1126
1127 assert_eq!(get_repost_res.status(), StatusCode::OK);
1128 let repost_body: Value = get_repost_res.json().await.unwrap();
1129 assert_eq!(repost_body["value"]["subject"]["uri"], post_uri);
1130
1131 let delete_payload = json!({
1132 "repo": bob_did,
1133 "collection": "app.bsky.feed.repost",
1134 "rkey": repost_rkey
1135 });
1136
1137 let delete_res = client
1138 .post(format!(
1139 "{}/xrpc/com.atproto.repo.deleteRecord",
1140 base_url().await
1141 ))
1142 .bearer_auth(&bob_jwt)
1143 .json(&delete_payload)
1144 .send()
1145 .await
1146 .expect("Failed to delete repost");
1147
1148 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete repost");
1149}
1150
1151#[tokio::test]
1152async fn test_unfollow_lifecycle() {
1153 let client = client();
1154
1155 let (alice_did, _alice_jwt) = setup_new_user("alice-unfollow").await;
1156 let (bob_did, bob_jwt) = setup_new_user("bob-unfollow").await;
1157
1158 let (follow_uri, _) = create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
1159
1160 let follow_rkey = follow_uri.split('/').last().unwrap();
1161 let get_follow_res = client
1162 .get(format!(
1163 "{}/xrpc/com.atproto.repo.getRecord",
1164 base_url().await
1165 ))
1166 .query(&[
1167 ("repo", bob_did.as_str()),
1168 ("collection", "app.bsky.graph.follow"),
1169 ("rkey", follow_rkey),
1170 ])
1171 .send()
1172 .await
1173 .expect("Failed to get follow");
1174
1175 assert_eq!(get_follow_res.status(), StatusCode::OK);
1176
1177 let unfollow_payload = json!({
1178 "repo": bob_did,
1179 "collection": "app.bsky.graph.follow",
1180 "rkey": follow_rkey
1181 });
1182
1183 let unfollow_res = client
1184 .post(format!(
1185 "{}/xrpc/com.atproto.repo.deleteRecord",
1186 base_url().await
1187 ))
1188 .bearer_auth(&bob_jwt)
1189 .json(&unfollow_payload)
1190 .send()
1191 .await
1192 .expect("Failed to unfollow");
1193
1194 assert_eq!(unfollow_res.status(), StatusCode::OK, "Failed to unfollow");
1195
1196 let get_deleted_res = client
1197 .get(format!(
1198 "{}/xrpc/com.atproto.repo.getRecord",
1199 base_url().await
1200 ))
1201 .query(&[
1202 ("repo", bob_did.as_str()),
1203 ("collection", "app.bsky.graph.follow"),
1204 ("rkey", follow_rkey),
1205 ])
1206 .send()
1207 .await
1208 .expect("Failed to check deleted follow");
1209
1210 assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Follow should be deleted");
1211}
1212
1213#[tokio::test]
1214async fn test_timeline_after_unfollow() {
1215 let client = client();
1216
1217 let (alice_did, alice_jwt) = setup_new_user("alice-tl-unfollow").await;
1218 let (bob_did, bob_jwt) = setup_new_user("bob-tl-unfollow").await;
1219
1220 let (follow_uri, _) = create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
1221
1222 create_post(&client, &alice_did, &alice_jwt, "Post while following").await;
1223
1224 tokio::time::sleep(Duration::from_secs(1)).await;
1225
1226 let timeline_res = client
1227 .get(format!(
1228 "{}/xrpc/app.bsky.feed.getTimeline",
1229 base_url().await
1230 ))
1231 .bearer_auth(&bob_jwt)
1232 .send()
1233 .await
1234 .expect("Failed to get timeline");
1235
1236 assert_eq!(timeline_res.status(), StatusCode::OK);
1237 let timeline_body: Value = timeline_res.json().await.unwrap();
1238 let feed = timeline_body["feed"].as_array().unwrap();
1239 assert_eq!(feed.len(), 1, "Should see 1 post from Alice");
1240
1241 let follow_rkey = follow_uri.split('/').last().unwrap();
1242 let unfollow_payload = json!({
1243 "repo": bob_did,
1244 "collection": "app.bsky.graph.follow",
1245 "rkey": follow_rkey
1246 });
1247 client
1248 .post(format!(
1249 "{}/xrpc/com.atproto.repo.deleteRecord",
1250 base_url().await
1251 ))
1252 .bearer_auth(&bob_jwt)
1253 .json(&unfollow_payload)
1254 .send()
1255 .await
1256 .expect("Failed to unfollow");
1257
1258 tokio::time::sleep(Duration::from_secs(1)).await;
1259
1260 let timeline_after_res = client
1261 .get(format!(
1262 "{}/xrpc/app.bsky.feed.getTimeline",
1263 base_url().await
1264 ))
1265 .bearer_auth(&bob_jwt)
1266 .send()
1267 .await
1268 .expect("Failed to get timeline after unfollow");
1269
1270 assert_eq!(timeline_after_res.status(), StatusCode::OK);
1271 let timeline_after: Value = timeline_after_res.json().await.unwrap();
1272 let feed_after = timeline_after["feed"].as_array().unwrap();
1273 assert_eq!(feed_after.len(), 0, "Should see 0 posts after unfollowing");
1274}
1275
1276#[tokio::test]
1277async fn test_blob_in_record_lifecycle() {
1278 let client = client();
1279 let (did, jwt) = setup_new_user("blob-record").await;
1280
1281 let blob_data = b"This is test blob data for a profile avatar";
1282 let upload_res = client
1283 .post(format!(
1284 "{}/xrpc/com.atproto.repo.uploadBlob",
1285 base_url().await
1286 ))
1287 .header(header::CONTENT_TYPE, "text/plain")
1288 .bearer_auth(&jwt)
1289 .body(blob_data.to_vec())
1290 .send()
1291 .await
1292 .expect("Failed to upload blob");
1293
1294 assert_eq!(upload_res.status(), StatusCode::OK);
1295 let upload_body: Value = upload_res.json().await.unwrap();
1296 let blob_ref = upload_body["blob"].clone();
1297
1298 let profile_payload = json!({
1299 "repo": did,
1300 "collection": "app.bsky.actor.profile",
1301 "rkey": "self",
1302 "record": {
1303 "$type": "app.bsky.actor.profile",
1304 "displayName": "User With Avatar",
1305 "avatar": blob_ref
1306 }
1307 });
1308
1309 let create_res = client
1310 .post(format!(
1311 "{}/xrpc/com.atproto.repo.putRecord",
1312 base_url().await
1313 ))
1314 .bearer_auth(&jwt)
1315 .json(&profile_payload)
1316 .send()
1317 .await
1318 .expect("Failed to create profile with blob");
1319
1320 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile with blob");
1321
1322 let get_res = client
1323 .get(format!(
1324 "{}/xrpc/com.atproto.repo.getRecord",
1325 base_url().await
1326 ))
1327 .query(&[
1328 ("repo", did.as_str()),
1329 ("collection", "app.bsky.actor.profile"),
1330 ("rkey", "self"),
1331 ])
1332 .send()
1333 .await
1334 .expect("Failed to get profile");
1335
1336 assert_eq!(get_res.status(), StatusCode::OK);
1337 let profile: Value = get_res.json().await.unwrap();
1338 assert!(profile["value"]["avatar"]["ref"]["$link"].is_string());
1339}
1340
1341#[tokio::test]
1342async fn test_authorization_cannot_modify_other_repo() {
1343 let client = client();
1344
1345 let (alice_did, _alice_jwt) = setup_new_user("alice-auth").await;
1346 let (_bob_did, bob_jwt) = setup_new_user("bob-auth").await;
1347
1348 let post_payload = json!({
1349 "repo": alice_did,
1350 "collection": "app.bsky.feed.post",
1351 "rkey": "unauthorized-post",
1352 "record": {
1353 "$type": "app.bsky.feed.post",
1354 "text": "Bob trying to post as Alice",
1355 "createdAt": Utc::now().to_rfc3339()
1356 }
1357 });
1358
1359 let res = client
1360 .post(format!(
1361 "{}/xrpc/com.atproto.repo.putRecord",
1362 base_url().await
1363 ))
1364 .bearer_auth(&bob_jwt)
1365 .json(&post_payload)
1366 .send()
1367 .await
1368 .expect("Failed to send request");
1369
1370 assert!(
1371 res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED,
1372 "Expected 403 or 401 when writing to another user's repo, got {}",
1373 res.status()
1374 );
1375}
1376
1377#[tokio::test]
1378async fn test_authorization_cannot_delete_other_record() {
1379 let client = client();
1380
1381 let (alice_did, alice_jwt) = setup_new_user("alice-del-auth").await;
1382 let (_bob_did, bob_jwt) = setup_new_user("bob-del-auth").await;
1383
1384 let (post_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's post").await;
1385 let post_rkey = post_uri.split('/').last().unwrap();
1386
1387 let delete_payload = json!({
1388 "repo": alice_did,
1389 "collection": "app.bsky.feed.post",
1390 "rkey": post_rkey
1391 });
1392
1393 let res = client
1394 .post(format!(
1395 "{}/xrpc/com.atproto.repo.deleteRecord",
1396 base_url().await
1397 ))
1398 .bearer_auth(&bob_jwt)
1399 .json(&delete_payload)
1400 .send()
1401 .await
1402 .expect("Failed to send request");
1403
1404 assert!(
1405 res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED,
1406 "Expected 403 or 401 when deleting another user's record, got {}",
1407 res.status()
1408 );
1409
1410 let get_res = client
1411 .get(format!(
1412 "{}/xrpc/com.atproto.repo.getRecord",
1413 base_url().await
1414 ))
1415 .query(&[
1416 ("repo", alice_did.as_str()),
1417 ("collection", "app.bsky.feed.post"),
1418 ("rkey", post_rkey),
1419 ])
1420 .send()
1421 .await
1422 .expect("Failed to verify record exists");
1423
1424 assert_eq!(get_res.status(), StatusCode::OK, "Record should still exist");
1425}
1426
1427#[tokio::test]
1428async fn test_list_records_pagination() {
1429 let client = client();
1430 let (did, jwt) = setup_new_user("list-pagination").await;
1431
1432 for i in 0..5 {
1433 tokio::time::sleep(Duration::from_millis(50)).await;
1434 create_post(&client, &did, &jwt, &format!("Post number {}", i)).await;
1435 }
1436
1437 let list_res = client
1438 .get(format!(
1439 "{}/xrpc/com.atproto.repo.listRecords",
1440 base_url().await
1441 ))
1442 .query(&[
1443 ("repo", did.as_str()),
1444 ("collection", "app.bsky.feed.post"),
1445 ("limit", "2"),
1446 ])
1447 .send()
1448 .await
1449 .expect("Failed to list records");
1450
1451 assert_eq!(list_res.status(), StatusCode::OK);
1452 let list_body: Value = list_res.json().await.unwrap();
1453 let records = list_body["records"].as_array().unwrap();
1454 assert_eq!(records.len(), 2, "Should return 2 records with limit=2");
1455
1456 if let Some(cursor) = list_body["cursor"].as_str() {
1457 let list_page2_res = client
1458 .get(format!(
1459 "{}/xrpc/com.atproto.repo.listRecords",
1460 base_url().await
1461 ))
1462 .query(&[
1463 ("repo", did.as_str()),
1464 ("collection", "app.bsky.feed.post"),
1465 ("limit", "2"),
1466 ("cursor", cursor),
1467 ])
1468 .send()
1469 .await
1470 .expect("Failed to list records page 2");
1471
1472 assert_eq!(list_page2_res.status(), StatusCode::OK);
1473 let page2_body: Value = list_page2_res.json().await.unwrap();
1474 let page2_records = page2_body["records"].as_array().unwrap();
1475 assert_eq!(page2_records.len(), 2, "Page 2 should have 2 more records");
1476 }
1477}
1478
1479#[tokio::test]
1480async fn test_mutual_follow_lifecycle() {
1481 let client = client();
1482
1483 let (alice_did, alice_jwt) = setup_new_user("alice-mutual").await;
1484 let (bob_did, bob_jwt) = setup_new_user("bob-mutual").await;
1485
1486 create_follow(&client, &alice_did, &alice_jwt, &bob_did).await;
1487 create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
1488
1489 create_post(&client, &alice_did, &alice_jwt, "Alice's post for mutual").await;
1490 create_post(&client, &bob_did, &bob_jwt, "Bob's post for mutual").await;
1491
1492 tokio::time::sleep(Duration::from_secs(1)).await;
1493
1494 let alice_timeline_res = client
1495 .get(format!(
1496 "{}/xrpc/app.bsky.feed.getTimeline",
1497 base_url().await
1498 ))
1499 .bearer_auth(&alice_jwt)
1500 .send()
1501 .await
1502 .expect("Failed to get Alice's timeline");
1503
1504 assert_eq!(alice_timeline_res.status(), StatusCode::OK);
1505 let alice_tl: Value = alice_timeline_res.json().await.unwrap();
1506 let alice_feed = alice_tl["feed"].as_array().unwrap();
1507 assert_eq!(alice_feed.len(), 1, "Alice should see Bob's 1 post");
1508
1509 let bob_timeline_res = client
1510 .get(format!(
1511 "{}/xrpc/app.bsky.feed.getTimeline",
1512 base_url().await
1513 ))
1514 .bearer_auth(&bob_jwt)
1515 .send()
1516 .await
1517 .expect("Failed to get Bob's timeline");
1518
1519 assert_eq!(bob_timeline_res.status(), StatusCode::OK);
1520 let bob_tl: Value = bob_timeline_res.json().await.unwrap();
1521 let bob_feed = bob_tl["feed"].as_array().unwrap();
1522 assert_eq!(bob_feed.len(), 1, "Bob should see Alice's 1 post");
1523}
1524
1525#[tokio::test]
1526async fn test_account_to_post_full_lifecycle() {
1527 let client = client();
1528 let ts = Utc::now().timestamp_millis();
1529 let handle = format!("fullcycle-{}.test", ts);
1530 let email = format!("fullcycle-{}@test.com", ts);
1531 let password = "fullcycle-password";
1532
1533 let create_account_res = client
1534 .post(format!(
1535 "{}/xrpc/com.atproto.server.createAccount",
1536 base_url().await
1537 ))
1538 .json(&json!({
1539 "handle": handle,
1540 "email": email,
1541 "password": password
1542 }))
1543 .send()
1544 .await
1545 .expect("Failed to create account");
1546
1547 assert_eq!(create_account_res.status(), StatusCode::OK);
1548 let account_body: Value = create_account_res.json().await.unwrap();
1549 let did = account_body["did"].as_str().unwrap().to_string();
1550 let access_jwt = account_body["accessJwt"].as_str().unwrap().to_string();
1551
1552 let get_session_res = client
1553 .get(format!(
1554 "{}/xrpc/com.atproto.server.getSession",
1555 base_url().await
1556 ))
1557 .bearer_auth(&access_jwt)
1558 .send()
1559 .await
1560 .expect("Failed to get session");
1561
1562 assert_eq!(get_session_res.status(), StatusCode::OK);
1563 let session_body: Value = get_session_res.json().await.unwrap();
1564 assert_eq!(session_body["did"], did);
1565 assert_eq!(session_body["handle"], handle);
1566
1567 let profile_res = client
1568 .post(format!(
1569 "{}/xrpc/com.atproto.repo.putRecord",
1570 base_url().await
1571 ))
1572 .bearer_auth(&access_jwt)
1573 .json(&json!({
1574 "repo": did,
1575 "collection": "app.bsky.actor.profile",
1576 "rkey": "self",
1577 "record": {
1578 "$type": "app.bsky.actor.profile",
1579 "displayName": "Full Cycle User"
1580 }
1581 }))
1582 .send()
1583 .await
1584 .expect("Failed to create profile");
1585
1586 assert_eq!(profile_res.status(), StatusCode::OK);
1587
1588 let (post_uri, post_cid) = create_post(&client, &did, &access_jwt, "My first post!").await;
1589
1590 let get_post_res = client
1591 .get(format!(
1592 "{}/xrpc/com.atproto.repo.getRecord",
1593 base_url().await
1594 ))
1595 .query(&[
1596 ("repo", did.as_str()),
1597 ("collection", "app.bsky.feed.post"),
1598 ("rkey", post_uri.split('/').last().unwrap()),
1599 ])
1600 .send()
1601 .await
1602 .expect("Failed to get post");
1603
1604 assert_eq!(get_post_res.status(), StatusCode::OK);
1605
1606 create_like(&client, &did, &access_jwt, &post_uri, &post_cid).await;
1607
1608 let describe_res = client
1609 .get(format!(
1610 "{}/xrpc/com.atproto.repo.describeRepo",
1611 base_url().await
1612 ))
1613 .query(&[("repo", did.as_str())])
1614 .send()
1615 .await
1616 .expect("Failed to describe repo");
1617
1618 assert_eq!(describe_res.status(), StatusCode::OK);
1619 let describe_body: Value = describe_res.json().await.unwrap();
1620 assert_eq!(describe_body["did"], did);
1621 assert_eq!(describe_body["handle"], handle);
1622}
1623
1624#[tokio::test]
1625async fn test_app_password_lifecycle() {
1626 let client = client();
1627 let ts = Utc::now().timestamp_millis();
1628 let handle = format!("apppass-{}.test", ts);
1629 let email = format!("apppass-{}@test.com", ts);
1630 let password = "apppass-password";
1631
1632 let create_res = client
1633 .post(format!(
1634 "{}/xrpc/com.atproto.server.createAccount",
1635 base_url().await
1636 ))
1637 .json(&json!({
1638 "handle": handle,
1639 "email": email,
1640 "password": password
1641 }))
1642 .send()
1643 .await
1644 .expect("Failed to create account");
1645
1646 assert_eq!(create_res.status(), StatusCode::OK);
1647 let account: Value = create_res.json().await.unwrap();
1648 let jwt = account["accessJwt"].as_str().unwrap();
1649
1650 let create_app_pass_res = client
1651 .post(format!(
1652 "{}/xrpc/com.atproto.server.createAppPassword",
1653 base_url().await
1654 ))
1655 .bearer_auth(jwt)
1656 .json(&json!({ "name": "Test App" }))
1657 .send()
1658 .await
1659 .expect("Failed to create app password");
1660
1661 assert_eq!(create_app_pass_res.status(), StatusCode::OK);
1662 let app_pass: Value = create_app_pass_res.json().await.unwrap();
1663 let app_password = app_pass["password"].as_str().unwrap().to_string();
1664 assert_eq!(app_pass["name"], "Test App");
1665
1666 let list_res = client
1667 .get(format!(
1668 "{}/xrpc/com.atproto.server.listAppPasswords",
1669 base_url().await
1670 ))
1671 .bearer_auth(jwt)
1672 .send()
1673 .await
1674 .expect("Failed to list app passwords");
1675
1676 assert_eq!(list_res.status(), StatusCode::OK);
1677 let list_body: Value = list_res.json().await.unwrap();
1678 let passwords = list_body["passwords"].as_array().unwrap();
1679 assert_eq!(passwords.len(), 1);
1680 assert_eq!(passwords[0]["name"], "Test App");
1681
1682 let login_res = client
1683 .post(format!(
1684 "{}/xrpc/com.atproto.server.createSession",
1685 base_url().await
1686 ))
1687 .json(&json!({
1688 "identifier": handle,
1689 "password": app_password
1690 }))
1691 .send()
1692 .await
1693 .expect("Failed to login with app password");
1694
1695 assert_eq!(login_res.status(), StatusCode::OK, "App password login should work");
1696
1697 let revoke_res = client
1698 .post(format!(
1699 "{}/xrpc/com.atproto.server.revokeAppPassword",
1700 base_url().await
1701 ))
1702 .bearer_auth(jwt)
1703 .json(&json!({ "name": "Test App" }))
1704 .send()
1705 .await
1706 .expect("Failed to revoke app password");
1707
1708 assert_eq!(revoke_res.status(), StatusCode::OK);
1709
1710 let login_after_revoke = client
1711 .post(format!(
1712 "{}/xrpc/com.atproto.server.createSession",
1713 base_url().await
1714 ))
1715 .json(&json!({
1716 "identifier": handle,
1717 "password": app_password
1718 }))
1719 .send()
1720 .await
1721 .expect("Failed to attempt login after revoke");
1722
1723 assert!(
1724 login_after_revoke.status() == StatusCode::UNAUTHORIZED
1725 || login_after_revoke.status() == StatusCode::BAD_REQUEST,
1726 "Revoked app password should not work"
1727 );
1728
1729 let list_after_revoke = client
1730 .get(format!(
1731 "{}/xrpc/com.atproto.server.listAppPasswords",
1732 base_url().await
1733 ))
1734 .bearer_auth(jwt)
1735 .send()
1736 .await
1737 .expect("Failed to list after revoke");
1738
1739 let list_after: Value = list_after_revoke.json().await.unwrap();
1740 let passwords_after = list_after["passwords"].as_array().unwrap();
1741 assert_eq!(passwords_after.len(), 0, "No app passwords should remain");
1742}
1743
1744#[tokio::test]
1745async fn test_account_deactivation_lifecycle() {
1746 let client = client();
1747 let ts = Utc::now().timestamp_millis();
1748 let handle = format!("deactivate-{}.test", ts);
1749 let email = format!("deactivate-{}@test.com", ts);
1750 let password = "deactivate-password";
1751
1752 let create_res = client
1753 .post(format!(
1754 "{}/xrpc/com.atproto.server.createAccount",
1755 base_url().await
1756 ))
1757 .json(&json!({
1758 "handle": handle,
1759 "email": email,
1760 "password": password
1761 }))
1762 .send()
1763 .await
1764 .expect("Failed to create account");
1765
1766 assert_eq!(create_res.status(), StatusCode::OK);
1767 let account: Value = create_res.json().await.unwrap();
1768 let did = account["did"].as_str().unwrap().to_string();
1769 let jwt = account["accessJwt"].as_str().unwrap().to_string();
1770
1771 let (post_uri, _) = create_post(&client, &did, &jwt, "Post before deactivation").await;
1772 let post_rkey = post_uri.split('/').last().unwrap();
1773
1774 let status_before = client
1775 .get(format!(
1776 "{}/xrpc/com.atproto.server.checkAccountStatus",
1777 base_url().await
1778 ))
1779 .bearer_auth(&jwt)
1780 .send()
1781 .await
1782 .expect("Failed to check status");
1783
1784 assert_eq!(status_before.status(), StatusCode::OK);
1785 let status_body: Value = status_before.json().await.unwrap();
1786 assert_eq!(status_body["activated"], true);
1787
1788 let deactivate_res = client
1789 .post(format!(
1790 "{}/xrpc/com.atproto.server.deactivateAccount",
1791 base_url().await
1792 ))
1793 .bearer_auth(&jwt)
1794 .json(&json!({}))
1795 .send()
1796 .await
1797 .expect("Failed to deactivate");
1798
1799 assert_eq!(deactivate_res.status(), StatusCode::OK);
1800
1801 let get_post_res = client
1802 .get(format!(
1803 "{}/xrpc/com.atproto.repo.getRecord",
1804 base_url().await
1805 ))
1806 .query(&[
1807 ("repo", did.as_str()),
1808 ("collection", "app.bsky.feed.post"),
1809 ("rkey", post_rkey),
1810 ])
1811 .send()
1812 .await
1813 .expect("Failed to get post while deactivated");
1814
1815 assert_eq!(get_post_res.status(), StatusCode::OK, "Records should still be readable");
1816
1817 let activate_res = client
1818 .post(format!(
1819 "{}/xrpc/com.atproto.server.activateAccount",
1820 base_url().await
1821 ))
1822 .bearer_auth(&jwt)
1823 .json(&json!({}))
1824 .send()
1825 .await
1826 .expect("Failed to reactivate");
1827
1828 assert_eq!(activate_res.status(), StatusCode::OK);
1829
1830 let status_after_activate = client
1831 .get(format!(
1832 "{}/xrpc/com.atproto.server.checkAccountStatus",
1833 base_url().await
1834 ))
1835 .bearer_auth(&jwt)
1836 .send()
1837 .await
1838 .expect("Failed to check status after activate");
1839
1840 assert_eq!(status_after_activate.status(), StatusCode::OK);
1841
1842 let (new_post_uri, _) = create_post(&client, &did, &jwt, "Post after reactivation").await;
1843 assert!(!new_post_uri.is_empty(), "Should be able to post after reactivation");
1844}