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!( 27 "{}/xrpc/com.atproto.repo.createRecord", 28 base_url().await 29 )) 30 .bearer_auth(&jwt) 31 .json(&payload) 32 .send() 33 .await 34 .expect("Failed to create record"); 35 36 assert_eq!(res.status(), StatusCode::OK); 37 let body: Value = res.json().await.unwrap(); 38 39 assert!(body["uri"].is_string(), "response must have uri"); 40 assert!(body["cid"].is_string(), "response must have cid"); 41 assert!( 42 body["cid"].as_str().unwrap().starts_with("bafy"), 43 "cid must be valid" 44 ); 45 46 assert!( 47 body["commit"].is_object(), 48 "response must have commit object" 49 ); 50 let commit = &body["commit"]; 51 assert!(commit["cid"].is_string(), "commit must have cid"); 52 assert!( 53 commit["cid"].as_str().unwrap().starts_with("bafy"), 54 "commit.cid must be valid" 55 ); 56 assert!(commit["rev"].is_string(), "commit must have rev"); 57 58 assert!( 59 body["validationStatus"].is_string(), 60 "response must have validationStatus when validate defaults to true" 61 ); 62 assert_eq!( 63 body["validationStatus"], "valid", 64 "validationStatus should be 'valid'" 65 ); 66} 67 68#[tokio::test] 69async fn test_create_record_no_validation_status_when_validate_false() { 70 let client = client(); 71 let (did, jwt) = setup_new_user("conform-create-noval").await; 72 let now = Utc::now().to_rfc3339(); 73 74 let payload = json!({ 75 "repo": did, 76 "collection": "app.bsky.feed.post", 77 "validate": false, 78 "record": { 79 "$type": "app.bsky.feed.post", 80 "text": "Testing without validation", 81 "createdAt": now 82 } 83 }); 84 85 let res = client 86 .post(format!( 87 "{}/xrpc/com.atproto.repo.createRecord", 88 base_url().await 89 )) 90 .bearer_auth(&jwt) 91 .json(&payload) 92 .send() 93 .await 94 .expect("Failed to create record"); 95 96 assert_eq!(res.status(), StatusCode::OK); 97 let body: Value = res.json().await.unwrap(); 98 99 assert!(body["uri"].is_string()); 100 assert!(body["commit"].is_object()); 101 assert!( 102 body["validationStatus"].is_null(), 103 "validationStatus should be omitted when validate=false" 104 ); 105} 106 107#[tokio::test] 108async fn test_put_record_response_schema() { 109 let client = client(); 110 let (did, jwt) = setup_new_user("conform-put").await; 111 let now = Utc::now().to_rfc3339(); 112 113 let payload = json!({ 114 "repo": did, 115 "collection": "app.bsky.feed.post", 116 "rkey": "conformance-put", 117 "record": { 118 "$type": "app.bsky.feed.post", 119 "text": "Testing putRecord conformance", 120 "createdAt": now 121 } 122 }); 123 124 let res = client 125 .post(format!( 126 "{}/xrpc/com.atproto.repo.putRecord", 127 base_url().await 128 )) 129 .bearer_auth(&jwt) 130 .json(&payload) 131 .send() 132 .await 133 .expect("Failed to put record"); 134 135 assert_eq!(res.status(), StatusCode::OK); 136 let body: Value = res.json().await.unwrap(); 137 138 assert!(body["uri"].is_string(), "response must have uri"); 139 assert!(body["cid"].is_string(), "response must have cid"); 140 141 assert!( 142 body["commit"].is_object(), 143 "response must have commit object" 144 ); 145 let commit = &body["commit"]; 146 assert!(commit["cid"].is_string(), "commit must have cid"); 147 assert!(commit["rev"].is_string(), "commit must have rev"); 148 149 assert_eq!( 150 body["validationStatus"], "valid", 151 "validationStatus should be 'valid'" 152 ); 153} 154 155#[tokio::test] 156async fn test_delete_record_response_schema() { 157 let client = client(); 158 let (did, jwt) = setup_new_user("conform-delete").await; 159 let now = Utc::now().to_rfc3339(); 160 161 let create_payload = json!({ 162 "repo": did, 163 "collection": "app.bsky.feed.post", 164 "rkey": "to-delete", 165 "record": { 166 "$type": "app.bsky.feed.post", 167 "text": "This will be deleted", 168 "createdAt": now 169 } 170 }); 171 let create_res = client 172 .post(format!( 173 "{}/xrpc/com.atproto.repo.putRecord", 174 base_url().await 175 )) 176 .bearer_auth(&jwt) 177 .json(&create_payload) 178 .send() 179 .await 180 .expect("Failed to create record"); 181 assert_eq!(create_res.status(), StatusCode::OK); 182 183 let delete_payload = json!({ 184 "repo": did, 185 "collection": "app.bsky.feed.post", 186 "rkey": "to-delete" 187 }); 188 let delete_res = client 189 .post(format!( 190 "{}/xrpc/com.atproto.repo.deleteRecord", 191 base_url().await 192 )) 193 .bearer_auth(&jwt) 194 .json(&delete_payload) 195 .send() 196 .await 197 .expect("Failed to delete record"); 198 199 assert_eq!(delete_res.status(), StatusCode::OK); 200 let body: Value = delete_res.json().await.unwrap(); 201 202 assert!( 203 body["commit"].is_object(), 204 "response must have commit object when record was deleted" 205 ); 206 let commit = &body["commit"]; 207 assert!(commit["cid"].is_string(), "commit must have cid"); 208 assert!(commit["rev"].is_string(), "commit must have rev"); 209} 210 211#[tokio::test] 212async fn test_delete_record_noop_response() { 213 let client = client(); 214 let (did, jwt) = setup_new_user("conform-delete-noop").await; 215 216 let delete_payload = json!({ 217 "repo": did, 218 "collection": "app.bsky.feed.post", 219 "rkey": "nonexistent-record" 220 }); 221 let delete_res = client 222 .post(format!( 223 "{}/xrpc/com.atproto.repo.deleteRecord", 224 base_url().await 225 )) 226 .bearer_auth(&jwt) 227 .json(&delete_payload) 228 .send() 229 .await 230 .expect("Failed to delete record"); 231 232 assert_eq!(delete_res.status(), StatusCode::OK); 233 let body: Value = delete_res.json().await.unwrap(); 234 235 assert!( 236 body["commit"].is_null(), 237 "commit should be omitted on no-op delete" 238 ); 239} 240 241#[tokio::test] 242async fn test_apply_writes_response_schema() { 243 let client = client(); 244 let (did, jwt) = setup_new_user("conform-apply").await; 245 let now = Utc::now().to_rfc3339(); 246 247 let payload = json!({ 248 "repo": did, 249 "writes": [ 250 { 251 "$type": "com.atproto.repo.applyWrites#create", 252 "collection": "app.bsky.feed.post", 253 "rkey": "apply-test-1", 254 "value": { 255 "$type": "app.bsky.feed.post", 256 "text": "First post", 257 "createdAt": now 258 } 259 }, 260 { 261 "$type": "com.atproto.repo.applyWrites#create", 262 "collection": "app.bsky.feed.post", 263 "rkey": "apply-test-2", 264 "value": { 265 "$type": "app.bsky.feed.post", 266 "text": "Second post", 267 "createdAt": now 268 } 269 } 270 ] 271 }); 272 273 let res = client 274 .post(format!( 275 "{}/xrpc/com.atproto.repo.applyWrites", 276 base_url().await 277 )) 278 .bearer_auth(&jwt) 279 .json(&payload) 280 .send() 281 .await 282 .expect("Failed to apply writes"); 283 284 assert_eq!(res.status(), StatusCode::OK); 285 let body: Value = res.json().await.unwrap(); 286 287 assert!( 288 body["commit"].is_object(), 289 "response must have commit object" 290 ); 291 let commit = &body["commit"]; 292 assert!(commit["cid"].is_string(), "commit must have cid"); 293 assert!(commit["rev"].is_string(), "commit must have rev"); 294 295 assert!( 296 body["results"].is_array(), 297 "response must have results array" 298 ); 299 let results = body["results"].as_array().unwrap(); 300 assert_eq!(results.len(), 2, "should have 2 results"); 301 302 for result in results { 303 assert!(result["uri"].is_string(), "result must have uri"); 304 assert!(result["cid"].is_string(), "result must have cid"); 305 assert_eq!( 306 result["validationStatus"], "valid", 307 "result must have validationStatus" 308 ); 309 assert_eq!(result["$type"], "com.atproto.repo.applyWrites#createResult"); 310 } 311} 312 313#[tokio::test] 314async fn test_apply_writes_update_and_delete_results() { 315 let client = client(); 316 let (did, jwt) = setup_new_user("conform-apply-upd").await; 317 let now = Utc::now().to_rfc3339(); 318 319 let create_payload = json!({ 320 "repo": did, 321 "collection": "app.bsky.feed.post", 322 "rkey": "to-update", 323 "record": { 324 "$type": "app.bsky.feed.post", 325 "text": "Original", 326 "createdAt": now 327 } 328 }); 329 client 330 .post(format!( 331 "{}/xrpc/com.atproto.repo.putRecord", 332 base_url().await 333 )) 334 .bearer_auth(&jwt) 335 .json(&create_payload) 336 .send() 337 .await 338 .expect("setup failed"); 339 340 let payload = json!({ 341 "repo": did, 342 "writes": [ 343 { 344 "$type": "com.atproto.repo.applyWrites#update", 345 "collection": "app.bsky.feed.post", 346 "rkey": "to-update", 347 "value": { 348 "$type": "app.bsky.feed.post", 349 "text": "Updated", 350 "createdAt": now 351 } 352 }, 353 { 354 "$type": "com.atproto.repo.applyWrites#delete", 355 "collection": "app.bsky.feed.post", 356 "rkey": "to-update" 357 } 358 ] 359 }); 360 361 let res = client 362 .post(format!( 363 "{}/xrpc/com.atproto.repo.applyWrites", 364 base_url().await 365 )) 366 .bearer_auth(&jwt) 367 .json(&payload) 368 .send() 369 .await 370 .expect("Failed to apply writes"); 371 372 assert_eq!(res.status(), StatusCode::OK); 373 let body: Value = res.json().await.unwrap(); 374 375 let results = body["results"].as_array().unwrap(); 376 assert_eq!(results.len(), 2); 377 378 let update_result = &results[0]; 379 assert_eq!( 380 update_result["$type"], 381 "com.atproto.repo.applyWrites#updateResult" 382 ); 383 assert!(update_result["uri"].is_string()); 384 assert!(update_result["cid"].is_string()); 385 assert_eq!(update_result["validationStatus"], "valid"); 386 387 let delete_result = &results[1]; 388 assert_eq!( 389 delete_result["$type"], 390 "com.atproto.repo.applyWrites#deleteResult" 391 ); 392 assert!( 393 delete_result["uri"].is_null(), 394 "delete result should not have uri" 395 ); 396 assert!( 397 delete_result["cid"].is_null(), 398 "delete result should not have cid" 399 ); 400 assert!( 401 delete_result["validationStatus"].is_null(), 402 "delete result should not have validationStatus" 403 ); 404} 405 406#[tokio::test] 407async fn test_get_record_error_code() { 408 let client = client(); 409 let (did, _jwt) = setup_new_user("conform-get-err").await; 410 411 let res = client 412 .get(format!( 413 "{}/xrpc/com.atproto.repo.getRecord", 414 base_url().await 415 )) 416 .query(&[ 417 ("repo", did.as_str()), 418 ("collection", "app.bsky.feed.post"), 419 ("rkey", "nonexistent"), 420 ]) 421 .send() 422 .await 423 .expect("Failed to get record"); 424 425 assert_eq!(res.status(), StatusCode::NOT_FOUND); 426 let body: Value = res.json().await.unwrap(); 427 assert_eq!( 428 body["error"], "RecordNotFound", 429 "error code should be RecordNotFound per atproto spec" 430 ); 431} 432 433#[tokio::test] 434async fn test_create_record_unknown_lexicon_default_validation() { 435 let client = client(); 436 let (did, jwt) = setup_new_user("conform-unknown-lex").await; 437 438 let payload = json!({ 439 "repo": did, 440 "collection": "com.example.custom", 441 "record": { 442 "$type": "com.example.custom", 443 "data": "some custom data" 444 } 445 }); 446 447 let res = client 448 .post(format!( 449 "{}/xrpc/com.atproto.repo.createRecord", 450 base_url().await 451 )) 452 .bearer_auth(&jwt) 453 .json(&payload) 454 .send() 455 .await 456 .expect("Failed to create record"); 457 458 assert_eq!( 459 res.status(), 460 StatusCode::OK, 461 "unknown lexicon should be allowed with default validation" 462 ); 463 let body: Value = res.json().await.unwrap(); 464 465 assert!(body["uri"].is_string()); 466 assert!(body["cid"].is_string()); 467 assert!(body["commit"].is_object()); 468 assert_eq!( 469 body["validationStatus"], "unknown", 470 "validationStatus should be 'unknown' for unknown lexicons" 471 ); 472} 473 474#[tokio::test] 475async fn test_create_record_unknown_lexicon_strict_validation() { 476 let client = client(); 477 let (did, jwt) = setup_new_user("conform-unknown-strict").await; 478 479 let payload = json!({ 480 "repo": did, 481 "collection": "com.example.custom", 482 "validate": true, 483 "record": { 484 "$type": "com.example.custom", 485 "data": "some custom data" 486 } 487 }); 488 489 let res = client 490 .post(format!( 491 "{}/xrpc/com.atproto.repo.createRecord", 492 base_url().await 493 )) 494 .bearer_auth(&jwt) 495 .json(&payload) 496 .send() 497 .await 498 .expect("Failed to send request"); 499 500 assert_eq!( 501 res.status(), 502 StatusCode::BAD_REQUEST, 503 "unknown lexicon should fail with validate=true" 504 ); 505 let body: Value = res.json().await.unwrap(); 506 assert_eq!(body["error"], "InvalidRecord"); 507 assert!( 508 body["message"] 509 .as_str() 510 .unwrap() 511 .contains("Lexicon not found"), 512 "error should mention lexicon not found" 513 ); 514} 515 516#[tokio::test] 517async fn test_put_record_noop_same_content() { 518 let client = client(); 519 let (did, jwt) = setup_new_user("conform-put-noop").await; 520 let now = Utc::now().to_rfc3339(); 521 522 let record = json!({ 523 "$type": "app.bsky.feed.post", 524 "text": "This content will not change", 525 "createdAt": now 526 }); 527 528 let payload = json!({ 529 "repo": did, 530 "collection": "app.bsky.feed.post", 531 "rkey": "noop-test", 532 "record": record.clone() 533 }); 534 535 let first_res = client 536 .post(format!( 537 "{}/xrpc/com.atproto.repo.putRecord", 538 base_url().await 539 )) 540 .bearer_auth(&jwt) 541 .json(&payload) 542 .send() 543 .await 544 .expect("Failed to put record"); 545 assert_eq!(first_res.status(), StatusCode::OK); 546 let first_body: Value = first_res.json().await.unwrap(); 547 assert!( 548 first_body["commit"].is_object(), 549 "first put should have commit" 550 ); 551 552 let second_res = client 553 .post(format!( 554 "{}/xrpc/com.atproto.repo.putRecord", 555 base_url().await 556 )) 557 .bearer_auth(&jwt) 558 .json(&payload) 559 .send() 560 .await 561 .expect("Failed to put record"); 562 assert_eq!(second_res.status(), StatusCode::OK); 563 let second_body: Value = second_res.json().await.unwrap(); 564 565 assert!( 566 second_body["commit"].is_null(), 567 "second put with same content should have no commit (no-op)" 568 ); 569 assert_eq!( 570 first_body["cid"], second_body["cid"], 571 "CID should be the same for identical content" 572 ); 573} 574 575#[tokio::test] 576async fn test_apply_writes_unknown_lexicon() { 577 let client = client(); 578 let (did, jwt) = setup_new_user("conform-apply-unknown").await; 579 580 let payload = json!({ 581 "repo": did, 582 "writes": [ 583 { 584 "$type": "com.atproto.repo.applyWrites#create", 585 "collection": "com.example.custom", 586 "rkey": "custom-1", 587 "value": { 588 "$type": "com.example.custom", 589 "data": "custom data" 590 } 591 } 592 ] 593 }); 594 595 let res = client 596 .post(format!( 597 "{}/xrpc/com.atproto.repo.applyWrites", 598 base_url().await 599 )) 600 .bearer_auth(&jwt) 601 .json(&payload) 602 .send() 603 .await 604 .expect("Failed to apply writes"); 605 606 assert_eq!(res.status(), StatusCode::OK); 607 let body: Value = res.json().await.unwrap(); 608 609 let results = body["results"].as_array().unwrap(); 610 assert_eq!(results.len(), 1); 611 assert_eq!( 612 results[0]["validationStatus"], "unknown", 613 "unknown lexicon should have 'unknown' status" 614 ); 615}