this repo has no description
1mod common; 2mod helpers; 3 4use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD}; 5use chrono::Utc; 6use common::{base_url, client}; 7use helpers::verify_new_account; 8use reqwest::{redirect, StatusCode}; 9use serde_json::{json, Value}; 10use sha2::{Digest, Sha256}; 11use wiremock::{Mock, MockServer, ResponseTemplate}; 12use wiremock::matchers::{method, path}; 13 14fn generate_pkce() -> (String, String) { 15 let verifier_bytes: [u8; 32] = rand::random(); 16 let code_verifier = URL_SAFE_NO_PAD.encode(verifier_bytes); 17 18 let mut hasher = Sha256::new(); 19 hasher.update(code_verifier.as_bytes()); 20 let hash = hasher.finalize(); 21 let code_challenge = URL_SAFE_NO_PAD.encode(&hash); 22 23 (code_verifier, code_challenge) 24} 25 26fn no_redirect_client() -> reqwest::Client { 27 reqwest::Client::builder() 28 .redirect(redirect::Policy::none()) 29 .build() 30 .unwrap() 31} 32 33async fn setup_mock_client_metadata(redirect_uri: &str) -> MockServer { 34 let mock_server = MockServer::start().await; 35 36 let client_id = mock_server.uri(); 37 let metadata = json!({ 38 "client_id": client_id, 39 "client_name": "Test OAuth Client", 40 "redirect_uris": [redirect_uri], 41 "grant_types": ["authorization_code", "refresh_token"], 42 "response_types": ["code"], 43 "token_endpoint_auth_method": "none", 44 "dpop_bound_access_tokens": false 45 }); 46 47 Mock::given(method("GET")) 48 .and(path("/")) 49 .respond_with(ResponseTemplate::new(200).set_body_json(metadata)) 50 .mount(&mock_server) 51 .await; 52 53 mock_server 54} 55 56struct OAuthSession { 57 access_token: String, 58 refresh_token: String, 59 did: String, 60 client_id: String, 61} 62 63async fn create_user_and_oauth_session(handle_prefix: &str, redirect_uri: &str) -> (OAuthSession, MockServer) { 64 let url = base_url().await; 65 let http_client = client(); 66 67 let ts = Utc::now().timestamp_millis(); 68 let handle = format!("{}-{}", handle_prefix, ts); 69 let email = format!("{}-{}@example.com", handle_prefix, ts); 70 let password = format!("{}-password", handle_prefix); 71 72 let create_res = http_client 73 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 74 .json(&json!({ 75 "handle": handle, 76 "email": email, 77 "password": password 78 })) 79 .send() 80 .await 81 .expect("Account creation failed"); 82 83 assert_eq!(create_res.status(), StatusCode::OK); 84 let account: Value = create_res.json().await.unwrap(); 85 let user_did = account["did"].as_str().unwrap().to_string(); 86 87 let _ = verify_new_account(&http_client, &user_did).await; 88 89 let mock_client = setup_mock_client_metadata(redirect_uri).await; 90 let client_id = mock_client.uri(); 91 92 let (code_verifier, code_challenge) = generate_pkce(); 93 94 let par_res = http_client 95 .post(format!("{}/oauth/par", url)) 96 .form(&[ 97 ("response_type", "code"), 98 ("client_id", &client_id), 99 ("redirect_uri", redirect_uri), 100 ("code_challenge", &code_challenge), 101 ("code_challenge_method", "S256"), 102 ("scope", "atproto"), 103 ]) 104 .send() 105 .await 106 .expect("PAR failed"); 107 108 assert_eq!(par_res.status(), StatusCode::OK); 109 let par_body: Value = par_res.json().await.unwrap(); 110 let request_uri = par_body["request_uri"].as_str().unwrap(); 111 112 let auth_client = no_redirect_client(); 113 let auth_res = auth_client 114 .post(format!("{}/oauth/authorize", url)) 115 .form(&[ 116 ("request_uri", request_uri), 117 ("username", &handle), 118 ("password", &password), 119 ("remember_device", "false"), 120 ]) 121 .send() 122 .await 123 .expect("Authorize failed"); 124 125 let location = auth_res.headers().get("location").unwrap().to_str().unwrap(); 126 let code = location.split("code=").nth(1).unwrap().split('&').next().unwrap(); 127 128 let token_res = http_client 129 .post(format!("{}/oauth/token", url)) 130 .form(&[ 131 ("grant_type", "authorization_code"), 132 ("code", code), 133 ("redirect_uri", redirect_uri), 134 ("code_verifier", &code_verifier), 135 ("client_id", &client_id), 136 ]) 137 .send() 138 .await 139 .expect("Token request failed"); 140 141 assert_eq!(token_res.status(), StatusCode::OK); 142 let token_body: Value = token_res.json().await.unwrap(); 143 144 let session = OAuthSession { 145 access_token: token_body["access_token"].as_str().unwrap().to_string(), 146 refresh_token: token_body["refresh_token"].as_str().unwrap().to_string(), 147 did: user_did, 148 client_id, 149 }; 150 151 (session, mock_client) 152} 153 154#[tokio::test] 155async fn test_oauth_token_can_create_and_read_records() { 156 let url = base_url().await; 157 let http_client = client(); 158 159 let (session, _mock) = create_user_and_oauth_session( 160 "oauth-records", 161 "https://example.com/callback" 162 ).await; 163 164 let collection = "app.bsky.feed.post"; 165 let post_text = "Hello from OAuth! This post was created with an OAuth access token."; 166 167 let create_res = http_client 168 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 169 .bearer_auth(&session.access_token) 170 .json(&json!({ 171 "repo": session.did, 172 "collection": collection, 173 "record": { 174 "$type": collection, 175 "text": post_text, 176 "createdAt": Utc::now().to_rfc3339() 177 } 178 })) 179 .send() 180 .await 181 .expect("createRecord failed"); 182 183 assert_eq!(create_res.status(), StatusCode::OK, "Should create record with OAuth token"); 184 185 let create_body: Value = create_res.json().await.unwrap(); 186 let uri = create_body["uri"].as_str().unwrap(); 187 let rkey = uri.split('/').last().unwrap(); 188 189 let get_res = http_client 190 .get(format!("{}/xrpc/com.atproto.repo.getRecord", url)) 191 .bearer_auth(&session.access_token) 192 .query(&[ 193 ("repo", session.did.as_str()), 194 ("collection", collection), 195 ("rkey", rkey), 196 ]) 197 .send() 198 .await 199 .expect("getRecord failed"); 200 201 assert_eq!(get_res.status(), StatusCode::OK, "Should read record with OAuth token"); 202 203 let get_body: Value = get_res.json().await.unwrap(); 204 assert_eq!(get_body["value"]["text"], post_text); 205} 206 207#[tokio::test] 208async fn test_oauth_token_can_upload_blob() { 209 let url = base_url().await; 210 let http_client = client(); 211 212 let (session, _mock) = create_user_and_oauth_session( 213 "oauth-blob", 214 "https://example.com/callback" 215 ).await; 216 217 let blob_data = b"This is test blob data uploaded via OAuth"; 218 219 let upload_res = http_client 220 .post(format!("{}/xrpc/com.atproto.repo.uploadBlob", url)) 221 .bearer_auth(&session.access_token) 222 .header("Content-Type", "text/plain") 223 .body(blob_data.to_vec()) 224 .send() 225 .await 226 .expect("uploadBlob failed"); 227 228 assert_eq!(upload_res.status(), StatusCode::OK, "Should upload blob with OAuth token"); 229 230 let upload_body: Value = upload_res.json().await.unwrap(); 231 assert!(upload_body["blob"]["ref"]["$link"].is_string()); 232 assert_eq!(upload_body["blob"]["mimeType"], "text/plain"); 233} 234 235#[tokio::test] 236async fn test_oauth_token_can_describe_repo() { 237 let url = base_url().await; 238 let http_client = client(); 239 240 let (session, _mock) = create_user_and_oauth_session( 241 "oauth-describe", 242 "https://example.com/callback" 243 ).await; 244 245 let describe_res = http_client 246 .get(format!("{}/xrpc/com.atproto.repo.describeRepo", url)) 247 .bearer_auth(&session.access_token) 248 .query(&[("repo", session.did.as_str())]) 249 .send() 250 .await 251 .expect("describeRepo failed"); 252 253 assert_eq!(describe_res.status(), StatusCode::OK, "Should describe repo with OAuth token"); 254 255 let describe_body: Value = describe_res.json().await.unwrap(); 256 assert_eq!(describe_body["did"], session.did); 257 assert!(describe_body["handle"].is_string()); 258} 259 260#[tokio::test] 261async fn test_oauth_full_post_lifecycle_create_edit_delete() { 262 let url = base_url().await; 263 let http_client = client(); 264 265 let (session, _mock) = create_user_and_oauth_session( 266 "oauth-lifecycle", 267 "https://example.com/callback" 268 ).await; 269 270 let collection = "app.bsky.feed.post"; 271 let original_text = "Original post content"; 272 273 let create_res = http_client 274 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 275 .bearer_auth(&session.access_token) 276 .json(&json!({ 277 "repo": session.did, 278 "collection": collection, 279 "record": { 280 "$type": collection, 281 "text": original_text, 282 "createdAt": Utc::now().to_rfc3339() 283 } 284 })) 285 .send() 286 .await 287 .unwrap(); 288 289 assert_eq!(create_res.status(), StatusCode::OK); 290 let create_body: Value = create_res.json().await.unwrap(); 291 let uri = create_body["uri"].as_str().unwrap(); 292 let rkey = uri.split('/').last().unwrap(); 293 294 let updated_text = "Updated post content via OAuth putRecord"; 295 296 let put_res = http_client 297 .post(format!("{}/xrpc/com.atproto.repo.putRecord", url)) 298 .bearer_auth(&session.access_token) 299 .json(&json!({ 300 "repo": session.did, 301 "collection": collection, 302 "rkey": rkey, 303 "record": { 304 "$type": collection, 305 "text": updated_text, 306 "createdAt": Utc::now().to_rfc3339() 307 } 308 })) 309 .send() 310 .await 311 .unwrap(); 312 313 assert_eq!(put_res.status(), StatusCode::OK, "Should update record with OAuth token"); 314 315 let get_res = http_client 316 .get(format!("{}/xrpc/com.atproto.repo.getRecord", url)) 317 .bearer_auth(&session.access_token) 318 .query(&[ 319 ("repo", session.did.as_str()), 320 ("collection", collection), 321 ("rkey", rkey), 322 ]) 323 .send() 324 .await 325 .unwrap(); 326 327 let get_body: Value = get_res.json().await.unwrap(); 328 assert_eq!(get_body["value"]["text"], updated_text, "Record should have updated text"); 329 330 let delete_res = http_client 331 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", url)) 332 .bearer_auth(&session.access_token) 333 .json(&json!({ 334 "repo": session.did, 335 "collection": collection, 336 "rkey": rkey 337 })) 338 .send() 339 .await 340 .unwrap(); 341 342 assert_eq!(delete_res.status(), StatusCode::OK, "Should delete record with OAuth token"); 343 344 let get_deleted_res = http_client 345 .get(format!("{}/xrpc/com.atproto.repo.getRecord", url)) 346 .bearer_auth(&session.access_token) 347 .query(&[ 348 ("repo", session.did.as_str()), 349 ("collection", collection), 350 ("rkey", rkey), 351 ]) 352 .send() 353 .await 354 .unwrap(); 355 356 assert!( 357 get_deleted_res.status() == StatusCode::BAD_REQUEST || get_deleted_res.status() == StatusCode::NOT_FOUND, 358 "Deleted record should not be found, got {}", 359 get_deleted_res.status() 360 ); 361} 362 363#[tokio::test] 364async fn test_oauth_batch_operations_apply_writes() { 365 let url = base_url().await; 366 let http_client = client(); 367 368 let (session, _mock) = create_user_and_oauth_session( 369 "oauth-batch", 370 "https://example.com/callback" 371 ).await; 372 373 let collection = "app.bsky.feed.post"; 374 let now = Utc::now().to_rfc3339(); 375 376 let apply_res = http_client 377 .post(format!("{}/xrpc/com.atproto.repo.applyWrites", url)) 378 .bearer_auth(&session.access_token) 379 .json(&json!({ 380 "repo": session.did, 381 "writes": [ 382 { 383 "$type": "com.atproto.repo.applyWrites#create", 384 "collection": collection, 385 "rkey": "batch1", 386 "value": { 387 "$type": collection, 388 "text": "Batch post 1", 389 "createdAt": now 390 } 391 }, 392 { 393 "$type": "com.atproto.repo.applyWrites#create", 394 "collection": collection, 395 "rkey": "batch2", 396 "value": { 397 "$type": collection, 398 "text": "Batch post 2", 399 "createdAt": now 400 } 401 }, 402 { 403 "$type": "com.atproto.repo.applyWrites#create", 404 "collection": collection, 405 "rkey": "batch3", 406 "value": { 407 "$type": collection, 408 "text": "Batch post 3", 409 "createdAt": now 410 } 411 } 412 ] 413 })) 414 .send() 415 .await 416 .unwrap(); 417 418 assert_eq!(apply_res.status(), StatusCode::OK, "Should apply batch writes with OAuth token"); 419 420 let list_res = http_client 421 .get(format!("{}/xrpc/com.atproto.repo.listRecords", url)) 422 .bearer_auth(&session.access_token) 423 .query(&[ 424 ("repo", session.did.as_str()), 425 ("collection", collection), 426 ]) 427 .send() 428 .await 429 .unwrap(); 430 431 assert_eq!(list_res.status(), StatusCode::OK); 432 let list_body: Value = list_res.json().await.unwrap(); 433 let records = list_body["records"].as_array().unwrap(); 434 assert!(records.len() >= 3, "Should have at least 3 records from batch"); 435} 436 437#[tokio::test] 438async fn test_oauth_token_refresh_maintains_access() { 439 let url = base_url().await; 440 let http_client = client(); 441 442 let (session, _mock) = create_user_and_oauth_session( 443 "oauth-refresh-access", 444 "https://example.com/callback" 445 ).await; 446 447 let collection = "app.bsky.feed.post"; 448 let create_res = http_client 449 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 450 .bearer_auth(&session.access_token) 451 .json(&json!({ 452 "repo": session.did, 453 "collection": collection, 454 "record": { 455 "$type": collection, 456 "text": "Post before refresh", 457 "createdAt": Utc::now().to_rfc3339() 458 } 459 })) 460 .send() 461 .await 462 .unwrap(); 463 464 assert_eq!(create_res.status(), StatusCode::OK, "Original token should work"); 465 466 let refresh_res = http_client 467 .post(format!("{}/oauth/token", url)) 468 .form(&[ 469 ("grant_type", "refresh_token"), 470 ("refresh_token", &session.refresh_token), 471 ("client_id", &session.client_id), 472 ]) 473 .send() 474 .await 475 .unwrap(); 476 477 assert_eq!(refresh_res.status(), StatusCode::OK); 478 let refresh_body: Value = refresh_res.json().await.unwrap(); 479 let new_access_token = refresh_body["access_token"].as_str().unwrap(); 480 481 assert_ne!(new_access_token, session.access_token, "New token should be different"); 482 483 let create_res2 = http_client 484 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 485 .bearer_auth(new_access_token) 486 .json(&json!({ 487 "repo": session.did, 488 "collection": collection, 489 "record": { 490 "$type": collection, 491 "text": "Post after refresh with new token", 492 "createdAt": Utc::now().to_rfc3339() 493 } 494 })) 495 .send() 496 .await 497 .unwrap(); 498 499 assert_eq!(create_res2.status(), StatusCode::OK, "New token should work for creating records"); 500 501 let list_res = http_client 502 .get(format!("{}/xrpc/com.atproto.repo.listRecords", url)) 503 .bearer_auth(new_access_token) 504 .query(&[ 505 ("repo", session.did.as_str()), 506 ("collection", collection), 507 ]) 508 .send() 509 .await 510 .unwrap(); 511 512 assert_eq!(list_res.status(), StatusCode::OK, "New token should work for listing records"); 513 let list_body: Value = list_res.json().await.unwrap(); 514 let records = list_body["records"].as_array().unwrap(); 515 assert_eq!(records.len(), 2, "Should have both posts"); 516} 517 518#[tokio::test] 519async fn test_oauth_revoked_token_cannot_access_resources() { 520 let url = base_url().await; 521 let http_client = client(); 522 523 let (session, _mock) = create_user_and_oauth_session( 524 "oauth-revoke-access", 525 "https://example.com/callback" 526 ).await; 527 528 let collection = "app.bsky.feed.post"; 529 let create_res = http_client 530 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 531 .bearer_auth(&session.access_token) 532 .json(&json!({ 533 "repo": session.did, 534 "collection": collection, 535 "record": { 536 "$type": collection, 537 "text": "Post before revocation", 538 "createdAt": Utc::now().to_rfc3339() 539 } 540 })) 541 .send() 542 .await 543 .unwrap(); 544 545 assert_eq!(create_res.status(), StatusCode::OK, "Token should work before revocation"); 546 547 let revoke_res = http_client 548 .post(format!("{}/oauth/revoke", url)) 549 .form(&[("token", session.refresh_token.as_str())]) 550 .send() 551 .await 552 .unwrap(); 553 554 assert_eq!(revoke_res.status(), StatusCode::OK, "Revocation should succeed"); 555 556 let refresh_res = http_client 557 .post(format!("{}/oauth/token", url)) 558 .form(&[ 559 ("grant_type", "refresh_token"), 560 ("refresh_token", &session.refresh_token), 561 ("client_id", &session.client_id), 562 ]) 563 .send() 564 .await 565 .unwrap(); 566 567 assert_eq!(refresh_res.status(), StatusCode::BAD_REQUEST, "Revoked refresh token should not work"); 568} 569 570#[tokio::test] 571async fn test_oauth_multiple_clients_same_user() { 572 let url = base_url().await; 573 let http_client = client(); 574 575 let ts = Utc::now().timestamp_millis(); 576 let handle = format!("multi-client-{}", ts); 577 let email = format!("multi-client-{}@example.com", ts); 578 let password = "multi-client-password"; 579 580 let create_res = http_client 581 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 582 .json(&json!({ 583 "handle": handle, 584 "email": email, 585 "password": password 586 })) 587 .send() 588 .await 589 .unwrap(); 590 591 assert_eq!(create_res.status(), StatusCode::OK); 592 let account: Value = create_res.json().await.unwrap(); 593 let user_did = account["did"].as_str().unwrap(); 594 595 let _ = verify_new_account(&http_client, user_did).await; 596 597 let mock_client1 = setup_mock_client_metadata("https://client1.example.com/callback").await; 598 let client1_id = mock_client1.uri(); 599 600 let mock_client2 = setup_mock_client_metadata("https://client2.example.com/callback").await; 601 let client2_id = mock_client2.uri(); 602 603 let (verifier1, challenge1) = generate_pkce(); 604 let par_res1 = http_client 605 .post(format!("{}/oauth/par", url)) 606 .form(&[ 607 ("response_type", "code"), 608 ("client_id", &client1_id), 609 ("redirect_uri", "https://client1.example.com/callback"), 610 ("code_challenge", &challenge1), 611 ("code_challenge_method", "S256"), 612 ]) 613 .send() 614 .await 615 .unwrap(); 616 let par_body1: Value = par_res1.json().await.unwrap(); 617 let request_uri1 = par_body1["request_uri"].as_str().unwrap(); 618 619 let auth_client = no_redirect_client(); 620 let auth_res1 = auth_client 621 .post(format!("{}/oauth/authorize", url)) 622 .form(&[ 623 ("request_uri", request_uri1), 624 ("username", &handle), 625 ("password", password), 626 ("remember_device", "false"), 627 ]) 628 .send() 629 .await 630 .unwrap(); 631 let location1 = auth_res1.headers().get("location").unwrap().to_str().unwrap(); 632 let code1 = location1.split("code=").nth(1).unwrap().split('&').next().unwrap(); 633 634 let token_res1 = http_client 635 .post(format!("{}/oauth/token", url)) 636 .form(&[ 637 ("grant_type", "authorization_code"), 638 ("code", code1), 639 ("redirect_uri", "https://client1.example.com/callback"), 640 ("code_verifier", &verifier1), 641 ("client_id", &client1_id), 642 ]) 643 .send() 644 .await 645 .unwrap(); 646 let token_body1: Value = token_res1.json().await.unwrap(); 647 let token1 = token_body1["access_token"].as_str().unwrap(); 648 649 let (verifier2, challenge2) = generate_pkce(); 650 let par_res2 = http_client 651 .post(format!("{}/oauth/par", url)) 652 .form(&[ 653 ("response_type", "code"), 654 ("client_id", &client2_id), 655 ("redirect_uri", "https://client2.example.com/callback"), 656 ("code_challenge", &challenge2), 657 ("code_challenge_method", "S256"), 658 ]) 659 .send() 660 .await 661 .unwrap(); 662 let par_body2: Value = par_res2.json().await.unwrap(); 663 let request_uri2 = par_body2["request_uri"].as_str().unwrap(); 664 665 let auth_res2 = auth_client 666 .post(format!("{}/oauth/authorize", url)) 667 .form(&[ 668 ("request_uri", request_uri2), 669 ("username", &handle), 670 ("password", password), 671 ("remember_device", "false"), 672 ]) 673 .send() 674 .await 675 .unwrap(); 676 let location2 = auth_res2.headers().get("location").unwrap().to_str().unwrap(); 677 let code2 = location2.split("code=").nth(1).unwrap().split('&').next().unwrap(); 678 679 let token_res2 = http_client 680 .post(format!("{}/oauth/token", url)) 681 .form(&[ 682 ("grant_type", "authorization_code"), 683 ("code", code2), 684 ("redirect_uri", "https://client2.example.com/callback"), 685 ("code_verifier", &verifier2), 686 ("client_id", &client2_id), 687 ]) 688 .send() 689 .await 690 .unwrap(); 691 let token_body2: Value = token_res2.json().await.unwrap(); 692 let token2 = token_body2["access_token"].as_str().unwrap(); 693 694 assert_ne!(token1, token2, "Different clients should get different tokens"); 695 696 let collection = "app.bsky.feed.post"; 697 698 let create_res1 = http_client 699 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 700 .bearer_auth(token1) 701 .json(&json!({ 702 "repo": user_did, 703 "collection": collection, 704 "record": { 705 "$type": collection, 706 "text": "Post from client 1", 707 "createdAt": Utc::now().to_rfc3339() 708 } 709 })) 710 .send() 711 .await 712 .unwrap(); 713 714 assert_eq!(create_res1.status(), StatusCode::OK, "Client 1 token should work"); 715 716 let create_res2 = http_client 717 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 718 .bearer_auth(token2) 719 .json(&json!({ 720 "repo": user_did, 721 "collection": collection, 722 "record": { 723 "$type": collection, 724 "text": "Post from client 2", 725 "createdAt": Utc::now().to_rfc3339() 726 } 727 })) 728 .send() 729 .await 730 .unwrap(); 731 732 assert_eq!(create_res2.status(), StatusCode::OK, "Client 2 token should work"); 733 734 let list_res = http_client 735 .get(format!("{}/xrpc/com.atproto.repo.listRecords", url)) 736 .bearer_auth(token1) 737 .query(&[ 738 ("repo", user_did), 739 ("collection", collection), 740 ]) 741 .send() 742 .await 743 .unwrap(); 744 745 let list_body: Value = list_res.json().await.unwrap(); 746 let records = list_body["records"].as_array().unwrap(); 747 assert_eq!(records.len(), 2, "Both posts should be visible to either client"); 748} 749 750#[tokio::test] 751async fn test_oauth_social_interactions_follow_like_repost() { 752 let url = base_url().await; 753 let http_client = client(); 754 755 let (alice, _mock_alice) = create_user_and_oauth_session( 756 "alice-social", 757 "https://alice-app.example.com/callback" 758 ).await; 759 760 let (bob, _mock_bob) = create_user_and_oauth_session( 761 "bob-social", 762 "https://bob-app.example.com/callback" 763 ).await; 764 765 let post_collection = "app.bsky.feed.post"; 766 let post_res = http_client 767 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 768 .bearer_auth(&alice.access_token) 769 .json(&json!({ 770 "repo": alice.did, 771 "collection": post_collection, 772 "record": { 773 "$type": post_collection, 774 "text": "Hello from Alice! Looking for friends.", 775 "createdAt": Utc::now().to_rfc3339() 776 } 777 })) 778 .send() 779 .await 780 .unwrap(); 781 782 assert_eq!(post_res.status(), StatusCode::OK); 783 let post_body: Value = post_res.json().await.unwrap(); 784 let post_uri = post_body["uri"].as_str().unwrap(); 785 let post_cid = post_body["cid"].as_str().unwrap(); 786 787 let follow_collection = "app.bsky.graph.follow"; 788 let follow_res = http_client 789 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 790 .bearer_auth(&bob.access_token) 791 .json(&json!({ 792 "repo": bob.did, 793 "collection": follow_collection, 794 "record": { 795 "$type": follow_collection, 796 "subject": alice.did, 797 "createdAt": Utc::now().to_rfc3339() 798 } 799 })) 800 .send() 801 .await 802 .unwrap(); 803 804 assert_eq!(follow_res.status(), StatusCode::OK, "Bob should be able to follow Alice via OAuth"); 805 806 let like_collection = "app.bsky.feed.like"; 807 let like_res = http_client 808 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 809 .bearer_auth(&bob.access_token) 810 .json(&json!({ 811 "repo": bob.did, 812 "collection": like_collection, 813 "record": { 814 "$type": like_collection, 815 "subject": { 816 "uri": post_uri, 817 "cid": post_cid 818 }, 819 "createdAt": Utc::now().to_rfc3339() 820 } 821 })) 822 .send() 823 .await 824 .unwrap(); 825 826 assert_eq!(like_res.status(), StatusCode::OK, "Bob should be able to like Alice's post via OAuth"); 827 828 let repost_collection = "app.bsky.feed.repost"; 829 let repost_res = http_client 830 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 831 .bearer_auth(&bob.access_token) 832 .json(&json!({ 833 "repo": bob.did, 834 "collection": repost_collection, 835 "record": { 836 "$type": repost_collection, 837 "subject": { 838 "uri": post_uri, 839 "cid": post_cid 840 }, 841 "createdAt": Utc::now().to_rfc3339() 842 } 843 })) 844 .send() 845 .await 846 .unwrap(); 847 848 assert_eq!(repost_res.status(), StatusCode::OK, "Bob should be able to repost Alice's post via OAuth"); 849 850 let bob_follows = http_client 851 .get(format!("{}/xrpc/com.atproto.repo.listRecords", url)) 852 .bearer_auth(&bob.access_token) 853 .query(&[ 854 ("repo", bob.did.as_str()), 855 ("collection", follow_collection), 856 ]) 857 .send() 858 .await 859 .unwrap(); 860 861 let follows_body: Value = bob_follows.json().await.unwrap(); 862 let follows = follows_body["records"].as_array().unwrap(); 863 assert_eq!(follows.len(), 1, "Bob should have 1 follow"); 864 assert_eq!(follows[0]["value"]["subject"], alice.did); 865 866 let bob_likes = http_client 867 .get(format!("{}/xrpc/com.atproto.repo.listRecords", url)) 868 .bearer_auth(&bob.access_token) 869 .query(&[ 870 ("repo", bob.did.as_str()), 871 ("collection", like_collection), 872 ]) 873 .send() 874 .await 875 .unwrap(); 876 877 let likes_body: Value = bob_likes.json().await.unwrap(); 878 let likes = likes_body["records"].as_array().unwrap(); 879 assert_eq!(likes.len(), 1, "Bob should have 1 like"); 880} 881 882#[tokio::test] 883async fn test_oauth_cannot_modify_other_users_repo() { 884 let url = base_url().await; 885 let http_client = client(); 886 887 let (alice, _mock_alice) = create_user_and_oauth_session( 888 "alice-boundary", 889 "https://alice.example.com/callback" 890 ).await; 891 892 let (bob, _mock_bob) = create_user_and_oauth_session( 893 "bob-boundary", 894 "https://bob.example.com/callback" 895 ).await; 896 897 let collection = "app.bsky.feed.post"; 898 let malicious_res = http_client 899 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 900 .bearer_auth(&bob.access_token) 901 .json(&json!({ 902 "repo": alice.did, 903 "collection": collection, 904 "record": { 905 "$type": collection, 906 "text": "Bob trying to post as Alice!", 907 "createdAt": Utc::now().to_rfc3339() 908 } 909 })) 910 .send() 911 .await 912 .unwrap(); 913 914 assert_ne!( 915 malicious_res.status(), 916 StatusCode::OK, 917 "Bob should NOT be able to create records in Alice's repo" 918 ); 919 920 let alice_posts = http_client 921 .get(format!("{}/xrpc/com.atproto.repo.listRecords", url)) 922 .bearer_auth(&alice.access_token) 923 .query(&[ 924 ("repo", alice.did.as_str()), 925 ("collection", collection), 926 ]) 927 .send() 928 .await 929 .unwrap(); 930 931 let posts_body: Value = alice_posts.json().await.unwrap(); 932 let posts = posts_body["records"].as_array().unwrap(); 933 assert_eq!(posts.len(), 0, "Alice's repo should have no posts from Bob"); 934} 935 936#[tokio::test] 937async fn test_oauth_session_isolation_between_users() { 938 let url = base_url().await; 939 let http_client = client(); 940 941 let (alice, _mock_alice) = create_user_and_oauth_session( 942 "alice-isolation", 943 "https://alice.example.com/callback" 944 ).await; 945 946 let (bob, _mock_bob) = create_user_and_oauth_session( 947 "bob-isolation", 948 "https://bob.example.com/callback" 949 ).await; 950 951 let collection = "app.bsky.feed.post"; 952 953 let alice_post = http_client 954 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 955 .bearer_auth(&alice.access_token) 956 .json(&json!({ 957 "repo": alice.did, 958 "collection": collection, 959 "record": { 960 "$type": collection, 961 "text": "Alice's private thoughts", 962 "createdAt": Utc::now().to_rfc3339() 963 } 964 })) 965 .send() 966 .await 967 .unwrap(); 968 969 assert_eq!(alice_post.status(), StatusCode::OK); 970 971 let bob_post = http_client 972 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 973 .bearer_auth(&bob.access_token) 974 .json(&json!({ 975 "repo": bob.did, 976 "collection": collection, 977 "record": { 978 "$type": collection, 979 "text": "Bob's different thoughts", 980 "createdAt": Utc::now().to_rfc3339() 981 } 982 })) 983 .send() 984 .await 985 .unwrap(); 986 987 assert_eq!(bob_post.status(), StatusCode::OK); 988 989 let alice_list = http_client 990 .get(format!("{}/xrpc/com.atproto.repo.listRecords", url)) 991 .bearer_auth(&alice.access_token) 992 .query(&[ 993 ("repo", alice.did.as_str()), 994 ("collection", collection), 995 ]) 996 .send() 997 .await 998 .unwrap(); 999 1000 let alice_records: Value = alice_list.json().await.unwrap(); 1001 let alice_posts = alice_records["records"].as_array().unwrap(); 1002 assert_eq!(alice_posts.len(), 1); 1003 assert_eq!(alice_posts[0]["value"]["text"], "Alice's private thoughts"); 1004 1005 let bob_list = http_client 1006 .get(format!("{}/xrpc/com.atproto.repo.listRecords", url)) 1007 .bearer_auth(&bob.access_token) 1008 .query(&[ 1009 ("repo", bob.did.as_str()), 1010 ("collection", collection), 1011 ]) 1012 .send() 1013 .await 1014 .unwrap(); 1015 1016 let bob_records: Value = bob_list.json().await.unwrap(); 1017 let bob_posts = bob_records["records"].as_array().unwrap(); 1018 assert_eq!(bob_posts.len(), 1); 1019 assert_eq!(bob_posts[0]["value"]["text"], "Bob's different thoughts"); 1020} 1021 1022#[tokio::test] 1023async fn test_oauth_token_works_with_sync_endpoints() { 1024 let url = base_url().await; 1025 let http_client = client(); 1026 1027 let (session, _mock) = create_user_and_oauth_session( 1028 "oauth-sync", 1029 "https://example.com/callback" 1030 ).await; 1031 1032 let collection = "app.bsky.feed.post"; 1033 http_client 1034 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) 1035 .bearer_auth(&session.access_token) 1036 .json(&json!({ 1037 "repo": session.did, 1038 "collection": collection, 1039 "record": { 1040 "$type": collection, 1041 "text": "Post to sync", 1042 "createdAt": Utc::now().to_rfc3339() 1043 } 1044 })) 1045 .send() 1046 .await 1047 .unwrap(); 1048 1049 let latest_commit = http_client 1050 .get(format!("{}/xrpc/com.atproto.sync.getLatestCommit", url)) 1051 .query(&[("did", session.did.as_str())]) 1052 .send() 1053 .await 1054 .unwrap(); 1055 1056 assert_eq!(latest_commit.status(), StatusCode::OK); 1057 let commit_body: Value = latest_commit.json().await.unwrap(); 1058 assert!(commit_body["cid"].is_string()); 1059 assert!(commit_body["rev"].is_string()); 1060 1061 let repo_status = http_client 1062 .get(format!("{}/xrpc/com.atproto.sync.getRepoStatus", url)) 1063 .query(&[("did", session.did.as_str())]) 1064 .send() 1065 .await 1066 .unwrap(); 1067 1068 assert_eq!(repo_status.status(), StatusCode::OK); 1069 let status_body: Value = repo_status.json().await.unwrap(); 1070 assert_eq!(status_body["did"], session.did); 1071 assert!(status_body["active"].as_bool().unwrap()); 1072}