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!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 30 .bearer_auth(&jwt) 31 .json(&create_payload) 32 .send() 33 .await 34 .expect("Failed to send create request"); 35 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create record"); 36 let create_body: Value = create_res.json().await.expect("create response was not JSON"); 37 let uri = create_body["uri"].as_str().unwrap(); 38 let initial_cid = create_body["cid"].as_str().unwrap().to_string(); 39 let params = [("repo", did.as_str()), ("collection", collection), ("rkey", &rkey)]; 40 let get_res = client 41 .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 42 .query(&params) 43 .send() 44 .await 45 .expect("Failed to send get request"); 46 assert_eq!(get_res.status(), StatusCode::OK, "Failed to get record after create"); 47 let get_body: Value = get_res.json().await.expect("get response was not JSON"); 48 assert_eq!(get_body["uri"], uri); 49 assert_eq!(get_body["value"]["text"], original_text); 50 let updated_text = "This post has been updated."; 51 let update_payload = json!({ 52 "repo": did, 53 "collection": collection, 54 "rkey": rkey, 55 "record": { "$type": collection, "text": updated_text, "createdAt": now }, 56 "swapRecord": initial_cid 57 }); 58 let update_res = client 59 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 60 .bearer_auth(&jwt) 61 .json(&update_payload) 62 .send() 63 .await 64 .expect("Failed to send update request"); 65 assert_eq!(update_res.status(), StatusCode::OK, "Failed to update record"); 66 let update_body: Value = update_res.json().await.expect("update response was not JSON"); 67 let updated_cid = update_body["cid"].as_str().unwrap().to_string(); 68 let get_updated_res = client 69 .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 70 .query(&params) 71 .send() 72 .await 73 .expect("Failed to send get-after-update request"); 74 let get_updated_body: Value = get_updated_res.json().await.expect("get-updated response was not JSON"); 75 assert_eq!(get_updated_body["value"]["text"], updated_text, "Text was not updated"); 76 let stale_update_payload = json!({ 77 "repo": did, 78 "collection": collection, 79 "rkey": rkey, 80 "record": { "$type": collection, "text": "Stale update", "createdAt": now }, 81 "swapRecord": initial_cid 82 }); 83 let stale_res = client 84 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 85 .bearer_auth(&jwt) 86 .json(&stale_update_payload) 87 .send() 88 .await 89 .expect("Failed to send stale update"); 90 assert_eq!(stale_res.status(), StatusCode::CONFLICT, "Stale update should cause 409"); 91 let good_update_payload = json!({ 92 "repo": did, 93 "collection": collection, 94 "rkey": rkey, 95 "record": { "$type": collection, "text": "Good update", "createdAt": now }, 96 "swapRecord": updated_cid 97 }); 98 let good_res = client 99 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 100 .bearer_auth(&jwt) 101 .json(&good_update_payload) 102 .send() 103 .await 104 .expect("Failed to send good update"); 105 assert_eq!(good_res.status(), StatusCode::OK, "Good update should succeed"); 106 let delete_payload = json!({ "repo": did, "collection": collection, "rkey": rkey }); 107 let delete_res = client 108 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 109 .bearer_auth(&jwt) 110 .json(&delete_payload) 111 .send() 112 .await 113 .expect("Failed to send delete request"); 114 assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete record"); 115 let get_deleted_res = client 116 .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 117 .query(&params) 118 .send() 119 .await 120 .expect("Failed to send get-after-delete request"); 121 assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Record should be deleted"); 122} 123 124#[tokio::test] 125async fn test_profile_with_blob_lifecycle() { 126 let client = client(); 127 let (did, jwt) = setup_new_user("profile-blob").await; 128 let blob_data = b"This is test blob data for a profile avatar"; 129 let upload_res = client 130 .post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await)) 131 .header(header::CONTENT_TYPE, "text/plain") 132 .bearer_auth(&jwt) 133 .body(blob_data.to_vec()) 134 .send() 135 .await 136 .expect("Failed to upload blob"); 137 assert_eq!(upload_res.status(), StatusCode::OK); 138 let upload_body: Value = upload_res.json().await.unwrap(); 139 let blob_ref = upload_body["blob"].clone(); 140 let profile_payload = json!({ 141 "repo": did, 142 "collection": "app.bsky.actor.profile", 143 "rkey": "self", 144 "record": { 145 "$type": "app.bsky.actor.profile", 146 "displayName": "Test User", 147 "description": "A test profile for lifecycle testing", 148 "avatar": blob_ref 149 } 150 }); 151 let create_res = client 152 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 153 .bearer_auth(&jwt) 154 .json(&profile_payload) 155 .send() 156 .await 157 .expect("Failed to create profile"); 158 assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile"); 159 let create_body: Value = create_res.json().await.unwrap(); 160 let initial_cid = create_body["cid"].as_str().unwrap().to_string(); 161 let get_res = client 162 .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 163 .query(&[("repo", did.as_str()), ("collection", "app.bsky.actor.profile"), ("rkey", "self")]) 164 .send() 165 .await 166 .expect("Failed to get profile"); 167 assert_eq!(get_res.status(), StatusCode::OK); 168 let get_body: Value = get_res.json().await.unwrap(); 169 assert_eq!(get_body["value"]["displayName"], "Test User"); 170 assert!(get_body["value"]["avatar"]["ref"]["$link"].is_string()); 171 let update_payload = json!({ 172 "repo": did, 173 "collection": "app.bsky.actor.profile", 174 "rkey": "self", 175 "record": { "$type": "app.bsky.actor.profile", "displayName": "Updated User", "description": "Profile updated" }, 176 "swapRecord": initial_cid 177 }); 178 let update_res = client 179 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 180 .bearer_auth(&jwt) 181 .json(&update_payload) 182 .send() 183 .await 184 .expect("Failed to update profile"); 185 assert_eq!(update_res.status(), StatusCode::OK, "Failed to update profile"); 186 let get_updated_res = client 187 .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 188 .query(&[("repo", did.as_str()), ("collection", "app.bsky.actor.profile"), ("rkey", "self")]) 189 .send() 190 .await 191 .expect("Failed to get updated profile"); 192 let updated_body: Value = get_updated_res.json().await.unwrap(); 193 assert_eq!(updated_body["value"]["displayName"], "Updated User"); 194} 195 196#[tokio::test] 197async fn test_reply_thread_lifecycle() { 198 let client = client(); 199 let (alice_did, alice_jwt) = setup_new_user("alice-thread").await; 200 let (bob_did, bob_jwt) = setup_new_user("bob-thread").await; 201 let (root_uri, root_cid) = create_post(&client, &alice_did, &alice_jwt, "This is the root post").await; 202 tokio::time::sleep(Duration::from_millis(100)).await; 203 let reply_collection = "app.bsky.feed.post"; 204 let reply_rkey = format!("e2e_reply_{}", Utc::now().timestamp_millis()); 205 let reply_payload = json!({ 206 "repo": bob_did, 207 "collection": reply_collection, 208 "rkey": reply_rkey, 209 "record": { 210 "$type": reply_collection, 211 "text": "This is Bob's reply to Alice", 212 "createdAt": Utc::now().to_rfc3339(), 213 "reply": { 214 "root": { "uri": root_uri, "cid": root_cid }, 215 "parent": { "uri": root_uri, "cid": root_cid } 216 } 217 } 218 }); 219 let reply_res = client 220 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 221 .bearer_auth(&bob_jwt) 222 .json(&reply_payload) 223 .send() 224 .await 225 .expect("Failed to create reply"); 226 assert_eq!(reply_res.status(), StatusCode::OK, "Failed to create reply"); 227 let reply_body: Value = reply_res.json().await.unwrap(); 228 let reply_uri = reply_body["uri"].as_str().unwrap(); 229 let reply_cid = reply_body["cid"].as_str().unwrap(); 230 let get_reply_res = client 231 .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 232 .query(&[("repo", bob_did.as_str()), ("collection", reply_collection), ("rkey", reply_rkey.as_str())]) 233 .send() 234 .await 235 .expect("Failed to get reply"); 236 assert_eq!(get_reply_res.status(), StatusCode::OK); 237 let reply_record: Value = get_reply_res.json().await.unwrap(); 238 assert_eq!(reply_record["value"]["reply"]["root"]["uri"], root_uri); 239 tokio::time::sleep(Duration::from_millis(100)).await; 240 let nested_reply_rkey = format!("e2e_nested_reply_{}", Utc::now().timestamp_millis()); 241 let nested_payload = json!({ 242 "repo": alice_did, 243 "collection": reply_collection, 244 "rkey": nested_reply_rkey, 245 "record": { 246 "$type": reply_collection, 247 "text": "Alice replies to Bob's reply", 248 "createdAt": Utc::now().to_rfc3339(), 249 "reply": { 250 "root": { "uri": root_uri, "cid": root_cid }, 251 "parent": { "uri": reply_uri, "cid": reply_cid } 252 } 253 } 254 }); 255 let nested_res = client 256 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 257 .bearer_auth(&alice_jwt) 258 .json(&nested_payload) 259 .send() 260 .await 261 .expect("Failed to create nested reply"); 262 assert_eq!(nested_res.status(), StatusCode::OK, "Failed to create nested reply"); 263} 264 265#[tokio::test] 266async fn test_authorization_protects_repos() { 267 let client = client(); 268 let (alice_did, alice_jwt) = setup_new_user("alice-auth").await; 269 let (_bob_did, bob_jwt) = setup_new_user("bob-auth").await; 270 let (post_uri, _) = create_post(&client, &alice_did, &alice_jwt, "Alice's post").await; 271 let post_rkey = post_uri.split('/').last().unwrap(); 272 let post_payload = json!({ 273 "repo": alice_did, 274 "collection": "app.bsky.feed.post", 275 "rkey": "unauthorized-post", 276 "record": { "$type": "app.bsky.feed.post", "text": "Bob trying to post as Alice", "createdAt": Utc::now().to_rfc3339() } 277 }); 278 let write_res = client 279 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 280 .bearer_auth(&bob_jwt) 281 .json(&post_payload) 282 .send() 283 .await 284 .expect("Failed to send request"); 285 assert!(write_res.status() == StatusCode::FORBIDDEN || write_res.status() == StatusCode::UNAUTHORIZED, 286 "Expected 403/401 for writing to another user's repo, got {}", write_res.status()); 287 let delete_payload = json!({ "repo": alice_did, "collection": "app.bsky.feed.post", "rkey": post_rkey }); 288 let delete_res = client 289 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 290 .bearer_auth(&bob_jwt) 291 .json(&delete_payload) 292 .send() 293 .await 294 .expect("Failed to send request"); 295 assert!(delete_res.status() == StatusCode::FORBIDDEN || delete_res.status() == StatusCode::UNAUTHORIZED, 296 "Expected 403/401 for deleting another user's record, got {}", delete_res.status()); 297 let get_res = client 298 .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 299 .query(&[("repo", alice_did.as_str()), ("collection", "app.bsky.feed.post"), ("rkey", post_rkey)]) 300 .send() 301 .await 302 .expect("Failed to verify record exists"); 303 assert_eq!(get_res.status(), StatusCode::OK, "Record should still exist"); 304} 305 306#[tokio::test] 307async fn test_apply_writes_batch() { 308 let client = client(); 309 let (did, jwt) = setup_new_user("apply-writes-batch").await; 310 let now = Utc::now().to_rfc3339(); 311 let writes_payload = json!({ 312 "repo": did, 313 "writes": [ 314 { "$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 } }, 315 { "$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 } }, 316 { "$type": "com.atproto.repo.applyWrites#create", "collection": "app.bsky.actor.profile", "rkey": "self", "value": { "$type": "app.bsky.actor.profile", "displayName": "Batch User" } } 317 ] 318 }); 319 let apply_res = client 320 .post(format!("{}/xrpc/com.atproto.repo.applyWrites", base_url().await)) 321 .bearer_auth(&jwt) 322 .json(&writes_payload) 323 .send() 324 .await 325 .expect("Failed to apply writes"); 326 assert_eq!(apply_res.status(), StatusCode::OK); 327 let get_post1 = client 328 .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 329 .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("rkey", "batch-post-1")]) 330 .send().await.expect("Failed to get post 1"); 331 assert_eq!(get_post1.status(), StatusCode::OK); 332 let post1_body: Value = get_post1.json().await.unwrap(); 333 assert_eq!(post1_body["value"]["text"], "First batch post"); 334 let get_post2 = client 335 .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 336 .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("rkey", "batch-post-2")]) 337 .send().await.expect("Failed to get post 2"); 338 assert_eq!(get_post2.status(), StatusCode::OK); 339 let get_profile = client 340 .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 341 .query(&[("repo", did.as_str()), ("collection", "app.bsky.actor.profile"), ("rkey", "self")]) 342 .send().await.expect("Failed to get profile"); 343 let profile_body: Value = get_profile.json().await.unwrap(); 344 assert_eq!(profile_body["value"]["displayName"], "Batch User"); 345 let update_writes = json!({ 346 "repo": did, 347 "writes": [ 348 { "$type": "com.atproto.repo.applyWrites#update", "collection": "app.bsky.actor.profile", "rkey": "self", "value": { "$type": "app.bsky.actor.profile", "displayName": "Updated Batch User" } }, 349 { "$type": "com.atproto.repo.applyWrites#delete", "collection": "app.bsky.feed.post", "rkey": "batch-post-1" } 350 ] 351 }); 352 let update_res = client 353 .post(format!("{}/xrpc/com.atproto.repo.applyWrites", base_url().await)) 354 .bearer_auth(&jwt) 355 .json(&update_writes) 356 .send() 357 .await 358 .expect("Failed to apply update writes"); 359 assert_eq!(update_res.status(), StatusCode::OK); 360 let get_updated_profile = client 361 .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 362 .query(&[("repo", did.as_str()), ("collection", "app.bsky.actor.profile"), ("rkey", "self")]) 363 .send().await.expect("Failed to get updated profile"); 364 let updated_profile: Value = get_updated_profile.json().await.unwrap(); 365 assert_eq!(updated_profile["value"]["displayName"], "Updated Batch User"); 366 let get_deleted_post = client 367 .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 368 .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("rkey", "batch-post-1")]) 369 .send().await.expect("Failed to check deleted post"); 370 assert_eq!(get_deleted_post.status(), StatusCode::NOT_FOUND, "Batch-deleted post should be gone"); 371} 372 373async fn create_post_with_rkey(client: &reqwest::Client, did: &str, jwt: &str, rkey: &str, text: &str) -> (String, String) { 374 let payload = json!({ 375 "repo": did, "collection": "app.bsky.feed.post", "rkey": rkey, 376 "record": { "$type": "app.bsky.feed.post", "text": text, "createdAt": Utc::now().to_rfc3339() } 377 }); 378 let res = client 379 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 380 .bearer_auth(jwt) 381 .json(&payload) 382 .send() 383 .await 384 .expect("Failed to create record"); 385 assert_eq!(res.status(), StatusCode::OK); 386 let body: Value = res.json().await.unwrap(); 387 (body["uri"].as_str().unwrap().to_string(), body["cid"].as_str().unwrap().to_string()) 388} 389 390#[tokio::test] 391async fn test_list_records_comprehensive() { 392 let client = client(); 393 let (did, jwt) = setup_new_user("list-records-test").await; 394 for i in 0..5 { 395 create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await; 396 tokio::time::sleep(Duration::from_millis(50)).await; 397 } 398 let res = client 399 .get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await)) 400 .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post")]) 401 .send().await.expect("Failed to list records"); 402 assert_eq!(res.status(), StatusCode::OK); 403 let body: Value = res.json().await.unwrap(); 404 let records = body["records"].as_array().unwrap(); 405 assert_eq!(records.len(), 5); 406 let rkeys: Vec<&str> = records.iter().map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()).collect(); 407 assert_eq!(rkeys, vec!["post04", "post03", "post02", "post01", "post00"], "Default order should be DESC"); 408 for record in records { 409 assert!(record["uri"].is_string()); 410 assert!(record["cid"].is_string()); 411 assert!(record["cid"].as_str().unwrap().starts_with("bafy")); 412 assert!(record["value"].is_object()); 413 } 414 let rev_res = client 415 .get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await)) 416 .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("reverse", "true")]) 417 .send().await.expect("Failed to list records reverse"); 418 let rev_body: Value = rev_res.json().await.unwrap(); 419 let rev_rkeys: Vec<&str> = rev_body["records"].as_array().unwrap().iter() 420 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()).collect(); 421 assert_eq!(rev_rkeys, vec!["post00", "post01", "post02", "post03", "post04"], "reverse=true should give ASC"); 422 let page1 = client 423 .get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await)) 424 .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("limit", "2")]) 425 .send().await.expect("Failed to list page 1"); 426 let page1_body: Value = page1.json().await.unwrap(); 427 let page1_records = page1_body["records"].as_array().unwrap(); 428 assert_eq!(page1_records.len(), 2); 429 let cursor = page1_body["cursor"].as_str().expect("Should have cursor"); 430 let page2 = client 431 .get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await)) 432 .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("limit", "2"), ("cursor", cursor)]) 433 .send().await.expect("Failed to list page 2"); 434 let page2_body: Value = page2.json().await.unwrap(); 435 let page2_records = page2_body["records"].as_array().unwrap(); 436 assert_eq!(page2_records.len(), 2); 437 let all_uris: Vec<&str> = page1_records.iter().chain(page2_records.iter()) 438 .map(|r| r["uri"].as_str().unwrap()).collect(); 439 let unique_uris: std::collections::HashSet<&str> = all_uris.iter().copied().collect(); 440 assert_eq!(all_uris.len(), unique_uris.len(), "Cursor pagination should not repeat records"); 441 let range_res = client 442 .get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await)) 443 .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post"), 444 ("rkeyStart", "post01"), ("rkeyEnd", "post03"), ("reverse", "true")]) 445 .send().await.expect("Failed to list range"); 446 let range_body: Value = range_res.json().await.unwrap(); 447 let range_rkeys: Vec<&str> = range_body["records"].as_array().unwrap().iter() 448 .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()).collect(); 449 for rkey in &range_rkeys { 450 assert!(*rkey >= "post01" && *rkey <= "post03", "Range should be inclusive"); 451 } 452 let limit_res = client 453 .get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await)) 454 .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("limit", "1000")]) 455 .send().await.expect("Failed with high limit"); 456 let limit_body: Value = limit_res.json().await.unwrap(); 457 assert!(limit_body["records"].as_array().unwrap().len() <= 100, "Limit should be clamped to max 100"); 458 let not_found_res = client 459 .get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await)) 460 .query(&[("repo", "did:plc:nonexistent12345"), ("collection", "app.bsky.feed.post")]) 461 .send().await.expect("Failed with nonexistent repo"); 462 assert_eq!(not_found_res.status(), StatusCode::NOT_FOUND); 463}