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}