this repo has no description
1mod common;
2mod helpers;
3use common::*;
4use helpers::*;
5use chrono::Utc;
6use reqwest::{StatusCode, header};
7use serde_json::{Value, json};
8use std::time::Duration;
9#[tokio::test]
10async fn test_post_crud_lifecycle() {
11 let client = client();
12 let (did, jwt) = setup_new_user("lifecycle-crud").await;
13 let collection = "app.bsky.feed.post";
14 let rkey = format!("e2e_lifecycle_{}", Utc::now().timestamp_millis());
15 let now = Utc::now().to_rfc3339();
16 let original_text = "Hello from the lifecycle test!";
17 let create_payload = json!({
18 "repo": did,
19 "collection": collection,
20 "rkey": rkey,
21 "record": {
22 "$type": collection,
23 "text": original_text,
24 "createdAt": now
25 }
26 });
27 let create_res = client
28 .post(format!(
29 "{}/xrpc/com.atproto.repo.putRecord",
30 base_url().await
31 ))
32 .bearer_auth(&jwt)
33 .json(&create_payload)
34 .send()
35 .await
36 .expect("Failed to send create request");
37 if create_res.status() != reqwest::StatusCode::OK {
38 let status = create_res.status();
39 let body = create_res
40 .text()
41 .await
42 .unwrap_or_else(|_| "Could not get body".to_string());
43 panic!(
44 "Failed to create record. Status: {}, Body: {}",
45 status, body
46 );
47 }
48 let create_body: Value = create_res
49 .json()
50 .await
51 .expect("create response was not JSON");
52 let uri = create_body["uri"].as_str().unwrap();
53 let params = [
54 ("repo", did.as_str()),
55 ("collection", collection),
56 ("rkey", &rkey),
57 ];
58 let get_res = client
59 .get(format!(
60 "{}/xrpc/com.atproto.repo.getRecord",
61 base_url().await
62 ))
63 .query(¶ms)
64 .send()
65 .await
66 .expect("Failed to send get request");
67 assert_eq!(
68 get_res.status(),
69 reqwest::StatusCode::OK,
70 "Failed to get record after create"
71 );
72 let get_body: Value = get_res.json().await.expect("get response was not JSON");
73 assert_eq!(get_body["uri"], uri);
74 assert_eq!(get_body["value"]["text"], original_text);
75 let updated_text = "This post has been updated.";
76 let update_payload = json!({
77 "repo": did,
78 "collection": collection,
79 "rkey": rkey,
80 "record": {
81 "$type": collection,
82 "text": updated_text,
83 "createdAt": now
84 }
85 });
86 let update_res = client
87 .post(format!(
88 "{}/xrpc/com.atproto.repo.putRecord",
89 base_url().await
90 ))
91 .bearer_auth(&jwt)
92 .json(&update_payload)
93 .send()
94 .await
95 .expect("Failed to send update request");
96 assert_eq!(
97 update_res.status(),
98 reqwest::StatusCode::OK,
99 "Failed to update record"
100 );
101 let get_updated_res = client
102 .get(format!(
103 "{}/xrpc/com.atproto.repo.getRecord",
104 base_url().await
105 ))
106 .query(¶ms)
107 .send()
108 .await
109 .expect("Failed to send get-after-update request");
110 assert_eq!(
111 get_updated_res.status(),
112 reqwest::StatusCode::OK,
113 "Failed to get record after update"
114 );
115 let get_updated_body: Value = get_updated_res
116 .json()
117 .await
118 .expect("get-updated response was not JSON");
119 assert_eq!(
120 get_updated_body["value"]["text"], updated_text,
121 "Text was not updated"
122 );
123 let delete_payload = json!({
124 "repo": did,
125 "collection": collection,
126 "rkey": rkey
127 });
128 let delete_res = client
129 .post(format!(
130 "{}/xrpc/com.atproto.repo.deleteRecord",
131 base_url().await
132 ))
133 .bearer_auth(&jwt)
134 .json(&delete_payload)
135 .send()
136 .await
137 .expect("Failed to send delete request");
138 assert_eq!(
139 delete_res.status(),
140 reqwest::StatusCode::OK,
141 "Failed to delete record"
142 );
143 let get_deleted_res = client
144 .get(format!(
145 "{}/xrpc/com.atproto.repo.getRecord",
146 base_url().await
147 ))
148 .query(¶ms)
149 .send()
150 .await
151 .expect("Failed to send get-after-delete request");
152 assert_eq!(
153 get_deleted_res.status(),
154 reqwest::StatusCode::NOT_FOUND,
155 "Record was found, but it should be deleted"
156 );
157}
158#[tokio::test]
159async fn test_record_update_conflict_lifecycle() {
160 let client = client();
161 let (user_did, user_jwt) = setup_new_user("user-conflict").await;
162 let profile_payload = json!({
163 "repo": user_did,
164 "collection": "app.bsky.actor.profile",
165 "rkey": "self",
166 "record": {
167 "$type": "app.bsky.actor.profile",
168 "displayName": "Original Name"
169 }
170 });
171 let create_res = client
172 .post(format!(
173 "{}/xrpc/com.atproto.repo.putRecord",
174 base_url().await
175 ))
176 .bearer_auth(&user_jwt)
177 .json(&profile_payload)
178 .send()
179 .await
180 .expect("create profile failed");
181 if create_res.status() != reqwest::StatusCode::OK {
182 return;
183 }
184 let get_res = client
185 .get(format!(
186 "{}/xrpc/com.atproto.repo.getRecord",
187 base_url().await
188 ))
189 .query(&[
190 ("repo", &user_did),
191 ("collection", &"app.bsky.actor.profile".to_string()),
192 ("rkey", &"self".to_string()),
193 ])
194 .send()
195 .await
196 .expect("getRecord failed");
197 let get_body: Value = get_res.json().await.expect("getRecord not json");
198 let cid_v1 = get_body["cid"]
199 .as_str()
200 .expect("Profile v1 had no CID")
201 .to_string();
202 let update_payload_v2 = json!({
203 "repo": user_did,
204 "collection": "app.bsky.actor.profile",
205 "rkey": "self",
206 "record": {
207 "$type": "app.bsky.actor.profile",
208 "displayName": "Updated Name (v2)"
209 },
210 "swapRecord": cid_v1
211 });
212 let update_res_v2 = client
213 .post(format!(
214 "{}/xrpc/com.atproto.repo.putRecord",
215 base_url().await
216 ))
217 .bearer_auth(&user_jwt)
218 .json(&update_payload_v2)
219 .send()
220 .await
221 .expect("putRecord v2 failed");
222 assert_eq!(
223 update_res_v2.status(),
224 reqwest::StatusCode::OK,
225 "v2 update failed"
226 );
227 let update_body_v2: Value = update_res_v2.json().await.expect("v2 body not json");
228 let cid_v2 = update_body_v2["cid"]
229 .as_str()
230 .expect("v2 response had no CID")
231 .to_string();
232 let update_payload_v3_stale = json!({
233 "repo": user_did,
234 "collection": "app.bsky.actor.profile",
235 "rkey": "self",
236 "record": {
237 "$type": "app.bsky.actor.profile",
238 "displayName": "Stale Update (v3)"
239 },
240 "swapRecord": cid_v1
241 });
242 let update_res_v3_stale = client
243 .post(format!(
244 "{}/xrpc/com.atproto.repo.putRecord",
245 base_url().await
246 ))
247 .bearer_auth(&user_jwt)
248 .json(&update_payload_v3_stale)
249 .send()
250 .await
251 .expect("putRecord v3 (stale) failed");
252 assert_eq!(
253 update_res_v3_stale.status(),
254 reqwest::StatusCode::CONFLICT,
255 "Stale update did not cause a 409 Conflict"
256 );
257 let update_payload_v3_good = json!({
258 "repo": user_did,
259 "collection": "app.bsky.actor.profile",
260 "rkey": "self",
261 "record": {
262 "$type": "app.bsky.actor.profile",
263 "displayName": "Good Update (v3)"
264 },
265 "swapRecord": cid_v2
266 });
267 let update_res_v3_good = client
268 .post(format!(
269 "{}/xrpc/com.atproto.repo.putRecord",
270 base_url().await
271 ))
272 .bearer_auth(&user_jwt)
273 .json(&update_payload_v3_good)
274 .send()
275 .await
276 .expect("putRecord v3 (good) failed");
277 assert_eq!(
278 update_res_v3_good.status(),
279 reqwest::StatusCode::OK,
280 "v3 (good) update failed"
281 );
282}
283#[tokio::test]
284async fn test_profile_lifecycle() {
285 let client = client();
286 let (did, jwt) = setup_new_user("profile-lifecycle").await;
287 let profile_payload = json!({
288 "repo": did,
289 "collection": "app.bsky.actor.profile",
290 "rkey": "self",
291 "record": {
292 "$type": "app.bsky.actor.profile",
293 "displayName": "Test User",
294 "description": "A test profile for lifecycle testing"
295 }
296 });
297 let create_res = client
298 .post(format!(
299 "{}/xrpc/com.atproto.repo.putRecord",
300 base_url().await
301 ))
302 .bearer_auth(&jwt)
303 .json(&profile_payload)
304 .send()
305 .await
306 .expect("Failed to create profile");
307 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile");
308 let create_body: Value = create_res.json().await.unwrap();
309 let initial_cid = create_body["cid"].as_str().unwrap().to_string();
310 let get_res = client
311 .get(format!(
312 "{}/xrpc/com.atproto.repo.getRecord",
313 base_url().await
314 ))
315 .query(&[
316 ("repo", did.as_str()),
317 ("collection", "app.bsky.actor.profile"),
318 ("rkey", "self"),
319 ])
320 .send()
321 .await
322 .expect("Failed to get profile");
323 assert_eq!(get_res.status(), StatusCode::OK);
324 let get_body: Value = get_res.json().await.unwrap();
325 assert_eq!(get_body["value"]["displayName"], "Test User");
326 assert_eq!(get_body["value"]["description"], "A test profile for lifecycle testing");
327 let update_payload = json!({
328 "repo": did,
329 "collection": "app.bsky.actor.profile",
330 "rkey": "self",
331 "record": {
332 "$type": "app.bsky.actor.profile",
333 "displayName": "Updated User",
334 "description": "Profile has been updated"
335 },
336 "swapRecord": initial_cid
337 });
338 let update_res = client
339 .post(format!(
340 "{}/xrpc/com.atproto.repo.putRecord",
341 base_url().await
342 ))
343 .bearer_auth(&jwt)
344 .json(&update_payload)
345 .send()
346 .await
347 .expect("Failed to update profile");
348 assert_eq!(update_res.status(), StatusCode::OK, "Failed to update profile");
349 let get_updated_res = client
350 .get(format!(
351 "{}/xrpc/com.atproto.repo.getRecord",
352 base_url().await
353 ))
354 .query(&[
355 ("repo", did.as_str()),
356 ("collection", "app.bsky.actor.profile"),
357 ("rkey", "self"),
358 ])
359 .send()
360 .await
361 .expect("Failed to get updated profile");
362 let updated_body: Value = get_updated_res.json().await.unwrap();
363 assert_eq!(updated_body["value"]["displayName"], "Updated User");
364}
365#[tokio::test]
366async fn test_reply_thread_lifecycle() {
367 let client = client();
368 let (alice_did, alice_jwt) = setup_new_user("alice-thread").await;
369 let (bob_did, bob_jwt) = setup_new_user("bob-thread").await;
370 let (root_uri, root_cid) = create_post(&client, &alice_did, &alice_jwt, "This is the root post").await;
371 tokio::time::sleep(Duration::from_millis(100)).await;
372 let reply_collection = "app.bsky.feed.post";
373 let reply_rkey = format!("e2e_reply_{}", Utc::now().timestamp_millis());
374 let now = Utc::now().to_rfc3339();
375 let reply_payload = json!({
376 "repo": bob_did,
377 "collection": reply_collection,
378 "rkey": reply_rkey,
379 "record": {
380 "$type": reply_collection,
381 "text": "This is Bob's reply to Alice",
382 "createdAt": now,
383 "reply": {
384 "root": {
385 "uri": root_uri,
386 "cid": root_cid
387 },
388 "parent": {
389 "uri": root_uri,
390 "cid": root_cid
391 }
392 }
393 }
394 });
395 let reply_res = client
396 .post(format!(
397 "{}/xrpc/com.atproto.repo.putRecord",
398 base_url().await
399 ))
400 .bearer_auth(&bob_jwt)
401 .json(&reply_payload)
402 .send()
403 .await
404 .expect("Failed to create reply");
405 assert_eq!(reply_res.status(), StatusCode::OK, "Failed to create reply");
406 let reply_body: Value = reply_res.json().await.unwrap();
407 let reply_uri = reply_body["uri"].as_str().unwrap();
408 let reply_cid = reply_body["cid"].as_str().unwrap();
409 let get_reply_res = client
410 .get(format!(
411 "{}/xrpc/com.atproto.repo.getRecord",
412 base_url().await
413 ))
414 .query(&[
415 ("repo", bob_did.as_str()),
416 ("collection", reply_collection),
417 ("rkey", reply_rkey.as_str()),
418 ])
419 .send()
420 .await
421 .expect("Failed to get reply");
422 assert_eq!(get_reply_res.status(), StatusCode::OK);
423 let reply_record: Value = get_reply_res.json().await.unwrap();
424 assert_eq!(reply_record["value"]["reply"]["root"]["uri"], root_uri);
425 assert_eq!(reply_record["value"]["reply"]["parent"]["uri"], root_uri);
426 tokio::time::sleep(Duration::from_millis(100)).await;
427 let nested_reply_rkey = format!("e2e_nested_reply_{}", Utc::now().timestamp_millis());
428 let nested_payload = json!({
429 "repo": alice_did,
430 "collection": reply_collection,
431 "rkey": nested_reply_rkey,
432 "record": {
433 "$type": reply_collection,
434 "text": "Alice replies to Bob's reply",
435 "createdAt": Utc::now().to_rfc3339(),
436 "reply": {
437 "root": {
438 "uri": root_uri,
439 "cid": root_cid
440 },
441 "parent": {
442 "uri": reply_uri,
443 "cid": reply_cid
444 }
445 }
446 }
447 });
448 let nested_res = client
449 .post(format!(
450 "{}/xrpc/com.atproto.repo.putRecord",
451 base_url().await
452 ))
453 .bearer_auth(&alice_jwt)
454 .json(&nested_payload)
455 .send()
456 .await
457 .expect("Failed to create nested reply");
458 assert_eq!(nested_res.status(), StatusCode::OK, "Failed to create nested reply");
459}
460#[tokio::test]
461async fn test_blob_in_record_lifecycle() {
462 let client = client();
463 let (did, jwt) = setup_new_user("blob-record").await;
464 let blob_data = b"This is test blob data for a profile avatar";
465 let upload_res = client
466 .post(format!(
467 "{}/xrpc/com.atproto.repo.uploadBlob",
468 base_url().await
469 ))
470 .header(header::CONTENT_TYPE, "text/plain")
471 .bearer_auth(&jwt)
472 .body(blob_data.to_vec())
473 .send()
474 .await
475 .expect("Failed to upload blob");
476 assert_eq!(upload_res.status(), StatusCode::OK);
477 let upload_body: Value = upload_res.json().await.unwrap();
478 let blob_ref = upload_body["blob"].clone();
479 let profile_payload = json!({
480 "repo": did,
481 "collection": "app.bsky.actor.profile",
482 "rkey": "self",
483 "record": {
484 "$type": "app.bsky.actor.profile",
485 "displayName": "User With Avatar",
486 "avatar": blob_ref
487 }
488 });
489 let create_res = client
490 .post(format!(
491 "{}/xrpc/com.atproto.repo.putRecord",
492 base_url().await
493 ))
494 .bearer_auth(&jwt)
495 .json(&profile_payload)
496 .send()
497 .await
498 .expect("Failed to create profile with blob");
499 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile with blob");
500 let get_res = client
501 .get(format!(
502 "{}/xrpc/com.atproto.repo.getRecord",
503 base_url().await
504 ))
505 .query(&[
506 ("repo", did.as_str()),
507 ("collection", "app.bsky.actor.profile"),
508 ("rkey", "self"),
509 ])
510 .send()
511 .await
512 .expect("Failed to get profile");
513 assert_eq!(get_res.status(), StatusCode::OK);
514 let profile: Value = get_res.json().await.unwrap();
515 assert!(profile["value"]["avatar"]["ref"]["$link"].is_string());
516}
517#[tokio::test]
518async fn test_authorization_cannot_modify_other_repo() {
519 let client = client();
520 let (alice_did, _alice_jwt) = setup_new_user("alice-auth").await;
521 let (_bob_did, bob_jwt) = setup_new_user("bob-auth").await;
522 let post_payload = json!({
523 "repo": alice_did,
524 "collection": "app.bsky.feed.post",
525 "rkey": "unauthorized-post",
526 "record": {
527 "$type": "app.bsky.feed.post",
528 "text": "Bob trying to post as Alice",
529 "createdAt": Utc::now().to_rfc3339()
530 }
531 });
532 let res = client
533 .post(format!(
534 "{}/xrpc/com.atproto.repo.putRecord",
535 base_url().await
536 ))
537 .bearer_auth(&bob_jwt)
538 .json(&post_payload)
539 .send()
540 .await
541 .expect("Failed to send request");
542 assert!(
543 res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED,
544 "Expected 403 or 401 when writing to another user's repo, got {}",
545 res.status()
546 );
547}
548#[tokio::test]
549async fn test_authorization_cannot_delete_other_record() {
550 let client = client();
551 let (alice_did, alice_jwt) = setup_new_user("alice-del-auth").await;
552 let (_bob_did, bob_jwt) = setup_new_user("bob-del-auth").await;
553 let (post_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's post").await;
554 let post_rkey = post_uri.split('/').last().unwrap();
555 let delete_payload = json!({
556 "repo": alice_did,
557 "collection": "app.bsky.feed.post",
558 "rkey": post_rkey
559 });
560 let res = client
561 .post(format!(
562 "{}/xrpc/com.atproto.repo.deleteRecord",
563 base_url().await
564 ))
565 .bearer_auth(&bob_jwt)
566 .json(&delete_payload)
567 .send()
568 .await
569 .expect("Failed to send request");
570 assert!(
571 res.status() == StatusCode::FORBIDDEN || res.status() == StatusCode::UNAUTHORIZED,
572 "Expected 403 or 401 when deleting another user's record, got {}",
573 res.status()
574 );
575 let get_res = client
576 .get(format!(
577 "{}/xrpc/com.atproto.repo.getRecord",
578 base_url().await
579 ))
580 .query(&[
581 ("repo", alice_did.as_str()),
582 ("collection", "app.bsky.feed.post"),
583 ("rkey", post_rkey),
584 ])
585 .send()
586 .await
587 .expect("Failed to verify record exists");
588 assert_eq!(get_res.status(), StatusCode::OK, "Record should still exist");
589}
590#[tokio::test]
591async fn test_apply_writes_batch_lifecycle() {
592 let client = client();
593 let (did, jwt) = setup_new_user("apply-writes-batch").await;
594 let now = Utc::now().to_rfc3339();
595 let writes_payload = json!({
596 "repo": did,
597 "writes": [
598 {
599 "$type": "com.atproto.repo.applyWrites#create",
600 "collection": "app.bsky.feed.post",
601 "rkey": "batch-post-1",
602 "value": {
603 "$type": "app.bsky.feed.post",
604 "text": "First batch post",
605 "createdAt": now
606 }
607 },
608 {
609 "$type": "com.atproto.repo.applyWrites#create",
610 "collection": "app.bsky.feed.post",
611 "rkey": "batch-post-2",
612 "value": {
613 "$type": "app.bsky.feed.post",
614 "text": "Second batch post",
615 "createdAt": now
616 }
617 },
618 {
619 "$type": "com.atproto.repo.applyWrites#create",
620 "collection": "app.bsky.actor.profile",
621 "rkey": "self",
622 "value": {
623 "$type": "app.bsky.actor.profile",
624 "displayName": "Batch User"
625 }
626 }
627 ]
628 });
629 let apply_res = client
630 .post(format!(
631 "{}/xrpc/com.atproto.repo.applyWrites",
632 base_url().await
633 ))
634 .bearer_auth(&jwt)
635 .json(&writes_payload)
636 .send()
637 .await
638 .expect("Failed to apply writes");
639 assert_eq!(apply_res.status(), StatusCode::OK);
640 let get_post1 = client
641 .get(format!(
642 "{}/xrpc/com.atproto.repo.getRecord",
643 base_url().await
644 ))
645 .query(&[
646 ("repo", did.as_str()),
647 ("collection", "app.bsky.feed.post"),
648 ("rkey", "batch-post-1"),
649 ])
650 .send()
651 .await
652 .expect("Failed to get post 1");
653 assert_eq!(get_post1.status(), StatusCode::OK);
654 let post1_body: Value = get_post1.json().await.unwrap();
655 assert_eq!(post1_body["value"]["text"], "First batch post");
656 let get_post2 = client
657 .get(format!(
658 "{}/xrpc/com.atproto.repo.getRecord",
659 base_url().await
660 ))
661 .query(&[
662 ("repo", did.as_str()),
663 ("collection", "app.bsky.feed.post"),
664 ("rkey", "batch-post-2"),
665 ])
666 .send()
667 .await
668 .expect("Failed to get post 2");
669 assert_eq!(get_post2.status(), StatusCode::OK);
670 let get_profile = client
671 .get(format!(
672 "{}/xrpc/com.atproto.repo.getRecord",
673 base_url().await
674 ))
675 .query(&[
676 ("repo", did.as_str()),
677 ("collection", "app.bsky.actor.profile"),
678 ("rkey", "self"),
679 ])
680 .send()
681 .await
682 .expect("Failed to get profile");
683 assert_eq!(get_profile.status(), StatusCode::OK);
684 let profile_body: Value = get_profile.json().await.unwrap();
685 assert_eq!(profile_body["value"]["displayName"], "Batch User");
686 let update_writes = json!({
687 "repo": did,
688 "writes": [
689 {
690 "$type": "com.atproto.repo.applyWrites#update",
691 "collection": "app.bsky.actor.profile",
692 "rkey": "self",
693 "value": {
694 "$type": "app.bsky.actor.profile",
695 "displayName": "Updated Batch User"
696 }
697 },
698 {
699 "$type": "com.atproto.repo.applyWrites#delete",
700 "collection": "app.bsky.feed.post",
701 "rkey": "batch-post-1"
702 }
703 ]
704 });
705 let update_res = client
706 .post(format!(
707 "{}/xrpc/com.atproto.repo.applyWrites",
708 base_url().await
709 ))
710 .bearer_auth(&jwt)
711 .json(&update_writes)
712 .send()
713 .await
714 .expect("Failed to apply update writes");
715 assert_eq!(update_res.status(), StatusCode::OK);
716 let get_updated_profile = client
717 .get(format!(
718 "{}/xrpc/com.atproto.repo.getRecord",
719 base_url().await
720 ))
721 .query(&[
722 ("repo", did.as_str()),
723 ("collection", "app.bsky.actor.profile"),
724 ("rkey", "self"),
725 ])
726 .send()
727 .await
728 .expect("Failed to get updated profile");
729 let updated_profile: Value = get_updated_profile.json().await.unwrap();
730 assert_eq!(updated_profile["value"]["displayName"], "Updated Batch User");
731 let get_deleted_post = client
732 .get(format!(
733 "{}/xrpc/com.atproto.repo.getRecord",
734 base_url().await
735 ))
736 .query(&[
737 ("repo", did.as_str()),
738 ("collection", "app.bsky.feed.post"),
739 ("rkey", "batch-post-1"),
740 ])
741 .send()
742 .await
743 .expect("Failed to check deleted post");
744 assert_eq!(
745 get_deleted_post.status(),
746 StatusCode::NOT_FOUND,
747 "Batch-deleted post should be gone"
748 );
749}
750async fn create_post_with_rkey(
751 client: &reqwest::Client,
752 did: &str,
753 jwt: &str,
754 rkey: &str,
755 text: &str,
756) -> (String, String) {
757 let payload = json!({
758 "repo": did,
759 "collection": "app.bsky.feed.post",
760 "rkey": rkey,
761 "record": {
762 "$type": "app.bsky.feed.post",
763 "text": text,
764 "createdAt": Utc::now().to_rfc3339()
765 }
766 });
767 let res = client
768 .post(format!(
769 "{}/xrpc/com.atproto.repo.putRecord",
770 base_url().await
771 ))
772 .bearer_auth(jwt)
773 .json(&payload)
774 .send()
775 .await
776 .expect("Failed to create record");
777 assert_eq!(res.status(), StatusCode::OK);
778 let body: Value = res.json().await.unwrap();
779 (
780 body["uri"].as_str().unwrap().to_string(),
781 body["cid"].as_str().unwrap().to_string(),
782 )
783}
784#[tokio::test]
785async fn test_list_records_default_order() {
786 let client = client();
787 let (did, jwt) = setup_new_user("list-default-order").await;
788 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First post").await;
789 tokio::time::sleep(Duration::from_millis(50)).await;
790 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second post").await;
791 tokio::time::sleep(Duration::from_millis(50)).await;
792 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third post").await;
793 let res = client
794 .get(format!(
795 "{}/xrpc/com.atproto.repo.listRecords",
796 base_url().await
797 ))
798 .query(&[
799 ("repo", did.as_str()),
800 ("collection", "app.bsky.feed.post"),
801 ])
802 .send()
803 .await
804 .expect("Failed to list records");
805 assert_eq!(res.status(), StatusCode::OK);
806 let body: Value = res.json().await.unwrap();
807 let records = body["records"].as_array().unwrap();
808 assert_eq!(records.len(), 3);
809 let rkeys: Vec<&str> = records
810 .iter()
811 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
812 .collect();
813 assert_eq!(rkeys, vec!["cccc", "bbbb", "aaaa"], "Default order should be DESC (newest first)");
814}
815#[tokio::test]
816async fn test_list_records_reverse_true() {
817 let client = client();
818 let (did, jwt) = setup_new_user("list-reverse").await;
819 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First post").await;
820 tokio::time::sleep(Duration::from_millis(50)).await;
821 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second post").await;
822 tokio::time::sleep(Duration::from_millis(50)).await;
823 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third post").await;
824 let res = client
825 .get(format!(
826 "{}/xrpc/com.atproto.repo.listRecords",
827 base_url().await
828 ))
829 .query(&[
830 ("repo", did.as_str()),
831 ("collection", "app.bsky.feed.post"),
832 ("reverse", "true"),
833 ])
834 .send()
835 .await
836 .expect("Failed to list records");
837 assert_eq!(res.status(), StatusCode::OK);
838 let body: Value = res.json().await.unwrap();
839 let records = body["records"].as_array().unwrap();
840 let rkeys: Vec<&str> = records
841 .iter()
842 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
843 .collect();
844 assert_eq!(rkeys, vec!["aaaa", "bbbb", "cccc"], "reverse=true should give ASC order (oldest first)");
845}
846#[tokio::test]
847async fn test_list_records_cursor_pagination() {
848 let client = client();
849 let (did, jwt) = setup_new_user("list-cursor").await;
850 for i in 0..5 {
851 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await;
852 tokio::time::sleep(Duration::from_millis(50)).await;
853 }
854 let res = client
855 .get(format!(
856 "{}/xrpc/com.atproto.repo.listRecords",
857 base_url().await
858 ))
859 .query(&[
860 ("repo", did.as_str()),
861 ("collection", "app.bsky.feed.post"),
862 ("limit", "2"),
863 ])
864 .send()
865 .await
866 .expect("Failed to list records");
867 assert_eq!(res.status(), StatusCode::OK);
868 let body: Value = res.json().await.unwrap();
869 let records = body["records"].as_array().unwrap();
870 assert_eq!(records.len(), 2);
871 let cursor = body["cursor"].as_str().expect("Should have cursor with more records");
872 let res2 = client
873 .get(format!(
874 "{}/xrpc/com.atproto.repo.listRecords",
875 base_url().await
876 ))
877 .query(&[
878 ("repo", did.as_str()),
879 ("collection", "app.bsky.feed.post"),
880 ("limit", "2"),
881 ("cursor", cursor),
882 ])
883 .send()
884 .await
885 .expect("Failed to list records with cursor");
886 assert_eq!(res2.status(), StatusCode::OK);
887 let body2: Value = res2.json().await.unwrap();
888 let records2 = body2["records"].as_array().unwrap();
889 assert_eq!(records2.len(), 2);
890 let all_uris: Vec<&str> = records
891 .iter()
892 .chain(records2.iter())
893 .map(|r| r["uri"].as_str().unwrap())
894 .collect();
895 let unique_uris: std::collections::HashSet<&str> = all_uris.iter().copied().collect();
896 assert_eq!(all_uris.len(), unique_uris.len(), "Cursor pagination should not repeat records");
897}
898#[tokio::test]
899async fn test_list_records_rkey_start() {
900 let client = client();
901 let (did, jwt) = setup_new_user("list-rkey-start").await;
902 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First").await;
903 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second").await;
904 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third").await;
905 create_post_with_rkey(&client, &did, &jwt, "dddd", "Fourth").await;
906 let res = client
907 .get(format!(
908 "{}/xrpc/com.atproto.repo.listRecords",
909 base_url().await
910 ))
911 .query(&[
912 ("repo", did.as_str()),
913 ("collection", "app.bsky.feed.post"),
914 ("rkeyStart", "bbbb"),
915 ("reverse", "true"),
916 ])
917 .send()
918 .await
919 .expect("Failed to list records");
920 assert_eq!(res.status(), StatusCode::OK);
921 let body: Value = res.json().await.unwrap();
922 let records = body["records"].as_array().unwrap();
923 let rkeys: Vec<&str> = records
924 .iter()
925 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
926 .collect();
927 for rkey in &rkeys {
928 assert!(*rkey >= "bbbb", "rkeyStart should filter records >= start");
929 }
930}
931#[tokio::test]
932async fn test_list_records_rkey_end() {
933 let client = client();
934 let (did, jwt) = setup_new_user("list-rkey-end").await;
935 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First").await;
936 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second").await;
937 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third").await;
938 create_post_with_rkey(&client, &did, &jwt, "dddd", "Fourth").await;
939 let res = client
940 .get(format!(
941 "{}/xrpc/com.atproto.repo.listRecords",
942 base_url().await
943 ))
944 .query(&[
945 ("repo", did.as_str()),
946 ("collection", "app.bsky.feed.post"),
947 ("rkeyEnd", "cccc"),
948 ("reverse", "true"),
949 ])
950 .send()
951 .await
952 .expect("Failed to list records");
953 assert_eq!(res.status(), StatusCode::OK);
954 let body: Value = res.json().await.unwrap();
955 let records = body["records"].as_array().unwrap();
956 let rkeys: Vec<&str> = records
957 .iter()
958 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
959 .collect();
960 for rkey in &rkeys {
961 assert!(*rkey <= "cccc", "rkeyEnd should filter records <= end");
962 }
963}
964#[tokio::test]
965async fn test_list_records_rkey_range() {
966 let client = client();
967 let (did, jwt) = setup_new_user("list-rkey-range").await;
968 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First").await;
969 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second").await;
970 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third").await;
971 create_post_with_rkey(&client, &did, &jwt, "dddd", "Fourth").await;
972 create_post_with_rkey(&client, &did, &jwt, "eeee", "Fifth").await;
973 let res = client
974 .get(format!(
975 "{}/xrpc/com.atproto.repo.listRecords",
976 base_url().await
977 ))
978 .query(&[
979 ("repo", did.as_str()),
980 ("collection", "app.bsky.feed.post"),
981 ("rkeyStart", "bbbb"),
982 ("rkeyEnd", "dddd"),
983 ("reverse", "true"),
984 ])
985 .send()
986 .await
987 .expect("Failed to list records");
988 assert_eq!(res.status(), StatusCode::OK);
989 let body: Value = res.json().await.unwrap();
990 let records = body["records"].as_array().unwrap();
991 let rkeys: Vec<&str> = records
992 .iter()
993 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
994 .collect();
995 for rkey in &rkeys {
996 assert!(*rkey >= "bbbb" && *rkey <= "dddd", "Range should be inclusive, got {}", rkey);
997 }
998 assert!(!rkeys.is_empty(), "Should have at least some records in range");
999}
1000#[tokio::test]
1001async fn test_list_records_limit_clamping_max() {
1002 let client = client();
1003 let (did, jwt) = setup_new_user("list-limit-max").await;
1004 for i in 0..5 {
1005 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await;
1006 }
1007 let res = client
1008 .get(format!(
1009 "{}/xrpc/com.atproto.repo.listRecords",
1010 base_url().await
1011 ))
1012 .query(&[
1013 ("repo", did.as_str()),
1014 ("collection", "app.bsky.feed.post"),
1015 ("limit", "1000"),
1016 ])
1017 .send()
1018 .await
1019 .expect("Failed to list records");
1020 assert_eq!(res.status(), StatusCode::OK);
1021 let body: Value = res.json().await.unwrap();
1022 let records = body["records"].as_array().unwrap();
1023 assert!(records.len() <= 100, "Limit should be clamped to max 100");
1024}
1025#[tokio::test]
1026async fn test_list_records_limit_clamping_min() {
1027 let client = client();
1028 let (did, jwt) = setup_new_user("list-limit-min").await;
1029 create_post_with_rkey(&client, &did, &jwt, "aaaa", "Post").await;
1030 let res = client
1031 .get(format!(
1032 "{}/xrpc/com.atproto.repo.listRecords",
1033 base_url().await
1034 ))
1035 .query(&[
1036 ("repo", did.as_str()),
1037 ("collection", "app.bsky.feed.post"),
1038 ("limit", "0"),
1039 ])
1040 .send()
1041 .await
1042 .expect("Failed to list records");
1043 assert_eq!(res.status(), StatusCode::OK);
1044 let body: Value = res.json().await.unwrap();
1045 let records = body["records"].as_array().unwrap();
1046 assert!(records.len() >= 1, "Limit should be clamped to min 1");
1047}
1048#[tokio::test]
1049async fn test_list_records_empty_collection() {
1050 let client = client();
1051 let (did, _jwt) = setup_new_user("list-empty").await;
1052 let res = client
1053 .get(format!(
1054 "{}/xrpc/com.atproto.repo.listRecords",
1055 base_url().await
1056 ))
1057 .query(&[
1058 ("repo", did.as_str()),
1059 ("collection", "app.bsky.feed.post"),
1060 ])
1061 .send()
1062 .await
1063 .expect("Failed to list records");
1064 assert_eq!(res.status(), StatusCode::OK);
1065 let body: Value = res.json().await.unwrap();
1066 let records = body["records"].as_array().unwrap();
1067 assert!(records.is_empty(), "Empty collection should return empty array");
1068 assert!(body["cursor"].is_null(), "Empty collection should have no cursor");
1069}
1070#[tokio::test]
1071async fn test_list_records_exact_limit() {
1072 let client = client();
1073 let (did, jwt) = setup_new_user("list-exact-limit").await;
1074 for i in 0..10 {
1075 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await;
1076 }
1077 let res = client
1078 .get(format!(
1079 "{}/xrpc/com.atproto.repo.listRecords",
1080 base_url().await
1081 ))
1082 .query(&[
1083 ("repo", did.as_str()),
1084 ("collection", "app.bsky.feed.post"),
1085 ("limit", "5"),
1086 ])
1087 .send()
1088 .await
1089 .expect("Failed to list records");
1090 assert_eq!(res.status(), StatusCode::OK);
1091 let body: Value = res.json().await.unwrap();
1092 let records = body["records"].as_array().unwrap();
1093 assert_eq!(records.len(), 5, "Should return exactly 5 records when limit=5");
1094}
1095#[tokio::test]
1096async fn test_list_records_cursor_exhaustion() {
1097 let client = client();
1098 let (did, jwt) = setup_new_user("list-cursor-exhaust").await;
1099 for i in 0..3 {
1100 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await;
1101 }
1102 let res = client
1103 .get(format!(
1104 "{}/xrpc/com.atproto.repo.listRecords",
1105 base_url().await
1106 ))
1107 .query(&[
1108 ("repo", did.as_str()),
1109 ("collection", "app.bsky.feed.post"),
1110 ("limit", "10"),
1111 ])
1112 .send()
1113 .await
1114 .expect("Failed to list records");
1115 assert_eq!(res.status(), StatusCode::OK);
1116 let body: Value = res.json().await.unwrap();
1117 let records = body["records"].as_array().unwrap();
1118 assert_eq!(records.len(), 3);
1119}
1120#[tokio::test]
1121async fn test_list_records_repo_not_found() {
1122 let client = client();
1123 let res = client
1124 .get(format!(
1125 "{}/xrpc/com.atproto.repo.listRecords",
1126 base_url().await
1127 ))
1128 .query(&[
1129 ("repo", "did:plc:nonexistent12345"),
1130 ("collection", "app.bsky.feed.post"),
1131 ])
1132 .send()
1133 .await
1134 .expect("Failed to list records");
1135 assert_eq!(res.status(), StatusCode::NOT_FOUND);
1136}
1137#[tokio::test]
1138async fn test_list_records_includes_cid() {
1139 let client = client();
1140 let (did, jwt) = setup_new_user("list-includes-cid").await;
1141 create_post_with_rkey(&client, &did, &jwt, "test", "Test post").await;
1142 let res = client
1143 .get(format!(
1144 "{}/xrpc/com.atproto.repo.listRecords",
1145 base_url().await
1146 ))
1147 .query(&[
1148 ("repo", did.as_str()),
1149 ("collection", "app.bsky.feed.post"),
1150 ])
1151 .send()
1152 .await
1153 .expect("Failed to list records");
1154 assert_eq!(res.status(), StatusCode::OK);
1155 let body: Value = res.json().await.unwrap();
1156 let records = body["records"].as_array().unwrap();
1157 for record in records {
1158 assert!(record["uri"].is_string(), "Record should have uri");
1159 assert!(record["cid"].is_string(), "Record should have cid");
1160 assert!(record["value"].is_object(), "Record should have value");
1161 let cid = record["cid"].as_str().unwrap();
1162 assert!(cid.starts_with("bafy"), "CID should be valid");
1163 }
1164}
1165#[tokio::test]
1166async fn test_list_records_cursor_with_reverse() {
1167 let client = client();
1168 let (did, jwt) = setup_new_user("list-cursor-reverse").await;
1169 for i in 0..5 {
1170 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await;
1171 }
1172 let res = client
1173 .get(format!(
1174 "{}/xrpc/com.atproto.repo.listRecords",
1175 base_url().await
1176 ))
1177 .query(&[
1178 ("repo", did.as_str()),
1179 ("collection", "app.bsky.feed.post"),
1180 ("limit", "2"),
1181 ("reverse", "true"),
1182 ])
1183 .send()
1184 .await
1185 .expect("Failed to list records");
1186 assert_eq!(res.status(), StatusCode::OK);
1187 let body: Value = res.json().await.unwrap();
1188 let records = body["records"].as_array().unwrap();
1189 let first_rkeys: Vec<&str> = records
1190 .iter()
1191 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
1192 .collect();
1193 assert_eq!(first_rkeys, vec!["post00", "post01"], "First page with reverse should start from oldest");
1194 if let Some(cursor) = body["cursor"].as_str() {
1195 let res2 = client
1196 .get(format!(
1197 "{}/xrpc/com.atproto.repo.listRecords",
1198 base_url().await
1199 ))
1200 .query(&[
1201 ("repo", did.as_str()),
1202 ("collection", "app.bsky.feed.post"),
1203 ("limit", "2"),
1204 ("reverse", "true"),
1205 ("cursor", cursor),
1206 ])
1207 .send()
1208 .await
1209 .expect("Failed to list records with cursor");
1210 let body2: Value = res2.json().await.unwrap();
1211 let records2 = body2["records"].as_array().unwrap();
1212 let second_rkeys: Vec<&str> = records2
1213 .iter()
1214 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
1215 .collect();
1216 assert_eq!(second_rkeys, vec!["post02", "post03"], "Second page should continue in ASC order");
1217 }
1218}