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