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