this repo has no description
1mod common; 2mod helpers; 3use chrono::Utc; 4use common::*; 5use helpers::*; 6use reqwest::StatusCode; 7use serde_json::{Value, json}; 8 9#[tokio::test] 10async fn test_create_record_response_schema() { 11 let client = client(); 12 let (did, jwt) = setup_new_user("conform-create").await; 13 let now = Utc::now().to_rfc3339(); 14 15 let payload = json!({ 16 "repo": did, 17 "collection": "app.bsky.feed.post", 18 "record": { 19 "$type": "app.bsky.feed.post", 20 "text": "Testing conformance", 21 "createdAt": now 22 } 23 }); 24 25 let res = client 26 .post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) 27 .bearer_auth(&jwt) 28 .json(&payload) 29 .send() 30 .await 31 .expect("Failed to create record"); 32 33 assert_eq!(res.status(), StatusCode::OK); 34 let body: Value = res.json().await.unwrap(); 35 36 assert!(body["uri"].is_string(), "response must have uri"); 37 assert!(body["cid"].is_string(), "response must have cid"); 38 assert!(body["cid"].as_str().unwrap().starts_with("bafy"), "cid must be valid"); 39 40 assert!(body["commit"].is_object(), "response must have commit object"); 41 let commit = &body["commit"]; 42 assert!(commit["cid"].is_string(), "commit must have cid"); 43 assert!(commit["cid"].as_str().unwrap().starts_with("bafy"), "commit.cid must be valid"); 44 assert!(commit["rev"].is_string(), "commit must have rev"); 45 46 assert!(body["validationStatus"].is_string(), "response must have validationStatus when validate defaults to true"); 47 assert_eq!(body["validationStatus"], "valid", "validationStatus should be 'valid'"); 48} 49 50#[tokio::test] 51async fn test_create_record_no_validation_status_when_validate_false() { 52 let client = client(); 53 let (did, jwt) = setup_new_user("conform-create-noval").await; 54 let now = Utc::now().to_rfc3339(); 55 56 let payload = json!({ 57 "repo": did, 58 "collection": "app.bsky.feed.post", 59 "validate": false, 60 "record": { 61 "$type": "app.bsky.feed.post", 62 "text": "Testing without validation", 63 "createdAt": now 64 } 65 }); 66 67 let res = client 68 .post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) 69 .bearer_auth(&jwt) 70 .json(&payload) 71 .send() 72 .await 73 .expect("Failed to create record"); 74 75 assert_eq!(res.status(), StatusCode::OK); 76 let body: Value = res.json().await.unwrap(); 77 78 assert!(body["uri"].is_string()); 79 assert!(body["commit"].is_object()); 80 assert!(body["validationStatus"].is_null(), "validationStatus should be omitted when validate=false"); 81} 82 83#[tokio::test] 84async fn test_put_record_response_schema() { 85 let client = client(); 86 let (did, jwt) = setup_new_user("conform-put").await; 87 let now = Utc::now().to_rfc3339(); 88 89 let payload = json!({ 90 "repo": did, 91 "collection": "app.bsky.feed.post", 92 "rkey": "conformance-put", 93 "record": { 94 "$type": "app.bsky.feed.post", 95 "text": "Testing putRecord conformance", 96 "createdAt": now 97 } 98 }); 99 100 let res = client 101 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 102 .bearer_auth(&jwt) 103 .json(&payload) 104 .send() 105 .await 106 .expect("Failed to put record"); 107 108 assert_eq!(res.status(), StatusCode::OK); 109 let body: Value = res.json().await.unwrap(); 110 111 assert!(body["uri"].is_string(), "response must have uri"); 112 assert!(body["cid"].is_string(), "response must have cid"); 113 114 assert!(body["commit"].is_object(), "response must have commit object"); 115 let commit = &body["commit"]; 116 assert!(commit["cid"].is_string(), "commit must have cid"); 117 assert!(commit["rev"].is_string(), "commit must have rev"); 118 119 assert_eq!(body["validationStatus"], "valid", "validationStatus should be 'valid'"); 120} 121 122#[tokio::test] 123async fn test_delete_record_response_schema() { 124 let client = client(); 125 let (did, jwt) = setup_new_user("conform-delete").await; 126 let now = Utc::now().to_rfc3339(); 127 128 let create_payload = json!({ 129 "repo": did, 130 "collection": "app.bsky.feed.post", 131 "rkey": "to-delete", 132 "record": { 133 "$type": "app.bsky.feed.post", 134 "text": "This will be deleted", 135 "createdAt": now 136 } 137 }); 138 let create_res = client 139 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 140 .bearer_auth(&jwt) 141 .json(&create_payload) 142 .send() 143 .await 144 .expect("Failed to create record"); 145 assert_eq!(create_res.status(), StatusCode::OK); 146 147 let delete_payload = json!({ 148 "repo": did, 149 "collection": "app.bsky.feed.post", 150 "rkey": "to-delete" 151 }); 152 let delete_res = client 153 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 154 .bearer_auth(&jwt) 155 .json(&delete_payload) 156 .send() 157 .await 158 .expect("Failed to delete record"); 159 160 assert_eq!(delete_res.status(), StatusCode::OK); 161 let body: Value = delete_res.json().await.unwrap(); 162 163 assert!(body["commit"].is_object(), "response must have commit object when record was deleted"); 164 let commit = &body["commit"]; 165 assert!(commit["cid"].is_string(), "commit must have cid"); 166 assert!(commit["rev"].is_string(), "commit must have rev"); 167} 168 169#[tokio::test] 170async fn test_delete_record_noop_response() { 171 let client = client(); 172 let (did, jwt) = setup_new_user("conform-delete-noop").await; 173 174 let delete_payload = json!({ 175 "repo": did, 176 "collection": "app.bsky.feed.post", 177 "rkey": "nonexistent-record" 178 }); 179 let delete_res = client 180 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await)) 181 .bearer_auth(&jwt) 182 .json(&delete_payload) 183 .send() 184 .await 185 .expect("Failed to delete record"); 186 187 assert_eq!(delete_res.status(), StatusCode::OK); 188 let body: Value = delete_res.json().await.unwrap(); 189 190 assert!(body["commit"].is_null(), "commit should be omitted on no-op delete"); 191} 192 193#[tokio::test] 194async fn test_apply_writes_response_schema() { 195 let client = client(); 196 let (did, jwt) = setup_new_user("conform-apply").await; 197 let now = Utc::now().to_rfc3339(); 198 199 let payload = json!({ 200 "repo": did, 201 "writes": [ 202 { 203 "$type": "com.atproto.repo.applyWrites#create", 204 "collection": "app.bsky.feed.post", 205 "rkey": "apply-test-1", 206 "value": { 207 "$type": "app.bsky.feed.post", 208 "text": "First post", 209 "createdAt": now 210 } 211 }, 212 { 213 "$type": "com.atproto.repo.applyWrites#create", 214 "collection": "app.bsky.feed.post", 215 "rkey": "apply-test-2", 216 "value": { 217 "$type": "app.bsky.feed.post", 218 "text": "Second post", 219 "createdAt": now 220 } 221 } 222 ] 223 }); 224 225 let res = client 226 .post(format!("{}/xrpc/com.atproto.repo.applyWrites", base_url().await)) 227 .bearer_auth(&jwt) 228 .json(&payload) 229 .send() 230 .await 231 .expect("Failed to apply writes"); 232 233 assert_eq!(res.status(), StatusCode::OK); 234 let body: Value = res.json().await.unwrap(); 235 236 assert!(body["commit"].is_object(), "response must have commit object"); 237 let commit = &body["commit"]; 238 assert!(commit["cid"].is_string(), "commit must have cid"); 239 assert!(commit["rev"].is_string(), "commit must have rev"); 240 241 assert!(body["results"].is_array(), "response must have results array"); 242 let results = body["results"].as_array().unwrap(); 243 assert_eq!(results.len(), 2, "should have 2 results"); 244 245 for result in results { 246 assert!(result["uri"].is_string(), "result must have uri"); 247 assert!(result["cid"].is_string(), "result must have cid"); 248 assert_eq!(result["validationStatus"], "valid", "result must have validationStatus"); 249 assert_eq!(result["$type"], "com.atproto.repo.applyWrites#createResult"); 250 } 251} 252 253#[tokio::test] 254async fn test_apply_writes_update_and_delete_results() { 255 let client = client(); 256 let (did, jwt) = setup_new_user("conform-apply-upd").await; 257 let now = Utc::now().to_rfc3339(); 258 259 let create_payload = json!({ 260 "repo": did, 261 "collection": "app.bsky.feed.post", 262 "rkey": "to-update", 263 "record": { 264 "$type": "app.bsky.feed.post", 265 "text": "Original", 266 "createdAt": now 267 } 268 }); 269 client 270 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 271 .bearer_auth(&jwt) 272 .json(&create_payload) 273 .send() 274 .await 275 .expect("setup failed"); 276 277 let payload = json!({ 278 "repo": did, 279 "writes": [ 280 { 281 "$type": "com.atproto.repo.applyWrites#update", 282 "collection": "app.bsky.feed.post", 283 "rkey": "to-update", 284 "value": { 285 "$type": "app.bsky.feed.post", 286 "text": "Updated", 287 "createdAt": now 288 } 289 }, 290 { 291 "$type": "com.atproto.repo.applyWrites#delete", 292 "collection": "app.bsky.feed.post", 293 "rkey": "to-update" 294 } 295 ] 296 }); 297 298 let res = client 299 .post(format!("{}/xrpc/com.atproto.repo.applyWrites", base_url().await)) 300 .bearer_auth(&jwt) 301 .json(&payload) 302 .send() 303 .await 304 .expect("Failed to apply writes"); 305 306 assert_eq!(res.status(), StatusCode::OK); 307 let body: Value = res.json().await.unwrap(); 308 309 let results = body["results"].as_array().unwrap(); 310 assert_eq!(results.len(), 2); 311 312 let update_result = &results[0]; 313 assert_eq!(update_result["$type"], "com.atproto.repo.applyWrites#updateResult"); 314 assert!(update_result["uri"].is_string()); 315 assert!(update_result["cid"].is_string()); 316 assert_eq!(update_result["validationStatus"], "valid"); 317 318 let delete_result = &results[1]; 319 assert_eq!(delete_result["$type"], "com.atproto.repo.applyWrites#deleteResult"); 320 assert!(delete_result["uri"].is_null(), "delete result should not have uri"); 321 assert!(delete_result["cid"].is_null(), "delete result should not have cid"); 322 assert!(delete_result["validationStatus"].is_null(), "delete result should not have validationStatus"); 323} 324 325#[tokio::test] 326async fn test_get_record_error_code() { 327 let client = client(); 328 let (did, _jwt) = setup_new_user("conform-get-err").await; 329 330 let res = client 331 .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await)) 332 .query(&[ 333 ("repo", did.as_str()), 334 ("collection", "app.bsky.feed.post"), 335 ("rkey", "nonexistent"), 336 ]) 337 .send() 338 .await 339 .expect("Failed to get record"); 340 341 assert_eq!(res.status(), StatusCode::NOT_FOUND); 342 let body: Value = res.json().await.unwrap(); 343 assert_eq!(body["error"], "RecordNotFound", "error code should be RecordNotFound per atproto spec"); 344} 345 346#[tokio::test] 347async fn test_create_record_unknown_lexicon_default_validation() { 348 let client = client(); 349 let (did, jwt) = setup_new_user("conform-unknown-lex").await; 350 351 let payload = json!({ 352 "repo": did, 353 "collection": "com.example.custom", 354 "record": { 355 "$type": "com.example.custom", 356 "data": "some custom data" 357 } 358 }); 359 360 let res = client 361 .post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) 362 .bearer_auth(&jwt) 363 .json(&payload) 364 .send() 365 .await 366 .expect("Failed to create record"); 367 368 assert_eq!(res.status(), StatusCode::OK, "unknown lexicon should be allowed with default validation"); 369 let body: Value = res.json().await.unwrap(); 370 371 assert!(body["uri"].is_string()); 372 assert!(body["cid"].is_string()); 373 assert!(body["commit"].is_object()); 374 assert_eq!(body["validationStatus"], "unknown", "validationStatus should be 'unknown' for unknown lexicons"); 375} 376 377#[tokio::test] 378async fn test_create_record_unknown_lexicon_strict_validation() { 379 let client = client(); 380 let (did, jwt) = setup_new_user("conform-unknown-strict").await; 381 382 let payload = json!({ 383 "repo": did, 384 "collection": "com.example.custom", 385 "validate": true, 386 "record": { 387 "$type": "com.example.custom", 388 "data": "some custom data" 389 } 390 }); 391 392 let res = client 393 .post(format!("{}/xrpc/com.atproto.repo.createRecord", base_url().await)) 394 .bearer_auth(&jwt) 395 .json(&payload) 396 .send() 397 .await 398 .expect("Failed to send request"); 399 400 assert_eq!(res.status(), StatusCode::BAD_REQUEST, "unknown lexicon should fail with validate=true"); 401 let body: Value = res.json().await.unwrap(); 402 assert_eq!(body["error"], "InvalidRecord"); 403 assert!(body["message"].as_str().unwrap().contains("Lexicon not found"), "error should mention lexicon not found"); 404} 405 406#[tokio::test] 407async fn test_put_record_noop_same_content() { 408 let client = client(); 409 let (did, jwt) = setup_new_user("conform-put-noop").await; 410 let now = Utc::now().to_rfc3339(); 411 412 let record = json!({ 413 "$type": "app.bsky.feed.post", 414 "text": "This content will not change", 415 "createdAt": now 416 }); 417 418 let payload = json!({ 419 "repo": did, 420 "collection": "app.bsky.feed.post", 421 "rkey": "noop-test", 422 "record": record.clone() 423 }); 424 425 let first_res = client 426 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 427 .bearer_auth(&jwt) 428 .json(&payload) 429 .send() 430 .await 431 .expect("Failed to put record"); 432 assert_eq!(first_res.status(), StatusCode::OK); 433 let first_body: Value = first_res.json().await.unwrap(); 434 assert!(first_body["commit"].is_object(), "first put should have commit"); 435 436 let second_res = client 437 .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await)) 438 .bearer_auth(&jwt) 439 .json(&payload) 440 .send() 441 .await 442 .expect("Failed to put record"); 443 assert_eq!(second_res.status(), StatusCode::OK); 444 let second_body: Value = second_res.json().await.unwrap(); 445 446 assert!(second_body["commit"].is_null(), "second put with same content should have no commit (no-op)"); 447 assert_eq!(first_body["cid"], second_body["cid"], "CID should be the same for identical content"); 448} 449 450#[tokio::test] 451async fn test_apply_writes_unknown_lexicon() { 452 let client = client(); 453 let (did, jwt) = setup_new_user("conform-apply-unknown").await; 454 455 let payload = json!({ 456 "repo": did, 457 "writes": [ 458 { 459 "$type": "com.atproto.repo.applyWrites#create", 460 "collection": "com.example.custom", 461 "rkey": "custom-1", 462 "value": { 463 "$type": "com.example.custom", 464 "data": "custom data" 465 } 466 } 467 ] 468 }); 469 470 let res = client 471 .post(format!("{}/xrpc/com.atproto.repo.applyWrites", base_url().await)) 472 .bearer_auth(&jwt) 473 .json(&payload) 474 .send() 475 .await 476 .expect("Failed to apply writes"); 477 478 assert_eq!(res.status(), StatusCode::OK); 479 let body: Value = res.json().await.unwrap(); 480 481 let results = body["results"].as_array().unwrap(); 482 assert_eq!(results.len(), 1); 483 assert_eq!(results[0]["validationStatus"], "unknown", "unknown lexicon should have 'unknown' status"); 484}