this repo has no description
1mod common;
2use common::*;
3
4use reqwest::StatusCode;
5use serde_json::{json, Value};
6use chrono::Utc;
7use std::time::Duration;
8
9use reqwest::Client;
10#[allow(unused_imports)]
11use std::collections::HashMap;
12
13#[tokio::test]
14async fn test_post_crud_lifecycle() {
15 let client = client();
16 let collection = "app.bsky.feed.post";
17
18 let rkey = format!("e2e_lifecycle_{}", Utc::now().timestamp_millis());
19 let now = Utc::now().to_rfc3339();
20
21 let original_text = "Hello from the lifecycle test!";
22 let create_payload = json!({
23 "repo": AUTH_DID,
24 "collection": collection,
25 "rkey": rkey,
26 "record": {
27 "$type": collection,
28 "text": original_text,
29 "createdAt": now
30 }
31 });
32
33 let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
34 .bearer_auth(AUTH_TOKEN)
35 .json(&create_payload)
36 .send()
37 .await
38 .expect("Failed to send create request");
39
40 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create record");
41 let create_body: Value = create_res.json().await.expect("create response was not JSON");
42 let uri = create_body["uri"].as_str().unwrap();
43
44
45 let params = [
46 ("repo", AUTH_DID),
47 ("collection", collection),
48 ("rkey", &rkey),
49 ];
50 let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
51 .query(¶ms)
52 .send()
53 .await
54 .expect("Failed to send get request");
55
56 assert_eq!(get_res.status(), StatusCode::OK, "Failed to get record after create");
57 let get_body: Value = get_res.json().await.expect("get response was not JSON");
58 assert_eq!(get_body["uri"], uri);
59 assert_eq!(get_body["value"]["text"], original_text);
60
61
62 let updated_text = "This post has been updated.";
63 let update_payload = json!({
64 "repo": AUTH_DID,
65 "collection": collection,
66 "rkey": rkey,
67 "record": {
68 "$type": collection,
69 "text": updated_text,
70 "createdAt": now
71 }
72 });
73
74 let update_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
75 .bearer_auth(AUTH_TOKEN)
76 .json(&update_payload)
77 .send()
78 .await
79 .expect("Failed to send update request");
80
81 assert_eq!(update_res.status(), StatusCode::OK, "Failed to update record");
82
83
84 let get_updated_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
85 .query(¶ms)
86 .send()
87 .await
88 .expect("Failed to send get-after-update request");
89
90 assert_eq!(get_updated_res.status(), StatusCode::OK, "Failed to get record after update");
91 let get_updated_body: Value = get_updated_res.json().await.expect("get-updated response was not JSON");
92 assert_eq!(get_updated_body["value"]["text"], updated_text, "Text was not updated");
93
94
95 let delete_payload = json!({
96 "repo": AUTH_DID,
97 "collection": collection,
98 "rkey": rkey
99 });
100
101 let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
102 .bearer_auth(AUTH_TOKEN)
103 .json(&delete_payload)
104 .send()
105 .await
106 .expect("Failed to send delete request");
107
108 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete record");
109
110
111 let get_deleted_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
112 .query(¶ms)
113 .send()
114 .await
115 .expect("Failed to send get-after-delete request");
116
117 assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Record was found, but it should be deleted");
118}
119
120#[tokio::test]
121async fn test_post_with_image_lifecycle() {
122 let client = client();
123
124 let now_str = Utc::now().to_rfc3339();
125 let fake_image_data = format!("This is a fake PNG for test at {}", now_str);
126
127 let image_blob = upload_test_blob(
128 &client,
129 Box::leak(fake_image_data.into_boxed_str()),
130 "image/png"
131 ).await;
132
133 let blob_ref = image_blob["ref"].clone();
134 assert!(blob_ref.is_object(), "Blob ref is not an object");
135
136
137 let collection = "app.bsky.feed.post";
138 let rkey = format!("e2e_image_post_{}", Utc::now().timestamp_millis());
139
140 let create_payload = json!({
141 "repo": AUTH_DID,
142 "collection": collection,
143 "rkey": rkey,
144 "record": {
145 "$type": collection,
146 "text": "Check out this image!",
147 "createdAt": Utc::now().to_rfc3339(),
148 "embed": {
149 "$type": "app.bsky.embed.images",
150 "images": [
151 {
152 "image": image_blob,
153 "alt": "A test image"
154 }
155 ]
156 }
157 }
158 });
159
160 let create_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
161 .bearer_auth(AUTH_TOKEN)
162 .json(&create_payload)
163 .send()
164 .await
165 .expect("Failed to create image post");
166
167 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create post with image");
168
169
170 let params = [
171 ("repo", AUTH_DID),
172 ("collection", collection),
173 ("rkey", &rkey),
174 ];
175 let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
176 .query(¶ms)
177 .send()
178 .await
179 .expect("Failed to get image post");
180
181 assert_eq!(get_res.status(), StatusCode::OK, "Failed to get image post");
182 let get_body: Value = get_res.json().await.expect("get image post was not JSON");
183
184 let embed_image = &get_body["value"]["embed"]["images"][0]["image"];
185 assert!(embed_image.is_object(), "Embedded image is missing");
186 assert_eq!(embed_image["ref"], blob_ref, "Embedded blob ref does not match uploaded ref");
187}
188
189#[tokio::test]
190async fn test_graph_lifecycle_follow_unfollow() {
191 let client = client();
192 let collection = "app.bsky.graph.follow";
193
194 let create_payload = json!({
195 "repo": AUTH_DID,
196 "collection": collection,
197 // "rkey" is omitted, server will generate it right?
198 "record": {
199 "$type": collection,
200 "subject": TARGET_DID,
201 "createdAt": Utc::now().to_rfc3339()
202 }
203 });
204
205 let create_res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL))
206 .bearer_auth(AUTH_TOKEN)
207 .json(&create_payload)
208 .send()
209 .await
210 .expect("Failed to send follow createRecord");
211
212 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create follow record");
213 let create_body: Value = create_res.json().await.expect("create follow response was not JSON");
214 let follow_uri = create_body["uri"].as_str().expect("Response had no URI");
215
216 let rkey = follow_uri.split('/').last().expect("URI was malformed");
217
218
219 let params_get_follows = [
220 ("actor", AUTH_DID),
221 ];
222 let get_follows_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL))
223 .query(¶ms_get_follows)
224 .bearer_auth(AUTH_TOKEN)
225 .send()
226 .await
227 .expect("Failed to send getFollows");
228
229 assert_eq!(get_follows_res.status(), StatusCode::OK, "getFollows did not return 200");
230 let get_follows_body: Value = get_follows_res.json().await.expect("getFollows response was not JSON");
231
232 let follows_list = get_follows_body["follows"].as_array().expect("follows key was not an array");
233 let is_following = follows_list.iter().any(|actor| {
234 actor["did"].as_str() == Some(TARGET_DID)
235 });
236
237 assert!(is_following, "getFollows list did not contain the target DID");
238
239
240 let delete_payload = json!({
241 "repo": AUTH_DID,
242 "collection": collection,
243 "rkey": rkey
244 });
245
246 let delete_res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
247 .bearer_auth(AUTH_TOKEN)
248 .json(&delete_payload)
249 .send()
250 .await
251 .expect("Failed to send unfollow deleteRecord");
252
253 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete follow record");
254
255
256 let get_unfollowed_res = client.get(format!("{}/xrpc/app.bsky.graph.getFollows", BASE_URL))
257 .query(¶ms_get_follows)
258 .bearer_auth(AUTH_TOKEN)
259 .send()
260 .await
261 .expect("Failed to send getFollows after delete");
262
263 assert_eq!(get_unfollowed_res.status(), StatusCode::OK, "getFollows (after delete) did not return 200");
264 let get_unfollowed_body: Value = get_unfollowed_res.json().await.expect("getFollows (after delete) was not JSON");
265
266 let follows_list_after = get_unfollowed_body["follows"].as_array().expect("follows key was not an array");
267 let is_still_following = follows_list_after.iter().any(|actor| {
268 actor["did"].as_str() == Some(TARGET_DID)
269 });
270
271 assert!(!is_still_following, "getFollows list *still* contains the target DID after unfollow");
272}
273
274#[tokio::test]
275async fn test_list_records_pagination() {
276 let client = client();
277 let collection = "app.bsky.feed.post";
278 let mut created_rkeys = Vec::new();
279
280 for i in 0..3 {
281 let rkey = format!("e2e_pagination_{}", Utc::now().timestamp_millis());
282 let payload = json!({
283 "repo": AUTH_DID,
284 "collection": collection,
285 "rkey": rkey,
286 "record": {
287 "$type": collection,
288 "text": format!("Pagination test post #{}", i),
289 "createdAt": Utc::now().to_rfc3339()
290 }
291 });
292
293 let res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
294 .bearer_auth(AUTH_TOKEN)
295 .json(&payload)
296 .send()
297 .await
298 .expect("Failed to create pagination post");
299
300 assert_eq!(res.status(), StatusCode::OK, "Failed to create post for pagination test");
301 created_rkeys.push(rkey);
302 tokio::time::sleep(Duration::from_millis(10)).await;
303 }
304
305 let params_page1 = [
306 ("repo", AUTH_DID),
307 ("collection", collection),
308 ("limit", "2"),
309 ];
310
311 let page1_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL))
312 .query(¶ms_page1)
313 .send()
314 .await
315 .expect("Failed to send listRecords (page 1)");
316
317 assert_eq!(page1_res.status(), StatusCode::OK, "listRecords (page 1) failed");
318 let page1_body: Value = page1_res.json().await.expect("listRecords (page 1) was not JSON");
319
320 let page1_records = page1_body["records"].as_array().expect("records was not an array");
321 assert_eq!(page1_records.len(), 2, "Page 1 did not return 2 records");
322
323 let cursor = page1_body["cursor"].as_str().expect("Page 1 did not have a cursor");
324
325
326 let params_page2 = [
327 ("repo", AUTH_DID),
328 ("collection", collection),
329 ("limit", "2"),
330 ("cursor", cursor),
331 ];
332
333 let page2_res = client.get(format!("{}/xrpc/com.atproto.repo.listRecords", BASE_URL))
334 .query(¶ms_page2)
335 .send()
336 .await
337 .expect("Failed to send listRecords (page 2)");
338
339 assert_eq!(page2_res.status(), StatusCode::OK, "listRecords (page 2) failed");
340 let page2_body: Value = page2_res.json().await.expect("listRecords (page 2) was not JSON");
341
342 let page2_records = page2_body["records"].as_array().expect("records was not an array");
343 assert_eq!(page2_records.len(), 1, "Page 2 did not return 1 record");
344
345 assert!(page2_body["cursor"].is_null() || page2_body["cursor"].as_str().is_none(), "Page 2 should not have a cursor");
346
347
348 for rkey in created_rkeys {
349 let delete_payload = json!({
350 "repo": AUTH_DID,
351 "collection": collection,
352 "rkey": rkey
353 });
354 client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
355 .bearer_auth(AUTH_TOKEN)
356 .json(&delete_payload)
357 .send()
358 .await
359 .expect("Failed to cleanup pagination post");
360 }
361}
362
363#[tokio::test]
364async fn test_reply_thread_lifecycle() {
365 let client = client();
366
367 let (root_uri, root_cid, root_rkey) = create_test_post(
368 &client,
369 "This is the root of the thread",
370 None
371 ).await;
372
373
374 let reply_ref = json!({
375 "root": { "uri": root_uri.clone(), "cid": root_cid.clone() },
376 "parent": { "uri": root_uri.clone(), "cid": root_cid.clone() }
377 });
378
379 let (reply_uri, _reply_cid, reply_rkey) = create_test_post(
380 &client,
381 "This is a reply!",
382 Some(reply_ref)
383 ).await;
384
385
386 let params = [
387 ("uri", &root_uri),
388 ];
389 let res = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL))
390 .query(¶ms)
391 .bearer_auth(AUTH_TOKEN)
392 .send()
393 .await
394 .expect("Failed to send getPostThread");
395
396 assert_eq!(res.status(), StatusCode::OK, "getPostThread did not return 200");
397 let body: Value = res.json().await.expect("getPostThread response was not JSON");
398
399 assert_eq!(body["thread"]["$type"], "app.bsky.feed.defs#threadViewPost");
400 assert_eq!(body["thread"]["post"]["uri"], root_uri);
401
402 let replies = body["thread"]["replies"].as_array().expect("replies was not an array");
403 assert!(!replies.is_empty(), "Replies array is empty, but should contain the reply");
404
405 let found_reply = replies.iter().find(|r| {
406 r["post"]["uri"] == reply_uri
407 });
408
409 assert!(found_reply.is_some(), "Our specific reply was not found in the thread's replies");
410
411
412 let collection = "app.bsky.feed.post";
413 client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
414 .bearer_auth(AUTH_TOKEN)
415 .json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": reply_rkey }))
416 .send().await.expect("Failed to delete reply");
417
418 client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
419 .bearer_auth(AUTH_TOKEN)
420 .json(&json!({ "repo": AUTH_DID, "collection": collection, "rkey": root_rkey }))
421 .send().await.expect("Failed to delete root post");
422}
423
424#[tokio::test]
425async fn test_account_journey_lifecycle() {
426 let client = client();
427
428 let ts = Utc::now().timestamp_millis();
429 let handle = format!("e2e-user-{}.test", ts);
430 let email = format!("e2e-user-{}@test.com", ts);
431 let password = "e2e-password-123";
432
433 let create_account_payload = json!({
434 "handle": handle,
435 "email": email,
436 "password": password
437 });
438
439 let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL))
440 .json(&create_account_payload)
441 .send()
442 .await
443 .expect("Failed to send createAccount");
444
445 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create account");
446 let create_body: Value = create_res.json().await.expect("createAccount response was not JSON");
447
448 let new_did = create_body["did"].as_str().expect("Response had no DID").to_string();
449 let _new_jwt = create_body["accessJwt"].as_str().expect("Response had no accessJwt").to_string();
450 assert_eq!(create_body["handle"], handle);
451
452
453 let session_payload = json!({
454 "identifier": handle,
455 "password": password
456 });
457
458 let session_res = client.post(format!("{}/xrpc/com.atproto.server.createSession", BASE_URL))
459 .json(&session_payload)
460 .send()
461 .await
462 .expect("Failed to send createSession");
463
464 assert_eq!(session_res.status(), StatusCode::OK, "Failed to create session");
465 let session_body: Value = session_res.json().await.expect("createSession response was not JSON");
466
467 let session_jwt = session_body["accessJwt"].as_str().expect("Session response had no accessJwt").to_string();
468 assert_eq!(session_body["did"], new_did);
469
470
471 let profile_payload = json!({
472 "repo": new_did,
473 "collection": "app.bsky.actor.profile",
474 "rkey": "self", // The rkey for a profile is always "self"
475 "record": {
476 "$type": "app.bsky.actor.profile",
477 "displayName": "E2E Test User",
478 "description": "A user created by the e2e test suite."
479 }
480 });
481
482 let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
483 .bearer_auth(&session_jwt)
484 .json(&profile_payload)
485 .send()
486 .await
487 .expect("Failed to send putRecord for profile");
488
489 assert_eq!(profile_res.status(), StatusCode::OK, "Failed to create profile");
490
491
492 let params_get_profile = [
493 ("actor", &handle),
494 ];
495 let get_profile_res = client.get(format!("{}/xrpc/app.bsky.actor.getProfile", BASE_URL))
496 .query(¶ms_get_profile)
497 .send()
498 .await
499 .expect("Failed to send getProfile");
500
501 assert_eq!(get_profile_res.status(), StatusCode::OK, "getProfile did not return 200");
502 let profile_body: Value = get_profile_res.json().await.expect("getProfile response was not JSON");
503
504 assert_eq!(profile_body["did"], new_did);
505 assert_eq!(profile_body["handle"], handle);
506 assert_eq!(profile_body["displayName"], "E2E Test User");
507
508
509 let logout_res = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", BASE_URL))
510 .bearer_auth(&session_jwt)
511 .send()
512 .await
513 .expect("Failed to send deleteSession");
514
515 assert_eq!(logout_res.status(), StatusCode::OK, "Failed to delete session");
516
517
518 let get_session_res = client.get(format!("{}/xrpc/com.atproto.server.getSession", BASE_URL))
519 .bearer_auth(&session_jwt)
520 .send()
521 .await
522 .expect("Failed to send getSession");
523
524 assert_eq!(get_session_res.status(), StatusCode::UNAUTHORIZED, "Session was still valid after logout");
525}
526
527async fn setup_new_user(handle_prefix: &str) -> (String, String) {
528 let client = client();
529 let ts = Utc::now().timestamp_millis();
530 let handle = format!("{}-{}.test", handle_prefix, ts);
531 let email = format!("{}-{}@test.com", handle_prefix, ts);
532 let password = "e2e-password-123";
533
534 let create_account_payload = json!({
535 "handle": handle,
536 "email": email,
537 "password": password
538 });
539 let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", BASE_URL))
540 .json(&create_account_payload)
541 .send()
542 .await
543 .expect("setup_new_user: Failed to send createAccount");
544 assert_eq!(create_res.status(), StatusCode::OK, "setup_new_user: Failed to create account");
545 let create_body: Value = create_res.json().await.expect("setup_new_user: createAccount response was not JSON");
546
547 let new_did = create_body["did"].as_str().expect("setup_new_user: Response had no DID").to_string();
548 let new_jwt = create_body["accessJwt"].as_str().expect("setup_new_user: Response had no accessJwt").to_string();
549
550 let profile_payload = json!({
551 "repo": new_did.clone(),
552 "collection": "app.bsky.actor.profile",
553 "rkey": "self",
554 "record": {
555 "$type": "app.bsky.actor.profile",
556 "displayName": format!("E2E User {}", handle),
557 "description": "A user created by the e2e test suite."
558 }
559 });
560 let profile_res = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
561 .bearer_auth(&new_jwt)
562 .json(&profile_payload)
563 .send()
564 .await
565 .expect("setup_new_user: Failed to send putRecord for profile");
566 assert_eq!(profile_res.status(), StatusCode::OK, "setup_new_user: Failed to create profile");
567
568 (new_did, new_jwt)
569}
570
571async fn create_record_as(
572 client: &Client,
573 jwt: &str,
574 did: &str,
575 collection: &str,
576 record: Value,
577) -> (String, String) {
578 let payload = json!({
579 "repo": did,
580 "collection": collection,
581 "record": record
582 });
583
584 let res = client.post(format!("{}/xrpc/com.atproto.repo.createRecord", BASE_URL))
585 .bearer_auth(jwt)
586 .json(&payload)
587 .send()
588 .await
589 .expect("create_record_as: Failed to send createRecord");
590
591 assert_eq!(res.status(), StatusCode::OK, "create_record_as: Failed to create record");
592 let body: Value = res.json().await.expect("create_record_as: response was not JSON");
593
594 let uri = body["uri"].as_str().expect("create_record_as: Response had no URI").to_string();
595 let cid = body["cid"].as_str().expect("create_record_as: Response had no CID").to_string();
596 (uri, cid)
597}
598
599async fn delete_record_as(
600 client: &Client,
601 jwt: &str,
602 did: &str,
603 collection: &str,
604 rkey: &str,
605) {
606 let payload = json!({
607 "repo": did,
608 "collection": collection,
609 "rkey": rkey
610 });
611
612 let res = client.post(format!("{}/xrpc/com.atproto.repo.deleteRecord", BASE_URL))
613 .bearer_auth(jwt)
614 .json(&payload)
615 .send()
616 .await
617 .expect("delete_record_as: Failed to send deleteRecord");
618
619 assert_eq!(res.status(), StatusCode::OK, "delete_record_as: Failed to delete record");
620}
621
622
623#[tokio::test]
624async fn test_notification_lifecycle() {
625 let client = client();
626
627 let (user_a_did, user_a_jwt) = setup_new_user("user-a-notif").await;
628 let (user_b_did, user_b_jwt) = setup_new_user("user-b-notif").await;
629
630 let (post_uri, post_cid) = create_record_as(
631 &client,
632 &user_a_jwt,
633 &user_a_did,
634 "app.bsky.feed.post",
635 json!({
636 "$type": "app.bsky.feed.post",
637 "text": "A post to be notified about",
638 "createdAt": Utc::now().to_rfc3339()
639 }),
640 ).await;
641 let post_ref = json!({ "uri": post_uri, "cid": post_cid });
642
643 let count_res_1 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL))
644 .bearer_auth(&user_a_jwt)
645 .send().await.expect("getUnreadCount 1 failed");
646 let count_body_1: Value = count_res_1.json().await.expect("count 1 not json");
647 assert_eq!(count_body_1["count"], 0, "Initial unread count was not 0");
648
649 create_record_as(
650 &client, &user_b_jwt, &user_b_did,
651 "app.bsky.graph.follow",
652 json!({
653 "$type": "app.bsky.graph.follow",
654 "subject": user_a_did,
655 "createdAt": Utc::now().to_rfc3339()
656 }),
657 ).await;
658 create_record_as(
659 &client, &user_b_jwt, &user_b_did,
660 "app.bsky.feed.like",
661 json!({
662 "$type": "app.bsky.feed.like",
663 "subject": post_ref,
664 "createdAt": Utc::now().to_rfc3339()
665 }),
666 ).await;
667 create_record_as(
668 &client, &user_b_jwt, &user_b_did,
669 "app.bsky.feed.post",
670 json!({
671 "$type": "app.bsky.feed.post",
672 "text": "This is a reply!",
673 "reply": { "root": post_ref.clone(), "parent": post_ref.clone() },
674 "createdAt": Utc::now().to_rfc3339()
675 }),
676 ).await;
677
678 tokio::time::sleep(Duration::from_millis(500)).await;
679
680 let count_res_2 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL))
681 .bearer_auth(&user_a_jwt)
682 .send().await.expect("getUnreadCount 2 failed");
683 let count_body_2: Value = count_res_2.json().await.expect("count 2 not json");
684 assert_eq!(count_body_2["count"], 3, "Unread count was not 3 after actions");
685
686 let list_res = client.get(format!("{}/xrpc/app.bsky.notification.listNotifications", BASE_URL))
687 .bearer_auth(&user_a_jwt)
688 .send().await.expect("listNotifications failed");
689 let list_body: Value = list_res.json().await.expect("list not json");
690
691 let notifs = list_body["notifications"].as_array().expect("notifications not array");
692 assert_eq!(notifs.len(), 3, "Notification list did not have 3 items");
693
694 let has_follow = notifs.iter().any(|n| n["reason"] == "follow" && n["author"]["did"] == user_b_did);
695 let has_like = notifs.iter().any(|n| n["reason"] == "like" && n["author"]["did"] == user_b_did);
696 let has_reply = notifs.iter().any(|n| n["reason"] == "reply" && n["author"]["did"] == user_b_did);
697
698 assert!(has_follow, "Notification list missing 'follow'");
699 assert!(has_like, "Notification list missing 'like'");
700 assert!(has_reply, "Notification list missing 'reply'");
701
702 let count_res_3 = client.get(format!("{}/xrpc/app.bsky.notification.getUnreadCount", BASE_URL))
703 .bearer_auth(&user_a_jwt)
704 .send().await.expect("getUnreadCount 3 failed");
705 let count_body_3: Value = count_res_3.json().await.expect("count 3 not json");
706 assert_eq!(count_body_3["count"], 0, "Unread count was not 0 after list");
707}
708
709
710#[tokio::test]
711async fn test_mute_lifecycle_filters_feed() {
712 let client = client();
713
714 let (user_a_did, user_a_jwt) = setup_new_user("user-a-mute").await;
715 let (user_b_did, user_b_jwt) = setup_new_user("user-b-mute").await;
716
717 let (post_uri, _) = create_record_as(
718 &client,
719 &user_b_jwt,
720 &user_b_did,
721 "app.bsky.feed.post",
722 json!({
723 "$type": "app.bsky.feed.post",
724 "text": "A post from User B",
725 "createdAt": Utc::now().to_rfc3339()
726 }),
727 ).await;
728
729 let feed_params_1 = [("actor", &user_b_did)];
730 let feed_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL))
731 .query(&feed_params_1)
732 .bearer_auth(&user_a_jwt)
733 .send().await.expect("getAuthorFeed 1 failed");
734 let feed_body_1: Value = feed_res_1.json().await.expect("feed 1 not json");
735
736 let feed_1 = feed_body_1["feed"].as_array().expect("feed 1 not array");
737 let found_post_1 = feed_1.iter().any(|p| p["post"]["uri"] == post_uri);
738 assert!(found_post_1, "User B's post was not in their feed before mute");
739
740 let (mute_uri, _) = create_record_as(
741 &client, &user_a_jwt, &user_a_did,
742 "app.bsky.graph.mute",
743 json!({
744 "$type": "app.bsky.graph.mute",
745 "subject": user_b_did,
746 "createdAt": Utc::now().to_rfc3339()
747 }),
748 ).await;
749 let mute_rkey = mute_uri.split('/').last().unwrap();
750
751 let feed_params_2 = [("actor", &user_b_did)];
752 let feed_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL))
753 .query(&feed_params_2)
754 .bearer_auth(&user_a_jwt)
755 .send().await.expect("getAuthorFeed 2 failed");
756 let feed_body_2: Value = feed_res_2.json().await.expect("feed 2 not json");
757
758 let feed_2 = feed_body_2["feed"].as_array().expect("feed 2 not array");
759 assert!(feed_2.is_empty(), "User B's feed was not empty after mute");
760
761 delete_record_as(
762 &client, &user_a_jwt, &user_a_did,
763 "app.bsky.graph.mute",
764 mute_rkey,
765 ).await;
766
767 let feed_params_3 = [("actor", &user_b_did)];
768 let feed_res_3 = client.get(format!("{}/xrpc/app.bsky.feed.getAuthorFeed", BASE_URL))
769 .query(&feed_params_3)
770 .bearer_auth(&user_a_jwt)
771 .send().await.expect("getAuthorFeed 3 failed");
772 let feed_body_3: Value = feed_res_3.json().await.expect("feed 3 not json");
773
774 let feed_3 = feed_body_3["feed"].as_array().expect("feed 3 not array");
775 let found_post_3 = feed_3.iter().any(|p| p["post"]["uri"] == post_uri);
776 assert!(found_post_3, "User B's post did not reappear after unmute");
777}
778
779
780#[tokio::test]
781async fn test_record_update_conflict_lifecycle() {
782 let client = client();
783
784 let (user_did, user_jwt) = setup_new_user("user-conflict").await;
785
786 let get_res = client.get(format!("{}/xrpc/com.atproto.repo.getRecord", BASE_URL))
787 .query(&[
788 ("repo", &user_did),
789 ("collection", &"app.bsky.actor.profile".to_string()),
790 ("rkey", &"self".to_string()),
791 ])
792 .send().await.expect("getRecord failed");
793 let get_body: Value = get_res.json().await.expect("getRecord not json");
794 let cid_v1 = get_body["cid"].as_str().expect("Profile v1 had no CID").to_string();
795
796 let update_payload_v2 = json!({
797 "repo": user_did,
798 "collection": "app.bsky.actor.profile",
799 "rkey": "self",
800 "record": {
801 "$type": "app.bsky.actor.profile",
802 "displayName": "Updated Name (v2)"
803 },
804 "swapCommit": cid_v1 // <-- Correctly point to v1
805 });
806 let update_res_v2 = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
807 .bearer_auth(&user_jwt)
808 .json(&update_payload_v2)
809 .send().await.expect("putRecord v2 failed");
810 assert_eq!(update_res_v2.status(), StatusCode::OK, "v2 update failed");
811 let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json");
812 let cid_v2 = update_body_v2["cid"].as_str().expect("v2 response had no CID").to_string();
813
814 let update_payload_v3_stale = json!({
815 "repo": user_did,
816 "collection": "app.bsky.actor.profile",
817 "rkey": "self",
818 "record": {
819 "$type": "app.bsky.actor.profile",
820 "displayName": "Stale Update (v3)"
821 },
822 "swapCommit": cid_v1
823 });
824 let update_res_v3_stale = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
825 .bearer_auth(&user_jwt)
826 .json(&update_payload_v3_stale)
827 .send().await.expect("putRecord v3 (stale) failed");
828
829 assert_eq!(
830 update_res_v3_stale.status(),
831 StatusCode::CONFLICT,
832 "Stale update did not cause a 409 Conflict"
833 );
834
835 let update_payload_v3_good = json!({
836 "repo": user_did,
837 "collection": "app.bsky.actor.profile",
838 "rkey": "self",
839 "record": {
840 "$type": "app.bsky.actor.profile",
841 "displayName": "Good Update (v3)"
842 },
843 "swapCommit": cid_v2 // <-- Correct
844 });
845 let update_res_v3_good = client.post(format!("{}/xrpc/com.atproto.repo.putRecord", BASE_URL))
846 .bearer_auth(&user_jwt)
847 .json(&update_payload_v3_good)
848 .send().await.expect("putRecord v3 (good) failed");
849
850 assert_eq!(update_res_v3_good.status(), StatusCode::OK, "v3 (good) update failed");
851}
852
853
854#[tokio::test]
855async fn test_complex_thread_deletion_lifecycle() {
856 let client = client();
857
858 let (user_a_did, user_a_jwt) = setup_new_user("user-a-thread").await;
859 let (user_b_did, user_b_jwt) = setup_new_user("user-b-thread").await;
860 let (user_c_did, user_c_jwt) = setup_new_user("user-c-thread").await;
861
862 let (p1_uri, p1_cid) = create_record_as(
863 &client, &user_a_jwt, &user_a_did,
864 "app.bsky.feed.post",
865 json!({
866 "$type": "app.bsky.feed.post",
867 "text": "P1 (Root)",
868 "createdAt": Utc::now().to_rfc3339()
869 }),
870 ).await;
871 let p1_ref = json!({ "uri": p1_uri.clone(), "cid": p1_cid.clone() });
872
873 let (p2_uri, p2_cid) = create_record_as(
874 &client, &user_b_jwt, &user_b_did,
875 "app.bsky.feed.post",
876 json!({
877 "$type": "app.bsky.feed.post",
878 "text": "P2 (Reply)",
879 "reply": { "root": p1_ref.clone(), "parent": p1_ref.clone() },
880 "createdAt": Utc::now().to_rfc3339()
881 }),
882 ).await;
883 let p2_ref = json!({ "uri": p2_uri.clone(), "cid": p2_cid.clone() });
884 let p2_rkey = p2_uri.split('/').last().unwrap().to_string();
885
886 let (p3_uri, _) = create_record_as(
887 &client, &user_c_jwt, &user_c_did,
888 "app.bsky.feed.post",
889 json!({
890 "$type": "app.bsky.feed.post",
891 "text": "P3 (Grandchild)",
892 "reply": { "root": p1_ref.clone(), "parent": p2_ref.clone() },
893 "createdAt": Utc::now().to_rfc3339()
894 }),
895 ).await;
896
897 let thread_res_1 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL))
898 .query(&[("uri", &p1_uri)])
899 .bearer_auth(&user_a_jwt)
900 .send().await.expect("getThread 1 failed");
901 let thread_body_1: Value = thread_res_1.json().await.expect("thread 1 not json");
902
903 let p1_replies = thread_body_1["thread"]["replies"].as_array().unwrap();
904 assert_eq!(p1_replies.len(), 1, "P1 should have 1 reply");
905 assert_eq!(p1_replies[0]["post"]["uri"], p2_uri, "P1's reply is not P2");
906
907 let p2_replies = p1_replies[0]["replies"].as_array().unwrap();
908 assert_eq!(p2_replies.len(), 1, "P2 should have 1 reply");
909 assert_eq!(p2_replies[0]["post"]["uri"], p3_uri, "P2's reply is not P3");
910
911 delete_record_as(
912 &client, &user_b_jwt, &user_b_did,
913 "app.bsky.feed.post",
914 &p2_rkey,
915 ).await;
916
917 let thread_res_2 = client.get(format!("{}/xrpc/app.bsky.feed.getPostThread", BASE_URL))
918 .query(&[("uri", &p1_uri)])
919 .bearer_auth(&user_a_jwt)
920 .send().await.expect("getThread 2 failed");
921 let thread_body_2: Value = thread_res_2.json().await.expect("thread 2 not json");
922
923 let p1_replies_2 = thread_body_2["thread"]["replies"].as_array().unwrap();
924 assert_eq!(p1_replies_2.len(), 1, "P1 should still have 1 reply (the deleted one)");
925
926 let deleted_post = &p1_replies_2[0];
927 assert_eq!(
928 deleted_post["$type"], "app.bsky.feed.defs#notFoundPost",
929 "P2 did not appear as a notFoundPost"
930 );
931 assert_eq!(deleted_post["uri"], p2_uri, "notFoundPost URI does not match P2");
932
933 let p3_reply = deleted_post["replies"].as_array().unwrap();
934 assert_eq!(p3_reply.len(), 1, "notFoundPost should still have P3 as a reply");
935 assert_eq!(p3_reply[0]["post"]["uri"], p3_uri, "The reply to the deleted post is not P3");
936}