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