this repo has no description
1mod common;
2mod helpers;
3use common::*;
4use helpers::*;
5
6use chrono::Utc;
7use reqwest::StatusCode;
8use serde_json::{Value, json};
9use std::time::Duration;
10
11async fn create_post_with_rkey(
12 client: &reqwest::Client,
13 did: &str,
14 jwt: &str,
15 rkey: &str,
16 text: &str,
17) -> (String, String) {
18 let payload = json!({
19 "repo": did,
20 "collection": "app.bsky.feed.post",
21 "rkey": rkey,
22 "record": {
23 "$type": "app.bsky.feed.post",
24 "text": text,
25 "createdAt": Utc::now().to_rfc3339()
26 }
27 });
28
29 let res = client
30 .post(format!(
31 "{}/xrpc/com.atproto.repo.putRecord",
32 base_url().await
33 ))
34 .bearer_auth(jwt)
35 .json(&payload)
36 .send()
37 .await
38 .expect("Failed to create record");
39
40 assert_eq!(res.status(), StatusCode::OK);
41 let body: Value = res.json().await.unwrap();
42 (
43 body["uri"].as_str().unwrap().to_string(),
44 body["cid"].as_str().unwrap().to_string(),
45 )
46}
47
48#[tokio::test]
49async fn test_list_records_default_order() {
50 let client = client();
51 let (did, jwt) = setup_new_user("list-default-order").await;
52
53 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First post").await;
54 tokio::time::sleep(Duration::from_millis(50)).await;
55 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second post").await;
56 tokio::time::sleep(Duration::from_millis(50)).await;
57 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third post").await;
58
59 let res = client
60 .get(format!(
61 "{}/xrpc/com.atproto.repo.listRecords",
62 base_url().await
63 ))
64 .query(&[
65 ("repo", did.as_str()),
66 ("collection", "app.bsky.feed.post"),
67 ])
68 .send()
69 .await
70 .expect("Failed to list records");
71
72 assert_eq!(res.status(), StatusCode::OK);
73 let body: Value = res.json().await.unwrap();
74 let records = body["records"].as_array().unwrap();
75
76 assert_eq!(records.len(), 3);
77 let rkeys: Vec<&str> = records
78 .iter()
79 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
80 .collect();
81
82 assert_eq!(rkeys, vec!["cccc", "bbbb", "aaaa"], "Default order should be DESC (newest first)");
83}
84
85#[tokio::test]
86async fn test_list_records_reverse_true() {
87 let client = client();
88 let (did, jwt) = setup_new_user("list-reverse").await;
89
90 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First post").await;
91 tokio::time::sleep(Duration::from_millis(50)).await;
92 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second post").await;
93 tokio::time::sleep(Duration::from_millis(50)).await;
94 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third post").await;
95
96 let res = client
97 .get(format!(
98 "{}/xrpc/com.atproto.repo.listRecords",
99 base_url().await
100 ))
101 .query(&[
102 ("repo", did.as_str()),
103 ("collection", "app.bsky.feed.post"),
104 ("reverse", "true"),
105 ])
106 .send()
107 .await
108 .expect("Failed to list records");
109
110 assert_eq!(res.status(), StatusCode::OK);
111 let body: Value = res.json().await.unwrap();
112 let records = body["records"].as_array().unwrap();
113
114 let rkeys: Vec<&str> = records
115 .iter()
116 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
117 .collect();
118
119 assert_eq!(rkeys, vec!["aaaa", "bbbb", "cccc"], "reverse=true should give ASC order (oldest first)");
120}
121
122#[tokio::test]
123async fn test_list_records_cursor_pagination() {
124 let client = client();
125 let (did, jwt) = setup_new_user("list-cursor").await;
126
127 for i in 0..5 {
128 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await;
129 tokio::time::sleep(Duration::from_millis(50)).await;
130 }
131
132 let res = client
133 .get(format!(
134 "{}/xrpc/com.atproto.repo.listRecords",
135 base_url().await
136 ))
137 .query(&[
138 ("repo", did.as_str()),
139 ("collection", "app.bsky.feed.post"),
140 ("limit", "2"),
141 ])
142 .send()
143 .await
144 .expect("Failed to list records");
145
146 assert_eq!(res.status(), StatusCode::OK);
147 let body: Value = res.json().await.unwrap();
148 let records = body["records"].as_array().unwrap();
149 assert_eq!(records.len(), 2);
150
151 let cursor = body["cursor"].as_str().expect("Should have cursor with more records");
152
153 let res2 = client
154 .get(format!(
155 "{}/xrpc/com.atproto.repo.listRecords",
156 base_url().await
157 ))
158 .query(&[
159 ("repo", did.as_str()),
160 ("collection", "app.bsky.feed.post"),
161 ("limit", "2"),
162 ("cursor", cursor),
163 ])
164 .send()
165 .await
166 .expect("Failed to list records with cursor");
167
168 assert_eq!(res2.status(), StatusCode::OK);
169 let body2: Value = res2.json().await.unwrap();
170 let records2 = body2["records"].as_array().unwrap();
171 assert_eq!(records2.len(), 2);
172
173 let all_uris: Vec<&str> = records
174 .iter()
175 .chain(records2.iter())
176 .map(|r| r["uri"].as_str().unwrap())
177 .collect();
178 let unique_uris: std::collections::HashSet<&str> = all_uris.iter().copied().collect();
179 assert_eq!(all_uris.len(), unique_uris.len(), "Cursor pagination should not repeat records");
180}
181
182#[tokio::test]
183async fn test_list_records_rkey_start() {
184 let client = client();
185 let (did, jwt) = setup_new_user("list-rkey-start").await;
186
187 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First").await;
188 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second").await;
189 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third").await;
190 create_post_with_rkey(&client, &did, &jwt, "dddd", "Fourth").await;
191
192 let res = client
193 .get(format!(
194 "{}/xrpc/com.atproto.repo.listRecords",
195 base_url().await
196 ))
197 .query(&[
198 ("repo", did.as_str()),
199 ("collection", "app.bsky.feed.post"),
200 ("rkeyStart", "bbbb"),
201 ("reverse", "true"),
202 ])
203 .send()
204 .await
205 .expect("Failed to list records");
206
207 assert_eq!(res.status(), StatusCode::OK);
208 let body: Value = res.json().await.unwrap();
209 let records = body["records"].as_array().unwrap();
210
211 let rkeys: Vec<&str> = records
212 .iter()
213 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
214 .collect();
215
216 for rkey in &rkeys {
217 assert!(*rkey >= "bbbb", "rkeyStart should filter records >= start");
218 }
219}
220
221#[tokio::test]
222async fn test_list_records_rkey_end() {
223 let client = client();
224 let (did, jwt) = setup_new_user("list-rkey-end").await;
225
226 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First").await;
227 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second").await;
228 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third").await;
229 create_post_with_rkey(&client, &did, &jwt, "dddd", "Fourth").await;
230
231 let res = client
232 .get(format!(
233 "{}/xrpc/com.atproto.repo.listRecords",
234 base_url().await
235 ))
236 .query(&[
237 ("repo", did.as_str()),
238 ("collection", "app.bsky.feed.post"),
239 ("rkeyEnd", "cccc"),
240 ("reverse", "true"),
241 ])
242 .send()
243 .await
244 .expect("Failed to list records");
245
246 assert_eq!(res.status(), StatusCode::OK);
247 let body: Value = res.json().await.unwrap();
248 let records = body["records"].as_array().unwrap();
249
250 let rkeys: Vec<&str> = records
251 .iter()
252 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
253 .collect();
254
255 for rkey in &rkeys {
256 assert!(*rkey <= "cccc", "rkeyEnd should filter records <= end");
257 }
258}
259
260#[tokio::test]
261async fn test_list_records_rkey_range() {
262 let client = client();
263 let (did, jwt) = setup_new_user("list-rkey-range").await;
264
265 create_post_with_rkey(&client, &did, &jwt, "aaaa", "First").await;
266 create_post_with_rkey(&client, &did, &jwt, "bbbb", "Second").await;
267 create_post_with_rkey(&client, &did, &jwt, "cccc", "Third").await;
268 create_post_with_rkey(&client, &did, &jwt, "dddd", "Fourth").await;
269 create_post_with_rkey(&client, &did, &jwt, "eeee", "Fifth").await;
270
271 let res = client
272 .get(format!(
273 "{}/xrpc/com.atproto.repo.listRecords",
274 base_url().await
275 ))
276 .query(&[
277 ("repo", did.as_str()),
278 ("collection", "app.bsky.feed.post"),
279 ("rkeyStart", "bbbb"),
280 ("rkeyEnd", "dddd"),
281 ("reverse", "true"),
282 ])
283 .send()
284 .await
285 .expect("Failed to list records");
286
287 assert_eq!(res.status(), StatusCode::OK);
288 let body: Value = res.json().await.unwrap();
289 let records = body["records"].as_array().unwrap();
290
291 let rkeys: Vec<&str> = records
292 .iter()
293 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
294 .collect();
295
296 for rkey in &rkeys {
297 assert!(*rkey >= "bbbb" && *rkey <= "dddd", "Range should be inclusive, got {}", rkey);
298 }
299 assert!(!rkeys.is_empty(), "Should have at least some records in range");
300}
301
302#[tokio::test]
303async fn test_list_records_limit_clamping_max() {
304 let client = client();
305 let (did, jwt) = setup_new_user("list-limit-max").await;
306
307 for i in 0..5 {
308 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await;
309 }
310
311 let res = client
312 .get(format!(
313 "{}/xrpc/com.atproto.repo.listRecords",
314 base_url().await
315 ))
316 .query(&[
317 ("repo", did.as_str()),
318 ("collection", "app.bsky.feed.post"),
319 ("limit", "1000"),
320 ])
321 .send()
322 .await
323 .expect("Failed to list records");
324
325 assert_eq!(res.status(), StatusCode::OK);
326 let body: Value = res.json().await.unwrap();
327 let records = body["records"].as_array().unwrap();
328 assert!(records.len() <= 100, "Limit should be clamped to max 100");
329}
330
331#[tokio::test]
332async fn test_list_records_limit_clamping_min() {
333 let client = client();
334 let (did, jwt) = setup_new_user("list-limit-min").await;
335
336 create_post_with_rkey(&client, &did, &jwt, "aaaa", "Post").await;
337
338 let res = client
339 .get(format!(
340 "{}/xrpc/com.atproto.repo.listRecords",
341 base_url().await
342 ))
343 .query(&[
344 ("repo", did.as_str()),
345 ("collection", "app.bsky.feed.post"),
346 ("limit", "0"),
347 ])
348 .send()
349 .await
350 .expect("Failed to list records");
351
352 assert_eq!(res.status(), StatusCode::OK);
353 let body: Value = res.json().await.unwrap();
354 let records = body["records"].as_array().unwrap();
355 assert!(records.len() >= 1, "Limit should be clamped to min 1");
356}
357
358#[tokio::test]
359async fn test_list_records_empty_collection() {
360 let client = client();
361 let (did, _jwt) = setup_new_user("list-empty").await;
362
363 let res = client
364 .get(format!(
365 "{}/xrpc/com.atproto.repo.listRecords",
366 base_url().await
367 ))
368 .query(&[
369 ("repo", did.as_str()),
370 ("collection", "app.bsky.feed.post"),
371 ])
372 .send()
373 .await
374 .expect("Failed to list records");
375
376 assert_eq!(res.status(), StatusCode::OK);
377 let body: Value = res.json().await.unwrap();
378 let records = body["records"].as_array().unwrap();
379 assert!(records.is_empty(), "Empty collection should return empty array");
380 assert!(body["cursor"].is_null(), "Empty collection should have no cursor");
381}
382
383#[tokio::test]
384async fn test_list_records_exact_limit() {
385 let client = client();
386 let (did, jwt) = setup_new_user("list-exact-limit").await;
387
388 for i in 0..10 {
389 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await;
390 }
391
392 let res = client
393 .get(format!(
394 "{}/xrpc/com.atproto.repo.listRecords",
395 base_url().await
396 ))
397 .query(&[
398 ("repo", did.as_str()),
399 ("collection", "app.bsky.feed.post"),
400 ("limit", "5"),
401 ])
402 .send()
403 .await
404 .expect("Failed to list records");
405
406 assert_eq!(res.status(), StatusCode::OK);
407 let body: Value = res.json().await.unwrap();
408 let records = body["records"].as_array().unwrap();
409 assert_eq!(records.len(), 5, "Should return exactly 5 records when limit=5");
410}
411
412#[tokio::test]
413async fn test_list_records_cursor_exhaustion() {
414 let client = client();
415 let (did, jwt) = setup_new_user("list-cursor-exhaust").await;
416
417 for i in 0..3 {
418 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await;
419 }
420
421 let res = client
422 .get(format!(
423 "{}/xrpc/com.atproto.repo.listRecords",
424 base_url().await
425 ))
426 .query(&[
427 ("repo", did.as_str()),
428 ("collection", "app.bsky.feed.post"),
429 ("limit", "10"),
430 ])
431 .send()
432 .await
433 .expect("Failed to list records");
434
435 assert_eq!(res.status(), StatusCode::OK);
436 let body: Value = res.json().await.unwrap();
437 let records = body["records"].as_array().unwrap();
438 assert_eq!(records.len(), 3);
439}
440
441#[tokio::test]
442async fn test_list_records_repo_not_found() {
443 let client = client();
444
445 let res = client
446 .get(format!(
447 "{}/xrpc/com.atproto.repo.listRecords",
448 base_url().await
449 ))
450 .query(&[
451 ("repo", "did:plc:nonexistent12345"),
452 ("collection", "app.bsky.feed.post"),
453 ])
454 .send()
455 .await
456 .expect("Failed to list records");
457
458 assert_eq!(res.status(), StatusCode::NOT_FOUND);
459}
460
461#[tokio::test]
462async fn test_list_records_includes_cid() {
463 let client = client();
464 let (did, jwt) = setup_new_user("list-includes-cid").await;
465
466 create_post_with_rkey(&client, &did, &jwt, "test", "Test post").await;
467
468 let res = client
469 .get(format!(
470 "{}/xrpc/com.atproto.repo.listRecords",
471 base_url().await
472 ))
473 .query(&[
474 ("repo", did.as_str()),
475 ("collection", "app.bsky.feed.post"),
476 ])
477 .send()
478 .await
479 .expect("Failed to list records");
480
481 assert_eq!(res.status(), StatusCode::OK);
482 let body: Value = res.json().await.unwrap();
483 let records = body["records"].as_array().unwrap();
484
485 for record in records {
486 assert!(record["uri"].is_string(), "Record should have uri");
487 assert!(record["cid"].is_string(), "Record should have cid");
488 assert!(record["value"].is_object(), "Record should have value");
489 let cid = record["cid"].as_str().unwrap();
490 assert!(cid.starts_with("bafy"), "CID should be valid");
491 }
492}
493
494#[tokio::test]
495async fn test_list_records_cursor_with_reverse() {
496 let client = client();
497 let (did, jwt) = setup_new_user("list-cursor-reverse").await;
498
499 for i in 0..5 {
500 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await;
501 }
502
503 let res = client
504 .get(format!(
505 "{}/xrpc/com.atproto.repo.listRecords",
506 base_url().await
507 ))
508 .query(&[
509 ("repo", did.as_str()),
510 ("collection", "app.bsky.feed.post"),
511 ("limit", "2"),
512 ("reverse", "true"),
513 ])
514 .send()
515 .await
516 .expect("Failed to list records");
517
518 assert_eq!(res.status(), StatusCode::OK);
519 let body: Value = res.json().await.unwrap();
520 let records = body["records"].as_array().unwrap();
521 let first_rkeys: Vec<&str> = records
522 .iter()
523 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
524 .collect();
525
526 assert_eq!(first_rkeys, vec!["post00", "post01"], "First page with reverse should start from oldest");
527
528 if let Some(cursor) = body["cursor"].as_str() {
529 let res2 = client
530 .get(format!(
531 "{}/xrpc/com.atproto.repo.listRecords",
532 base_url().await
533 ))
534 .query(&[
535 ("repo", did.as_str()),
536 ("collection", "app.bsky.feed.post"),
537 ("limit", "2"),
538 ("reverse", "true"),
539 ("cursor", cursor),
540 ])
541 .send()
542 .await
543 .expect("Failed to list records with cursor");
544
545 let body2: Value = res2.json().await.unwrap();
546 let records2 = body2["records"].as_array().unwrap();
547 let second_rkeys: Vec<&str> = records2
548 .iter()
549 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
550 .collect();
551
552 assert_eq!(second_rkeys, vec!["post02", "post03"], "Second page should continue in ASC order");
553 }
554}