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