this repo has no description
1mod common;
2mod helpers;
3use chrono::Utc;
4use common::*;
5use helpers::*;
6use reqwest::{StatusCode, header};
7use serde_json::{Value, json};
8use std::time::Duration;
9
10#[tokio::test]
11async fn test_record_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 assert_eq!(
39 create_res.status(),
40 StatusCode::OK,
41 "Failed to create record"
42 );
43 let create_body: Value = create_res
44 .json()
45 .await
46 .expect("create response was not JSON");
47 let uri = create_body["uri"].as_str().unwrap();
48 let initial_cid = create_body["cid"].as_str().unwrap().to_string();
49 let params = [
50 ("repo", did.as_str()),
51 ("collection", collection),
52 ("rkey", &rkey),
53 ];
54 let get_res = client
55 .get(format!(
56 "{}/xrpc/com.atproto.repo.getRecord",
57 base_url().await
58 ))
59 .query(¶ms)
60 .send()
61 .await
62 .expect("Failed to send get request");
63 assert_eq!(
64 get_res.status(),
65 StatusCode::OK,
66 "Failed to get record after create"
67 );
68 let get_body: Value = get_res.json().await.expect("get response was not JSON");
69 assert_eq!(get_body["uri"], uri);
70 assert_eq!(get_body["value"]["text"], original_text);
71 let updated_text = "This post has been updated.";
72 let update_payload = json!({
73 "repo": did,
74 "collection": collection,
75 "rkey": rkey,
76 "record": { "$type": collection, "text": updated_text, "createdAt": now },
77 "swapRecord": initial_cid
78 });
79 let update_res = client
80 .post(format!(
81 "{}/xrpc/com.atproto.repo.putRecord",
82 base_url().await
83 ))
84 .bearer_auth(&jwt)
85 .json(&update_payload)
86 .send()
87 .await
88 .expect("Failed to send update request");
89 assert_eq!(
90 update_res.status(),
91 StatusCode::OK,
92 "Failed to update record"
93 );
94 let update_body: Value = update_res
95 .json()
96 .await
97 .expect("update response was not JSON");
98 let updated_cid = update_body["cid"].as_str().unwrap().to_string();
99 let get_updated_res = client
100 .get(format!(
101 "{}/xrpc/com.atproto.repo.getRecord",
102 base_url().await
103 ))
104 .query(¶ms)
105 .send()
106 .await
107 .expect("Failed to send get-after-update request");
108 let get_updated_body: Value = get_updated_res
109 .json()
110 .await
111 .expect("get-updated response was not JSON");
112 assert_eq!(
113 get_updated_body["value"]["text"], updated_text,
114 "Text was not updated"
115 );
116 let stale_update_payload = json!({
117 "repo": did,
118 "collection": collection,
119 "rkey": rkey,
120 "record": { "$type": collection, "text": "Stale update", "createdAt": now },
121 "swapRecord": initial_cid
122 });
123 let stale_res = client
124 .post(format!(
125 "{}/xrpc/com.atproto.repo.putRecord",
126 base_url().await
127 ))
128 .bearer_auth(&jwt)
129 .json(&stale_update_payload)
130 .send()
131 .await
132 .expect("Failed to send stale update");
133 assert_eq!(
134 stale_res.status(),
135 StatusCode::CONFLICT,
136 "Stale update should cause 409"
137 );
138 let good_update_payload = json!({
139 "repo": did,
140 "collection": collection,
141 "rkey": rkey,
142 "record": { "$type": collection, "text": "Good update", "createdAt": now },
143 "swapRecord": updated_cid
144 });
145 let good_res = client
146 .post(format!(
147 "{}/xrpc/com.atproto.repo.putRecord",
148 base_url().await
149 ))
150 .bearer_auth(&jwt)
151 .json(&good_update_payload)
152 .send()
153 .await
154 .expect("Failed to send good update");
155 assert_eq!(
156 good_res.status(),
157 StatusCode::OK,
158 "Good update should succeed"
159 );
160 let delete_payload = json!({ "repo": did, "collection": collection, "rkey": rkey });
161 let delete_res = client
162 .post(format!(
163 "{}/xrpc/com.atproto.repo.deleteRecord",
164 base_url().await
165 ))
166 .bearer_auth(&jwt)
167 .json(&delete_payload)
168 .send()
169 .await
170 .expect("Failed to send delete request");
171 assert_eq!(
172 delete_res.status(),
173 StatusCode::OK,
174 "Failed to delete record"
175 );
176 let get_deleted_res = client
177 .get(format!(
178 "{}/xrpc/com.atproto.repo.getRecord",
179 base_url().await
180 ))
181 .query(¶ms)
182 .send()
183 .await
184 .expect("Failed to send get-after-delete request");
185 assert_eq!(
186 get_deleted_res.status(),
187 StatusCode::NOT_FOUND,
188 "Record should be deleted"
189 );
190}
191
192#[tokio::test]
193async fn test_profile_with_blob_lifecycle() {
194 let client = client();
195 let (did, jwt) = setup_new_user("profile-blob").await;
196 let blob_data = b"This is test blob data for a profile avatar";
197 let upload_res = client
198 .post(format!(
199 "{}/xrpc/com.atproto.repo.uploadBlob",
200 base_url().await
201 ))
202 .header(header::CONTENT_TYPE, "text/plain")
203 .bearer_auth(&jwt)
204 .body(blob_data.to_vec())
205 .send()
206 .await
207 .expect("Failed to upload blob");
208 assert_eq!(upload_res.status(), StatusCode::OK);
209 let upload_body: Value = upload_res.json().await.unwrap();
210 let blob_ref = upload_body["blob"].clone();
211 let profile_payload = json!({
212 "repo": did,
213 "collection": "app.bsky.actor.profile",
214 "rkey": "self",
215 "record": {
216 "$type": "app.bsky.actor.profile",
217 "displayName": "Test User",
218 "description": "A test profile for lifecycle testing",
219 "avatar": blob_ref
220 }
221 });
222 let create_res = client
223 .post(format!(
224 "{}/xrpc/com.atproto.repo.putRecord",
225 base_url().await
226 ))
227 .bearer_auth(&jwt)
228 .json(&profile_payload)
229 .send()
230 .await
231 .expect("Failed to create profile");
232 assert_eq!(
233 create_res.status(),
234 StatusCode::OK,
235 "Failed to create profile"
236 );
237 let create_body: Value = create_res.json().await.unwrap();
238 let initial_cid = create_body["cid"].as_str().unwrap().to_string();
239 let get_res = client
240 .get(format!(
241 "{}/xrpc/com.atproto.repo.getRecord",
242 base_url().await
243 ))
244 .query(&[
245 ("repo", did.as_str()),
246 ("collection", "app.bsky.actor.profile"),
247 ("rkey", "self"),
248 ])
249 .send()
250 .await
251 .expect("Failed to get profile");
252 assert_eq!(get_res.status(), StatusCode::OK);
253 let get_body: Value = get_res.json().await.unwrap();
254 assert_eq!(get_body["value"]["displayName"], "Test User");
255 assert!(get_body["value"]["avatar"]["ref"]["$link"].is_string());
256 let update_payload = json!({
257 "repo": did,
258 "collection": "app.bsky.actor.profile",
259 "rkey": "self",
260 "record": { "$type": "app.bsky.actor.profile", "displayName": "Updated User", "description": "Profile updated" },
261 "swapRecord": initial_cid
262 });
263 let update_res = client
264 .post(format!(
265 "{}/xrpc/com.atproto.repo.putRecord",
266 base_url().await
267 ))
268 .bearer_auth(&jwt)
269 .json(&update_payload)
270 .send()
271 .await
272 .expect("Failed to update profile");
273 assert_eq!(
274 update_res.status(),
275 StatusCode::OK,
276 "Failed to update profile"
277 );
278 let get_updated_res = client
279 .get(format!(
280 "{}/xrpc/com.atproto.repo.getRecord",
281 base_url().await
282 ))
283 .query(&[
284 ("repo", did.as_str()),
285 ("collection", "app.bsky.actor.profile"),
286 ("rkey", "self"),
287 ])
288 .send()
289 .await
290 .expect("Failed to get updated profile");
291 let updated_body: Value = get_updated_res.json().await.unwrap();
292 assert_eq!(updated_body["value"]["displayName"], "Updated User");
293}
294
295#[tokio::test]
296async fn test_reply_thread_lifecycle() {
297 let client = client();
298 let (alice_did, alice_jwt) = setup_new_user("alice-thread").await;
299 let (bob_did, bob_jwt) = setup_new_user("bob-thread").await;
300 let (root_uri, root_cid) =
301 create_post(&client, &alice_did, &alice_jwt, "This is the root post").await;
302 tokio::time::sleep(Duration::from_millis(100)).await;
303 let reply_collection = "app.bsky.feed.post";
304 let reply_rkey = format!("e2e_reply_{}", Utc::now().timestamp_millis());
305 let reply_payload = json!({
306 "repo": bob_did,
307 "collection": reply_collection,
308 "rkey": reply_rkey,
309 "record": {
310 "$type": reply_collection,
311 "text": "This is Bob's reply to Alice",
312 "createdAt": Utc::now().to_rfc3339(),
313 "reply": {
314 "root": { "uri": root_uri, "cid": root_cid },
315 "parent": { "uri": root_uri, "cid": root_cid }
316 }
317 }
318 });
319 let reply_res = client
320 .post(format!(
321 "{}/xrpc/com.atproto.repo.putRecord",
322 base_url().await
323 ))
324 .bearer_auth(&bob_jwt)
325 .json(&reply_payload)
326 .send()
327 .await
328 .expect("Failed to create reply");
329 assert_eq!(reply_res.status(), StatusCode::OK, "Failed to create reply");
330 let reply_body: Value = reply_res.json().await.unwrap();
331 let reply_uri = reply_body["uri"].as_str().unwrap();
332 let reply_cid = reply_body["cid"].as_str().unwrap();
333 let get_reply_res = client
334 .get(format!(
335 "{}/xrpc/com.atproto.repo.getRecord",
336 base_url().await
337 ))
338 .query(&[
339 ("repo", bob_did.as_str()),
340 ("collection", reply_collection),
341 ("rkey", reply_rkey.as_str()),
342 ])
343 .send()
344 .await
345 .expect("Failed to get reply");
346 assert_eq!(get_reply_res.status(), StatusCode::OK);
347 let reply_record: Value = get_reply_res.json().await.unwrap();
348 assert_eq!(reply_record["value"]["reply"]["root"]["uri"], root_uri);
349 tokio::time::sleep(Duration::from_millis(100)).await;
350 let nested_reply_rkey = format!("e2e_nested_reply_{}", Utc::now().timestamp_millis());
351 let nested_payload = json!({
352 "repo": alice_did,
353 "collection": reply_collection,
354 "rkey": nested_reply_rkey,
355 "record": {
356 "$type": reply_collection,
357 "text": "Alice replies to Bob's reply",
358 "createdAt": Utc::now().to_rfc3339(),
359 "reply": {
360 "root": { "uri": root_uri, "cid": root_cid },
361 "parent": { "uri": reply_uri, "cid": reply_cid }
362 }
363 }
364 });
365 let nested_res = client
366 .post(format!(
367 "{}/xrpc/com.atproto.repo.putRecord",
368 base_url().await
369 ))
370 .bearer_auth(&alice_jwt)
371 .json(&nested_payload)
372 .send()
373 .await
374 .expect("Failed to create nested reply");
375 assert_eq!(
376 nested_res.status(),
377 StatusCode::OK,
378 "Failed to create nested reply"
379 );
380}
381
382#[tokio::test]
383async fn test_authorization_protects_repos() {
384 let client = client();
385 let (alice_did, alice_jwt) = setup_new_user("alice-auth").await;
386 let (_bob_did, bob_jwt) = setup_new_user("bob-auth").await;
387 let (post_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's post").await;
388 let post_rkey = post_uri.split('/').last().unwrap();
389 let post_payload = json!({
390 "repo": alice_did,
391 "collection": "app.bsky.feed.post",
392 "rkey": "unauthorized-post",
393 "record": { "$type": "app.bsky.feed.post", "text": "Bob trying to post as Alice", "createdAt": Utc::now().to_rfc3339() }
394 });
395 let write_res = client
396 .post(format!(
397 "{}/xrpc/com.atproto.repo.putRecord",
398 base_url().await
399 ))
400 .bearer_auth(&bob_jwt)
401 .json(&post_payload)
402 .send()
403 .await
404 .expect("Failed to send request");
405 assert!(
406 write_res.status() == StatusCode::FORBIDDEN
407 || write_res.status() == StatusCode::UNAUTHORIZED,
408 "Expected 403/401 for writing to another user's repo, got {}",
409 write_res.status()
410 );
411 let delete_payload =
412 json!({ "repo": alice_did, "collection": "app.bsky.feed.post", "rkey": post_rkey });
413 let delete_res = client
414 .post(format!(
415 "{}/xrpc/com.atproto.repo.deleteRecord",
416 base_url().await
417 ))
418 .bearer_auth(&bob_jwt)
419 .json(&delete_payload)
420 .send()
421 .await
422 .expect("Failed to send request");
423 assert!(
424 delete_res.status() == StatusCode::FORBIDDEN
425 || delete_res.status() == StatusCode::UNAUTHORIZED,
426 "Expected 403/401 for deleting another user's record, got {}",
427 delete_res.status()
428 );
429 let get_res = client
430 .get(format!(
431 "{}/xrpc/com.atproto.repo.getRecord",
432 base_url().await
433 ))
434 .query(&[
435 ("repo", alice_did.as_str()),
436 ("collection", "app.bsky.feed.post"),
437 ("rkey", post_rkey),
438 ])
439 .send()
440 .await
441 .expect("Failed to verify record exists");
442 assert_eq!(
443 get_res.status(),
444 StatusCode::OK,
445 "Record should still exist"
446 );
447}
448
449#[tokio::test]
450async fn test_apply_writes_batch() {
451 let client = client();
452 let (did, jwt) = setup_new_user("apply-writes-batch").await;
453 let now = Utc::now().to_rfc3339();
454 let writes_payload = json!({
455 "repo": did,
456 "writes": [
457 { "$type": "com.atproto.repo.applyWrites#create", "collection": "app.bsky.feed.post", "rkey": "batch-post-1", "value": { "$type": "app.bsky.feed.post", "text": "First batch post", "createdAt": now } },
458 { "$type": "com.atproto.repo.applyWrites#create", "collection": "app.bsky.feed.post", "rkey": "batch-post-2", "value": { "$type": "app.bsky.feed.post", "text": "Second batch post", "createdAt": now } },
459 { "$type": "com.atproto.repo.applyWrites#create", "collection": "app.bsky.actor.profile", "rkey": "self", "value": { "$type": "app.bsky.actor.profile", "displayName": "Batch User" } }
460 ]
461 });
462 let apply_res = client
463 .post(format!(
464 "{}/xrpc/com.atproto.repo.applyWrites",
465 base_url().await
466 ))
467 .bearer_auth(&jwt)
468 .json(&writes_payload)
469 .send()
470 .await
471 .expect("Failed to apply writes");
472 assert_eq!(apply_res.status(), StatusCode::OK);
473 let get_post1 = client
474 .get(format!(
475 "{}/xrpc/com.atproto.repo.getRecord",
476 base_url().await
477 ))
478 .query(&[
479 ("repo", did.as_str()),
480 ("collection", "app.bsky.feed.post"),
481 ("rkey", "batch-post-1"),
482 ])
483 .send()
484 .await
485 .expect("Failed to get post 1");
486 assert_eq!(get_post1.status(), StatusCode::OK);
487 let post1_body: Value = get_post1.json().await.unwrap();
488 assert_eq!(post1_body["value"]["text"], "First batch post");
489 let get_post2 = client
490 .get(format!(
491 "{}/xrpc/com.atproto.repo.getRecord",
492 base_url().await
493 ))
494 .query(&[
495 ("repo", did.as_str()),
496 ("collection", "app.bsky.feed.post"),
497 ("rkey", "batch-post-2"),
498 ])
499 .send()
500 .await
501 .expect("Failed to get post 2");
502 assert_eq!(get_post2.status(), StatusCode::OK);
503 let get_profile = client
504 .get(format!(
505 "{}/xrpc/com.atproto.repo.getRecord",
506 base_url().await
507 ))
508 .query(&[
509 ("repo", did.as_str()),
510 ("collection", "app.bsky.actor.profile"),
511 ("rkey", "self"),
512 ])
513 .send()
514 .await
515 .expect("Failed to get profile");
516 let profile_body: Value = get_profile.json().await.unwrap();
517 assert_eq!(profile_body["value"]["displayName"], "Batch User");
518 let update_writes = json!({
519 "repo": did,
520 "writes": [
521 { "$type": "com.atproto.repo.applyWrites#update", "collection": "app.bsky.actor.profile", "rkey": "self", "value": { "$type": "app.bsky.actor.profile", "displayName": "Updated Batch User" } },
522 { "$type": "com.atproto.repo.applyWrites#delete", "collection": "app.bsky.feed.post", "rkey": "batch-post-1" }
523 ]
524 });
525 let update_res = client
526 .post(format!(
527 "{}/xrpc/com.atproto.repo.applyWrites",
528 base_url().await
529 ))
530 .bearer_auth(&jwt)
531 .json(&update_writes)
532 .send()
533 .await
534 .expect("Failed to apply update writes");
535 assert_eq!(update_res.status(), StatusCode::OK);
536 let get_updated_profile = client
537 .get(format!(
538 "{}/xrpc/com.atproto.repo.getRecord",
539 base_url().await
540 ))
541 .query(&[
542 ("repo", did.as_str()),
543 ("collection", "app.bsky.actor.profile"),
544 ("rkey", "self"),
545 ])
546 .send()
547 .await
548 .expect("Failed to get updated profile");
549 let updated_profile: Value = get_updated_profile.json().await.unwrap();
550 assert_eq!(
551 updated_profile["value"]["displayName"],
552 "Updated Batch User"
553 );
554 let get_deleted_post = client
555 .get(format!(
556 "{}/xrpc/com.atproto.repo.getRecord",
557 base_url().await
558 ))
559 .query(&[
560 ("repo", did.as_str()),
561 ("collection", "app.bsky.feed.post"),
562 ("rkey", "batch-post-1"),
563 ])
564 .send()
565 .await
566 .expect("Failed to check deleted post");
567 assert_eq!(
568 get_deleted_post.status(),
569 StatusCode::NOT_FOUND,
570 "Batch-deleted post should be gone"
571 );
572}
573
574async fn create_post_with_rkey(
575 client: &reqwest::Client,
576 did: &str,
577 jwt: &str,
578 rkey: &str,
579 text: &str,
580) -> (String, String) {
581 let payload = json!({
582 "repo": did, "collection": "app.bsky.feed.post", "rkey": rkey,
583 "record": { "$type": "app.bsky.feed.post", "text": text, "createdAt": Utc::now().to_rfc3339() }
584 });
585 let res = client
586 .post(format!(
587 "{}/xrpc/com.atproto.repo.putRecord",
588 base_url().await
589 ))
590 .bearer_auth(jwt)
591 .json(&payload)
592 .send()
593 .await
594 .expect("Failed to create record");
595 assert_eq!(res.status(), StatusCode::OK);
596 let body: Value = res.json().await.unwrap();
597 (
598 body["uri"].as_str().unwrap().to_string(),
599 body["cid"].as_str().unwrap().to_string(),
600 )
601}
602
603#[tokio::test]
604async fn test_list_records_comprehensive() {
605 let client = client();
606 let (did, jwt) = setup_new_user("list-records-test").await;
607 for i in 0..5 {
608 create_post_with_rkey(
609 &client,
610 &did,
611 &jwt,
612 &format!("post{:02}", i),
613 &format!("Post {}", i),
614 )
615 .await;
616 tokio::time::sleep(Duration::from_millis(50)).await;
617 }
618 let res = client
619 .get(format!(
620 "{}/xrpc/com.atproto.repo.listRecords",
621 base_url().await
622 ))
623 .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post")])
624 .send()
625 .await
626 .expect("Failed to list records");
627 assert_eq!(res.status(), StatusCode::OK);
628 let body: Value = res.json().await.unwrap();
629 let records = body["records"].as_array().unwrap();
630 assert_eq!(records.len(), 5);
631 let rkeys: Vec<&str> = records
632 .iter()
633 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
634 .collect();
635 assert_eq!(
636 rkeys,
637 vec!["post04", "post03", "post02", "post01", "post00"],
638 "Default order should be DESC"
639 );
640 for record in records {
641 assert!(record["uri"].is_string());
642 assert!(record["cid"].is_string());
643 assert!(record["cid"].as_str().unwrap().starts_with("bafy"));
644 assert!(record["value"].is_object());
645 }
646 let rev_res = client
647 .get(format!(
648 "{}/xrpc/com.atproto.repo.listRecords",
649 base_url().await
650 ))
651 .query(&[
652 ("repo", did.as_str()),
653 ("collection", "app.bsky.feed.post"),
654 ("reverse", "true"),
655 ])
656 .send()
657 .await
658 .expect("Failed to list records reverse");
659 let rev_body: Value = rev_res.json().await.unwrap();
660 let rev_rkeys: Vec<&str> = rev_body["records"]
661 .as_array()
662 .unwrap()
663 .iter()
664 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
665 .collect();
666 assert_eq!(
667 rev_rkeys,
668 vec!["post00", "post01", "post02", "post03", "post04"],
669 "reverse=true should give ASC"
670 );
671 let page1 = client
672 .get(format!(
673 "{}/xrpc/com.atproto.repo.listRecords",
674 base_url().await
675 ))
676 .query(&[
677 ("repo", did.as_str()),
678 ("collection", "app.bsky.feed.post"),
679 ("limit", "2"),
680 ])
681 .send()
682 .await
683 .expect("Failed to list page 1");
684 let page1_body: Value = page1.json().await.unwrap();
685 let page1_records = page1_body["records"].as_array().unwrap();
686 assert_eq!(page1_records.len(), 2);
687 let cursor = page1_body["cursor"].as_str().expect("Should have cursor");
688 let page2 = client
689 .get(format!(
690 "{}/xrpc/com.atproto.repo.listRecords",
691 base_url().await
692 ))
693 .query(&[
694 ("repo", did.as_str()),
695 ("collection", "app.bsky.feed.post"),
696 ("limit", "2"),
697 ("cursor", cursor),
698 ])
699 .send()
700 .await
701 .expect("Failed to list page 2");
702 let page2_body: Value = page2.json().await.unwrap();
703 let page2_records = page2_body["records"].as_array().unwrap();
704 assert_eq!(page2_records.len(), 2);
705 let all_uris: Vec<&str> = page1_records
706 .iter()
707 .chain(page2_records.iter())
708 .map(|r| r["uri"].as_str().unwrap())
709 .collect();
710 let unique_uris: std::collections::HashSet<&str> = all_uris.iter().copied().collect();
711 assert_eq!(
712 all_uris.len(),
713 unique_uris.len(),
714 "Cursor pagination should not repeat records"
715 );
716 let range_res = client
717 .get(format!(
718 "{}/xrpc/com.atproto.repo.listRecords",
719 base_url().await
720 ))
721 .query(&[
722 ("repo", did.as_str()),
723 ("collection", "app.bsky.feed.post"),
724 ("rkeyStart", "post01"),
725 ("rkeyEnd", "post03"),
726 ("reverse", "true"),
727 ])
728 .send()
729 .await
730 .expect("Failed to list range");
731 let range_body: Value = range_res.json().await.unwrap();
732 let range_rkeys: Vec<&str> = range_body["records"]
733 .as_array()
734 .unwrap()
735 .iter()
736 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
737 .collect();
738 for rkey in &range_rkeys {
739 assert!(
740 *rkey >= "post01" && *rkey <= "post03",
741 "Range should be inclusive"
742 );
743 }
744 let limit_res = client
745 .get(format!(
746 "{}/xrpc/com.atproto.repo.listRecords",
747 base_url().await
748 ))
749 .query(&[
750 ("repo", did.as_str()),
751 ("collection", "app.bsky.feed.post"),
752 ("limit", "1000"),
753 ])
754 .send()
755 .await
756 .expect("Failed with high limit");
757 let limit_body: Value = limit_res.json().await.unwrap();
758 assert!(
759 limit_body["records"].as_array().unwrap().len() <= 100,
760 "Limit should be clamped to max 100"
761 );
762 let not_found_res = client
763 .get(format!(
764 "{}/xrpc/com.atproto.repo.listRecords",
765 base_url().await
766 ))
767 .query(&[
768 ("repo", "did:plc:nonexistent12345"),
769 ("collection", "app.bsky.feed.post"),
770 ])
771 .send()
772 .await
773 .expect("Failed with nonexistent repo");
774 assert_eq!(not_found_res.status(), StatusCode::NOT_FOUND);
775}