···11+CREATE INDEX IF NOT EXISTS idx_session_tokens_did_created_at
22+ON session_tokens(did, created_at DESC);
33+44+CREATE INDEX IF NOT EXISTS idx_oauth_token_did_expires_at
55+ON oauth_token(did, expires_at DESC);
66+77+CREATE INDEX IF NOT EXISTS idx_oauth_token_did_created_at
88+ON oauth_token(did, created_at DESC);
99+1010+CREATE INDEX IF NOT EXISTS idx_session_tokens_did_refresh_expires
1111+ON session_tokens(did, refresh_expires_at DESC);
1212+1313+CREATE INDEX IF NOT EXISTS idx_app_passwords_user_created
1414+ON app_passwords(user_id, created_at DESC);
1515+1616+CREATE INDEX IF NOT EXISTS idx_records_repo_collection_rkey
1717+ON records(repo_id, collection, rkey);
1818+1919+CREATE INDEX IF NOT EXISTS idx_passkeys_did_created
2020+ON passkeys(did, created_at DESC);
2121+2222+CREATE INDEX IF NOT EXISTS idx_backup_codes_did_unused
2323+ON backup_codes(did) WHERE used_at IS NULL;
+125-31
src/api/admin/account/info.rs
···217217 _auth: BearerAuthAdmin,
218218 RawQuery(raw_query): RawQuery,
219219) -> Response {
220220- let dids = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids");
220220+ let dids: Vec<String> = crate::util::parse_repeated_query_param(raw_query.as_deref(), "dids")
221221+ .into_iter()
222222+ .filter(|d| !d.is_empty())
223223+ .collect();
221224 if dids.is_empty() {
222225 return (
223226 StatusCode::BAD_REQUEST,
···225228 )
226229 .into_response();
227230 }
228228- let mut infos = Vec::new();
229229- for did in &dids {
230230- if did.is_empty() {
231231- continue;
231231+ let users = match sqlx::query!(
232232+ r#"
233233+ SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at
234234+ FROM users
235235+ WHERE did = ANY($1)
236236+ "#,
237237+ &dids
238238+ )
239239+ .fetch_all(&state.db)
240240+ .await
241241+ {
242242+ Ok(rows) => rows,
243243+ Err(e) => {
244244+ error!("Failed to fetch account infos: {:?}", e);
245245+ return (
246246+ StatusCode::INTERNAL_SERVER_ERROR,
247247+ Json(json!({"error": "InternalError"})),
248248+ )
249249+ .into_response();
232250 }
233233- let result = sqlx::query!(
251251+ };
252252+253253+ let user_ids: Vec<uuid::Uuid> = users.iter().map(|u| u.id).collect();
254254+255255+ let all_invite_codes = sqlx::query!(
256256+ r#"
257257+ SELECT ic.code, ic.available_uses, ic.disabled, ic.for_account, ic.created_at,
258258+ ic.created_by_user, u.did as created_by
259259+ FROM invite_codes ic
260260+ JOIN users u ON ic.created_by_user = u.id
261261+ WHERE ic.created_by_user = ANY($1)
262262+ "#,
263263+ &user_ids
264264+ )
265265+ .fetch_all(&state.db)
266266+ .await
267267+ .unwrap_or_default();
268268+269269+ let all_codes: Vec<String> = all_invite_codes.iter().map(|c| c.code.clone()).collect();
270270+ let all_invite_uses = if !all_codes.is_empty() {
271271+ sqlx::query!(
234272 r#"
235235- SELECT id, did, handle, email, created_at, invites_disabled, email_verified, deactivated_at
236236- FROM users
237237- WHERE did = $1
273273+ SELECT icu.code, u.did as used_by, icu.used_at
274274+ FROM invite_code_uses icu
275275+ JOIN users u ON icu.used_by_user = u.id
276276+ WHERE icu.code = ANY($1)
238277 "#,
239239- did
278278+ &all_codes
240279 )
241241- .fetch_optional(&state.db)
242242- .await;
243243- if let Ok(Some(row)) = result {
244244- let invited_by = get_invited_by(&state.db, row.id).await;
245245- let invites = get_invites_for_user(&state.db, row.id).await;
246246- infos.push(AccountInfo {
247247- did: row.did,
248248- handle: row.handle,
249249- email: row.email,
250250- indexed_at: row.created_at.to_rfc3339(),
251251- invite_note: None,
252252- invites_disabled: row.invites_disabled.unwrap_or(false),
253253- email_confirmed_at: if row.email_verified {
254254- Some(row.created_at.to_rfc3339())
255255- } else {
256256- None
257257- },
258258- deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()),
259259- invited_by,
260260- invites,
280280+ .fetch_all(&state.db)
281281+ .await
282282+ .unwrap_or_default()
283283+ } else {
284284+ Vec::new()
285285+ };
286286+287287+ let invited_by_map: std::collections::HashMap<uuid::Uuid, String> = sqlx::query!(
288288+ r#"
289289+ SELECT icu.used_by_user, icu.code
290290+ FROM invite_code_uses icu
291291+ WHERE icu.used_by_user = ANY($1)
292292+ "#,
293293+ &user_ids
294294+ )
295295+ .fetch_all(&state.db)
296296+ .await
297297+ .unwrap_or_default()
298298+ .into_iter()
299299+ .map(|r| (r.used_by_user, r.code))
300300+ .collect();
301301+302302+ let mut uses_by_code: std::collections::HashMap<String, Vec<InviteCodeUseInfo>> =
303303+ std::collections::HashMap::new();
304304+ for u in all_invite_uses {
305305+ uses_by_code
306306+ .entry(u.code.clone())
307307+ .or_default()
308308+ .push(InviteCodeUseInfo {
309309+ used_by: u.used_by,
310310+ used_at: u.used_at.to_rfc3339(),
261311 });
262262- }
312312+ }
313313+314314+ let mut codes_by_user: std::collections::HashMap<uuid::Uuid, Vec<InviteCodeInfo>> =
315315+ std::collections::HashMap::new();
316316+ let mut code_info_map: std::collections::HashMap<String, InviteCodeInfo> =
317317+ std::collections::HashMap::new();
318318+ for ic in all_invite_codes {
319319+ let info = InviteCodeInfo {
320320+ code: ic.code.clone(),
321321+ available: ic.available_uses,
322322+ disabled: ic.disabled.unwrap_or(false),
323323+ for_account: ic.for_account,
324324+ created_by: ic.created_by,
325325+ created_at: ic.created_at.to_rfc3339(),
326326+ uses: uses_by_code.get(&ic.code).cloned().unwrap_or_default(),
327327+ };
328328+ code_info_map.insert(ic.code.clone(), info.clone());
329329+ codes_by_user
330330+ .entry(ic.created_by_user)
331331+ .or_default()
332332+ .push(info);
333333+ }
334334+335335+ let mut infos = Vec::with_capacity(users.len());
336336+ for row in users {
337337+ let invited_by = invited_by_map
338338+ .get(&row.id)
339339+ .and_then(|code| code_info_map.get(code).cloned());
340340+ let invites = codes_by_user.get(&row.id).cloned();
341341+ infos.push(AccountInfo {
342342+ did: row.did,
343343+ handle: row.handle,
344344+ email: row.email,
345345+ indexed_at: row.created_at.to_rfc3339(),
346346+ invite_note: None,
347347+ invites_disabled: row.invites_disabled.unwrap_or(false),
348348+ email_confirmed_at: if row.email_verified {
349349+ Some(row.created_at.to_rfc3339())
350350+ } else {
351351+ None
352352+ },
353353+ deactivated_at: row.deactivated_at.map(|dt| dt.to_rfc3339()),
354354+ invited_by,
355355+ invites,
356356+ });
263357 }
264358 (StatusCode::OK, Json(GetAccountInfosOutput { infos })).into_response()
265359}
+1-1
src/api/delegation.rs
···726726 }
727727 };
728728729729- let plc_client = crate::plc::PlcClient::new(None);
729729+ let plc_client = crate::plc::PlcClient::with_cache(None, Some(state.cache.clone()));
730730 if let Err(e) = plc_client
731731 .send_operation(&genesis_result.did, &genesis_result.signed_operation)
732732 .await
···5353 assert_eq!(list_res.status(), StatusCode::OK);
5454 let list_body: Value = list_res.json().await.expect("Invalid JSON");
5555 let backups = list_body["backups"].as_array().unwrap();
5656- assert!(backups.len() >= 1);
5656+ assert!(!backups.is_empty());
5757}
58585959#[tokio::test]
+23-21
tests/common/mod.rs
···5252 }
5353 if std::env::var("XDG_RUNTIME_DIR").is_ok() {
5454 let _ = std::process::Command::new("podman")
5555- .args(&["rm", "-f", "--filter", "label=tranquil_pds_test=true"])
5555+ .args(["rm", "-f", "--filter", "label=tranquil_pds_test=true"])
5656 .output();
5757 }
5858 let _ = std::process::Command::new("docker")
5959- .args(&[
5959+ .args([
6060 "container",
6161 "prune",
6262 "-f",
···8383 unsafe {
8484 std::env::set_var("TRANQUIL_PDS_ALLOW_INSECURE_SECRETS", "1");
8585 }
8686- if std::env::var("DOCKER_HOST").is_err() {
8787- if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
8888- let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock");
8989- if podman_sock.exists() {
9090- unsafe {
9191- std::env::set_var(
9292- "DOCKER_HOST",
9393- format!("unix://{}", podman_sock.display()),
9494- );
9595- }
8686+ if std::env::var("DOCKER_HOST").is_err()
8787+ && let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR")
8888+ {
8989+ let podman_sock = std::path::Path::new(&runtime_dir).join("podman/podman.sock");
9090+ if podman_sock.exists() {
9191+ unsafe {
9292+ std::env::set_var(
9393+ "DOCKER_HOST",
9494+ format!("unix://{}", podman_sock.display()),
9595+ );
9696 }
9797 }
9898 }
···135135 std::env::var("AWS_REGION").unwrap_or_else(|_| "us-east-1".to_string()),
136136 );
137137 std::env::set_var("S3_ENDPOINT", &s3_endpoint);
138138+ std::env::set_var("MAX_IMPORT_SIZE", "100000000");
138139 }
139140 let mock_server = MockServer::start().await;
140141 setup_mock_appview(&mock_server).await;
···168169 std::env::set_var("AWS_SECRET_ACCESS_KEY", "minioadmin");
169170 std::env::set_var("AWS_REGION", "us-east-1");
170171 std::env::set_var("S3_ENDPOINT", &s3_endpoint);
172172+ std::env::set_var("MAX_IMPORT_SIZE", "100000000");
171173 }
172174 let sdk_config = aws_config::defaults(BehaviorVersion::latest())
173175 .region("us-east-1")
···418420 .to_string();
419421 let rkey = uri
420422 .split('/')
421421- .last()
423423+ .next_back()
422424 .expect("URI was malformed")
423425 .to_string();
424426 (uri, cid, rkey)
···472474 .expect("Failed to mark user as admin");
473475 }
474476 let verification_required = body["verificationRequired"].as_bool().unwrap_or(true);
475475- if let Some(access_jwt) = body["accessJwt"].as_str() {
476476- if !verification_required {
477477- return (access_jwt.to_string(), did);
478478- }
477477+ if let Some(access_jwt) = body["accessJwt"].as_str()
478478+ && !verification_required
479479+ {
480480+ return (access_jwt.to_string(), did);
479481 }
480482 let body_text: String = sqlx::query_scalar!(
481483 "SELECT body FROM comms_queue WHERE user_id = (SELECT id FROM users WHERE did = $1) AND comms_type = 'email_verification' ORDER BY created_at DESC LIMIT 1",
···488490 let verification_code = lines
489491 .iter()
490492 .enumerate()
491491- .find(|(_, line)| {
493493+ .find(|(_, line): &(usize, &&str)| {
492494 line.contains("verification code is:") || line.contains("code is:")
493495 })
494494- .and_then(|(i, _)| lines.get(i + 1).map(|s| s.trim().to_string()))
496496+ .and_then(|(i, _)| lines.get(i + 1).map(|s: &&str| s.trim().to_string()))
495497 .or_else(|| {
496498 body_text
497499 .split_whitespace()
498498- .find(|word| {
500500+ .find(|word: &&str| {
499501 word.contains('-') && word.chars().filter(|c| *c == '-').count() >= 3
500502 })
501501- .map(|s| s.to_string())
503503+ .map(|s: &str| s.to_string())
502504 })
503505 .unwrap_or_else(|| body_text.clone());
504506
+6-6
tests/delete_account.rs
···4040 let handle = format!("delete-test-{}.test", ts);
4141 let email = format!("delete-test-{}@test.com", ts);
4242 let password = "Delete123pass!";
4343- let (did, jwt) = create_verified_account(&client, &base_url, &handle, &email, password).await;
4343+ let (did, jwt) = create_verified_account(&client, base_url, &handle, &email, password).await;
4444 let request_delete_res = client
4545 .post(format!(
4646 "{}/xrpc/com.atproto.server.requestAccountDelete",
···9797 let handle = format!("delete-wrongpw-{}.test", ts);
9898 let email = format!("delete-wrongpw-{}@test.com", ts);
9999 let password = "Correct123!";
100100- let (did, jwt) = create_verified_account(&client, &base_url, &handle, &email, password).await;
100100+ let (did, jwt) = create_verified_account(&client, base_url, &handle, &email, password).await;
101101 let request_delete_res = client
102102 .post(format!(
103103 "{}/xrpc/com.atproto.server.requestAccountDelete",
···187187 let handle = format!("delete-expired-{}.test", ts);
188188 let email = format!("delete-expired-{}@test.com", ts);
189189 let password = "Delete123!";
190190- let (did, jwt) = create_verified_account(&client, &base_url, &handle, &email, password).await;
190190+ let (did, jwt) = create_verified_account(&client, base_url, &handle, &email, password).await;
191191 let request_delete_res = client
192192 .post(format!(
193193 "{}/xrpc/com.atproto.server.requestAccountDelete",
···242242 let email1 = format!("delete-user1-{}@test.com", ts);
243243 let password1 = "User1pass123!";
244244 let (did1, jwt1) =
245245- create_verified_account(&client, &base_url, &handle1, &email1, password1).await;
245245+ create_verified_account(&client, base_url, &handle1, &email1, password1).await;
246246 let handle2 = format!("delete-user2-{}.test", ts);
247247 let email2 = format!("delete-user2-{}@test.com", ts);
248248 let password2 = "User2pass123!";
249249- let (did2, _) = create_verified_account(&client, &base_url, &handle2, &email2, password2).await;
249249+ let (did2, _) = create_verified_account(&client, base_url, &handle2, &email2, password2).await;
250250 let request_delete_res = client
251251 .post(format!(
252252 "{}/xrpc/com.atproto.server.requestAccountDelete",
···294294 let email = format!("delete-apppw-{}@test.com", ts);
295295 let main_password = "Mainpass123!";
296296 let (did, jwt) =
297297- create_verified_account(&client, &base_url, &handle, &email, main_password).await;
297297+ create_verified_account(&client, base_url, &handle, &email, main_password).await;
298298 let app_password_res = client
299299 .post(format!(
300300 "{}/xrpc/com.atproto.server.createAppPassword",
+537
tests/dpop_unit.rs
···11+use base64::Engine as _;
22+use base64::engine::general_purpose::URL_SAFE_NO_PAD;
33+use chrono::Utc;
44+use p256::ecdsa::{SigningKey, signature::Signer};
55+use serde_json::json;
66+77+use tranquil_pds::oauth::dpop::{
88+ DPoPJwk, DPoPVerifier, compute_access_token_hash, compute_jwk_thumbprint,
99+};
1010+1111+fn create_dpop_proof(
1212+ method: &str,
1313+ htu: &str,
1414+ iat_offset_secs: i64,
1515+ alg: &str,
1616+ nonce: Option<&str>,
1717+ ath: Option<&str>,
1818+) -> (String, p256::ecdsa::VerifyingKey) {
1919+ let signing_key = SigningKey::random(&mut rand::thread_rng());
2020+ let verifying_key = *signing_key.verifying_key();
2121+ let point = verifying_key.to_encoded_point(false);
2222+ let x = URL_SAFE_NO_PAD.encode(point.x().unwrap());
2323+ let y = URL_SAFE_NO_PAD.encode(point.y().unwrap());
2424+2525+ let header = json!({
2626+ "typ": "dpop+jwt",
2727+ "alg": alg,
2828+ "jwk": {
2929+ "kty": "EC",
3030+ "crv": "P-256",
3131+ "x": x,
3232+ "y": y
3333+ }
3434+ });
3535+3636+ let iat = Utc::now().timestamp() + iat_offset_secs;
3737+ let jti = uuid::Uuid::new_v4().to_string();
3838+3939+ let mut payload = json!({
4040+ "jti": jti,
4141+ "htm": method,
4242+ "htu": htu,
4343+ "iat": iat
4444+ });
4545+4646+ if let Some(n) = nonce {
4747+ payload["nonce"] = json!(n);
4848+ }
4949+ if let Some(a) = ath {
5050+ payload["ath"] = json!(a);
5151+ }
5252+5353+ let header_b64 = URL_SAFE_NO_PAD.encode(header.to_string().as_bytes());
5454+ let payload_b64 = URL_SAFE_NO_PAD.encode(payload.to_string().as_bytes());
5555+ let signing_input = format!("{}.{}", header_b64, payload_b64);
5656+5757+ let signature: p256::ecdsa::Signature = signing_key.sign(signing_input.as_bytes());
5858+ let sig_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
5959+6060+ let proof = format!("{}.{}.{}", header_b64, payload_b64, sig_b64);
6161+ (proof, verifying_key)
6262+}
6363+6464+fn create_dpop_proof_with_invalid_sig(method: &str, htu: &str, alg: &str) -> String {
6565+ let signing_key = SigningKey::random(&mut rand::thread_rng());
6666+ let verifying_key = *signing_key.verifying_key();
6767+ let point = verifying_key.to_encoded_point(false);
6868+ let x = URL_SAFE_NO_PAD.encode(point.x().unwrap());
6969+ let y = URL_SAFE_NO_PAD.encode(point.y().unwrap());
7070+7171+ let header = json!({
7272+ "typ": "dpop+jwt",
7373+ "alg": alg,
7474+ "jwk": {
7575+ "kty": "EC",
7676+ "crv": "P-256",
7777+ "x": x,
7878+ "y": y
7979+ }
8080+ });
8181+8282+ let iat = Utc::now().timestamp();
8383+ let jti = uuid::Uuid::new_v4().to_string();
8484+8585+ let payload = json!({
8686+ "jti": jti,
8787+ "htm": method,
8888+ "htu": htu,
8989+ "iat": iat
9090+ });
9191+9292+ let header_b64 = URL_SAFE_NO_PAD.encode(header.to_string().as_bytes());
9393+ let payload_b64 = URL_SAFE_NO_PAD.encode(payload.to_string().as_bytes());
9494+9595+ let fake_sig = URL_SAFE_NO_PAD.encode(vec![0u8; 64]);
9696+9797+ format!("{}.{}.{}", header_b64, payload_b64, fake_sig)
9898+}
9999+100100+#[test]
101101+fn test_dpop_htu_query_params_stripped() {
102102+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
103103+ let url_with_query = "https://pds.example/xrpc/com.atproto.server.getSession?foo=bar";
104104+ let url_without_query = "https://pds.example/xrpc/com.atproto.server.getSession";
105105+106106+ let (proof, _) = create_dpop_proof("GET", url_with_query, 0, "ES256", None, None);
107107+ let result = verifier.verify_proof(&proof, "GET", url_without_query, None);
108108+ assert!(
109109+ result.is_ok(),
110110+ "Query params in htu should be stripped for comparison"
111111+ );
112112+}
113113+114114+#[test]
115115+fn test_dpop_htu_fragment_behavior() {
116116+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
117117+ let url_with_fragment = "https://pds.example/xrpc/foo#fragment";
118118+ let url_without_fragment = "https://pds.example/xrpc/foo";
119119+120120+ let (proof, _) = create_dpop_proof("GET", url_with_fragment, 0, "ES256", None, None);
121121+ let result = verifier.verify_proof(&proof, "GET", url_without_fragment, None);
122122+123123+ assert!(
124124+ result.is_err(),
125125+ "Fragment in htu should cause mismatch (currently NOT stripped)"
126126+ );
127127+}
128128+129129+#[test]
130130+fn test_dpop_es512_algorithm_rejected() {
131131+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
132132+ let url = "https://pds.example/xrpc/foo";
133133+134134+ let signing_key = SigningKey::random(&mut rand::thread_rng());
135135+ let verifying_key = *signing_key.verifying_key();
136136+ let point = verifying_key.to_encoded_point(false);
137137+ let x = URL_SAFE_NO_PAD.encode(point.x().unwrap());
138138+ let y = URL_SAFE_NO_PAD.encode(point.y().unwrap());
139139+140140+ let header = json!({
141141+ "typ": "dpop+jwt",
142142+ "alg": "ES512",
143143+ "jwk": {
144144+ "kty": "EC",
145145+ "crv": "P-256",
146146+ "x": x,
147147+ "y": y
148148+ }
149149+ });
150150+151151+ let payload = json!({
152152+ "jti": uuid::Uuid::new_v4().to_string(),
153153+ "htm": "GET",
154154+ "htu": url,
155155+ "iat": Utc::now().timestamp()
156156+ });
157157+158158+ let header_b64 = URL_SAFE_NO_PAD.encode(header.to_string().as_bytes());
159159+ let payload_b64 = URL_SAFE_NO_PAD.encode(payload.to_string().as_bytes());
160160+ let signing_input = format!("{}.{}", header_b64, payload_b64);
161161+ let signature: p256::ecdsa::Signature = signing_key.sign(signing_input.as_bytes());
162162+ let sig_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
163163+ let proof = format!("{}.{}.{}", header_b64, payload_b64, sig_b64);
164164+165165+ let result = verifier.verify_proof(&proof, "GET", url, None);
166166+ assert!(result.is_err(), "ES512 should be rejected as unsupported");
167167+}
168168+169169+#[test]
170170+fn test_dpop_iat_clock_skew_within_bounds() {
171171+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
172172+ let url = "https://pds.example/xrpc/foo";
173173+174174+ let (proof_299s_future, _) = create_dpop_proof("GET", url, 299, "ES256", None, None);
175175+ let result = verifier.verify_proof(&proof_299s_future, "GET", url, None);
176176+ assert!(
177177+ result.is_ok(),
178178+ "299s in future should be within clock skew tolerance"
179179+ );
180180+181181+ let (proof_299s_past, _) = create_dpop_proof("GET", url, -299, "ES256", None, None);
182182+ let result = verifier.verify_proof(&proof_299s_past, "GET", url, None);
183183+ assert!(
184184+ result.is_ok(),
185185+ "299s in past should be within clock skew tolerance"
186186+ );
187187+}
188188+189189+#[test]
190190+fn test_dpop_iat_clock_skew_beyond_bounds() {
191191+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
192192+ let url = "https://pds.example/xrpc/foo";
193193+194194+ let (proof_301s_future, _) = create_dpop_proof("GET", url, 301, "ES256", None, None);
195195+ let result = verifier.verify_proof(&proof_301s_future, "GET", url, None);
196196+ assert!(
197197+ result.is_err(),
198198+ "301s in future should exceed clock skew tolerance"
199199+ );
200200+201201+ let (proof_301s_past, _) = create_dpop_proof("GET", url, -301, "ES256", None, None);
202202+ let result = verifier.verify_proof(&proof_301s_past, "GET", url, None);
203203+ assert!(
204204+ result.is_err(),
205205+ "301s in past should exceed clock skew tolerance"
206206+ );
207207+}
208208+209209+#[test]
210210+fn test_dpop_http_method_case_insensitive() {
211211+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
212212+ let url = "https://pds.example/xrpc/foo";
213213+214214+ let (proof_lowercase, _) = create_dpop_proof("get", url, 0, "ES256", None, None);
215215+ let result = verifier.verify_proof(&proof_lowercase, "GET", url, None);
216216+ assert!(
217217+ result.is_ok(),
218218+ "HTTP method comparison should be case-insensitive"
219219+ );
220220+221221+ let (proof_mixed, _) = create_dpop_proof("GeT", url, 0, "ES256", None, None);
222222+ let result = verifier.verify_proof(&proof_mixed, "GET", url, None);
223223+ assert!(
224224+ result.is_ok(),
225225+ "HTTP method comparison should be case-insensitive"
226226+ );
227227+}
228228+229229+#[test]
230230+fn test_dpop_http_method_mismatch() {
231231+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
232232+ let url = "https://pds.example/xrpc/foo";
233233+234234+ let (proof_post, _) = create_dpop_proof("POST", url, 0, "ES256", None, None);
235235+ let result = verifier.verify_proof(&proof_post, "GET", url, None);
236236+ assert!(result.is_err(), "HTTP method mismatch should fail");
237237+}
238238+239239+#[test]
240240+fn test_dpop_invalid_signature() {
241241+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
242242+ let url = "https://pds.example/xrpc/foo";
243243+244244+ let proof = create_dpop_proof_with_invalid_sig("GET", url, "ES256");
245245+ let result = verifier.verify_proof(&proof, "GET", url, None);
246246+ assert!(result.is_err(), "Invalid signature should be rejected");
247247+}
248248+249249+#[test]
250250+fn test_dpop_malformed_base64() {
251251+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
252252+ let result = verifier.verify_proof("not.valid.base64!!!", "GET", "https://example.com", None);
253253+ assert!(result.is_err(), "Malformed base64 should be rejected");
254254+}
255255+256256+#[test]
257257+fn test_dpop_missing_parts() {
258258+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
259259+260260+ let result = verifier.verify_proof("onlyonepart", "GET", "https://example.com", None);
261261+ assert!(
262262+ result.is_err(),
263263+ "DPoP with missing parts should be rejected"
264264+ );
265265+266266+ let result = verifier.verify_proof("two.parts", "GET", "https://example.com", None);
267267+ assert!(
268268+ result.is_err(),
269269+ "DPoP with only two parts should be rejected"
270270+ );
271271+}
272272+273273+#[test]
274274+fn test_dpop_invalid_typ() {
275275+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
276276+ let url = "https://pds.example/xrpc/foo";
277277+278278+ let signing_key = SigningKey::random(&mut rand::thread_rng());
279279+ let verifying_key = *signing_key.verifying_key();
280280+ let point = verifying_key.to_encoded_point(false);
281281+ let x = URL_SAFE_NO_PAD.encode(point.x().unwrap());
282282+ let y = URL_SAFE_NO_PAD.encode(point.y().unwrap());
283283+284284+ let header = json!({
285285+ "typ": "jwt",
286286+ "alg": "ES256",
287287+ "jwk": {
288288+ "kty": "EC",
289289+ "crv": "P-256",
290290+ "x": x,
291291+ "y": y
292292+ }
293293+ });
294294+295295+ let payload = json!({
296296+ "jti": uuid::Uuid::new_v4().to_string(),
297297+ "htm": "GET",
298298+ "htu": url,
299299+ "iat": Utc::now().timestamp()
300300+ });
301301+302302+ let header_b64 = URL_SAFE_NO_PAD.encode(header.to_string().as_bytes());
303303+ let payload_b64 = URL_SAFE_NO_PAD.encode(payload.to_string().as_bytes());
304304+ let signing_input = format!("{}.{}", header_b64, payload_b64);
305305+ let signature: p256::ecdsa::Signature = signing_key.sign(signing_input.as_bytes());
306306+ let sig_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
307307+ let proof = format!("{}.{}.{}", header_b64, payload_b64, sig_b64);
308308+309309+ let result = verifier.verify_proof(&proof, "GET", url, None);
310310+ assert!(result.is_err(), "Invalid typ claim should be rejected");
311311+}
312312+313313+#[test]
314314+fn test_dpop_unsupported_algorithm() {
315315+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
316316+ let url = "https://pds.example/xrpc/foo";
317317+318318+ let signing_key = SigningKey::random(&mut rand::thread_rng());
319319+ let verifying_key = *signing_key.verifying_key();
320320+ let point = verifying_key.to_encoded_point(false);
321321+ let x = URL_SAFE_NO_PAD.encode(point.x().unwrap());
322322+ let y = URL_SAFE_NO_PAD.encode(point.y().unwrap());
323323+324324+ let header = json!({
325325+ "typ": "dpop+jwt",
326326+ "alg": "RS256",
327327+ "jwk": {
328328+ "kty": "EC",
329329+ "crv": "P-256",
330330+ "x": x,
331331+ "y": y
332332+ }
333333+ });
334334+335335+ let payload = json!({
336336+ "jti": uuid::Uuid::new_v4().to_string(),
337337+ "htm": "GET",
338338+ "htu": url,
339339+ "iat": Utc::now().timestamp()
340340+ });
341341+342342+ let header_b64 = URL_SAFE_NO_PAD.encode(header.to_string().as_bytes());
343343+ let payload_b64 = URL_SAFE_NO_PAD.encode(payload.to_string().as_bytes());
344344+ let signing_input = format!("{}.{}", header_b64, payload_b64);
345345+ let signature: p256::ecdsa::Signature = signing_key.sign(signing_input.as_bytes());
346346+ let sig_b64 = URL_SAFE_NO_PAD.encode(signature.to_bytes());
347347+ let proof = format!("{}.{}.{}", header_b64, payload_b64, sig_b64);
348348+349349+ let result = verifier.verify_proof(&proof, "GET", url, None);
350350+ assert!(result.is_err(), "Unsupported algorithm should be rejected");
351351+}
352352+353353+#[test]
354354+fn test_dpop_access_token_hash() {
355355+ let token = "test-access-token";
356356+ let hash = compute_access_token_hash(token);
357357+ assert!(!hash.is_empty());
358358+359359+ let hash2 = compute_access_token_hash(token);
360360+ assert_eq!(hash, hash2, "Same token should produce same hash");
361361+362362+ let hash3 = compute_access_token_hash("different-token");
363363+ assert_ne!(hash, hash3, "Different token should produce different hash");
364364+}
365365+366366+#[test]
367367+fn test_dpop_nonce_generation_and_validation() {
368368+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
369369+ let nonce = verifier.generate_nonce();
370370+ assert!(!nonce.is_empty());
371371+372372+ let result = verifier.validate_nonce(&nonce);
373373+ assert!(result.is_ok(), "Freshly generated nonce should be valid");
374374+}
375375+376376+#[test]
377377+fn test_dpop_nonce_invalid_encoding() {
378378+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
379379+ let result = verifier.validate_nonce("not-valid-base64!!!");
380380+ assert!(result.is_err(), "Invalid base64 nonce should be rejected");
381381+}
382382+383383+#[test]
384384+fn test_dpop_nonce_too_short() {
385385+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
386386+ let short_nonce = URL_SAFE_NO_PAD.encode(vec![0u8; 10]);
387387+ let result = verifier.validate_nonce(&short_nonce);
388388+ assert!(result.is_err(), "Too short nonce should be rejected");
389389+}
390390+391391+#[test]
392392+fn test_dpop_nonce_tampered_signature() {
393393+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
394394+ let nonce = verifier.generate_nonce();
395395+396396+ let nonce_bytes = URL_SAFE_NO_PAD.decode(&nonce).unwrap();
397397+ let mut tampered = nonce_bytes.clone();
398398+ tampered[10] ^= 0xFF;
399399+ let tampered_nonce = URL_SAFE_NO_PAD.encode(&tampered);
400400+401401+ let result = verifier.validate_nonce(&tampered_nonce);
402402+ assert!(result.is_err(), "Tampered nonce should be rejected");
403403+}
404404+405405+#[test]
406406+fn test_jwk_thumbprint_ec() {
407407+ let jwk = DPoPJwk {
408408+ kty: "EC".to_string(),
409409+ crv: Some("P-256".to_string()),
410410+ x: Some("test_x".to_string()),
411411+ y: Some("test_y".to_string()),
412412+ };
413413+ let thumbprint = compute_jwk_thumbprint(&jwk).unwrap();
414414+ assert!(!thumbprint.is_empty());
415415+416416+ let thumbprint2 = compute_jwk_thumbprint(&jwk).unwrap();
417417+ assert_eq!(
418418+ thumbprint, thumbprint2,
419419+ "Same JWK should produce same thumbprint"
420420+ );
421421+}
422422+423423+#[test]
424424+fn test_jwk_thumbprint_okp() {
425425+ let jwk = DPoPJwk {
426426+ kty: "OKP".to_string(),
427427+ crv: Some("Ed25519".to_string()),
428428+ x: Some("test_x".to_string()),
429429+ y: None,
430430+ };
431431+ let thumbprint = compute_jwk_thumbprint(&jwk).unwrap();
432432+ assert!(!thumbprint.is_empty());
433433+}
434434+435435+#[test]
436436+fn test_jwk_thumbprint_unsupported_kty() {
437437+ let jwk = DPoPJwk {
438438+ kty: "RSA".to_string(),
439439+ crv: None,
440440+ x: None,
441441+ y: None,
442442+ };
443443+ let result = compute_jwk_thumbprint(&jwk);
444444+ assert!(result.is_err(), "Unsupported key type should error");
445445+}
446446+447447+#[test]
448448+fn test_jwk_thumbprint_missing_fields() {
449449+ let jwk = DPoPJwk {
450450+ kty: "EC".to_string(),
451451+ crv: None,
452452+ x: None,
453453+ y: None,
454454+ };
455455+ let result = compute_jwk_thumbprint(&jwk);
456456+ assert!(result.is_err(), "Missing crv should error");
457457+}
458458+459459+#[test]
460460+fn test_dpop_uri_normalization_preserves_port() {
461461+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
462462+ let url_with_port = "https://pds.example:8080/xrpc/foo";
463463+464464+ let (proof, _) = create_dpop_proof("GET", url_with_port, 0, "ES256", None, None);
465465+ let result = verifier.verify_proof(&proof, "GET", url_with_port, None);
466466+ assert!(result.is_ok(), "URL with port should work");
467467+468468+ let url_without_port = "https://pds.example/xrpc/foo";
469469+ let result = verifier.verify_proof(&proof, "GET", url_without_port, None);
470470+ assert!(result.is_err(), "Different port should fail");
471471+}
472472+473473+#[test]
474474+fn test_dpop_uri_normalization_preserves_path() {
475475+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
476476+ let url = "https://pds.example/xrpc/com.atproto.server.getSession";
477477+478478+ let (proof, _) = create_dpop_proof("GET", url, 0, "ES256", None, None);
479479+480480+ let different_path = "https://pds.example/xrpc/com.atproto.server.refreshSession";
481481+ let result = verifier.verify_proof(&proof, "GET", different_path, None);
482482+ assert!(result.is_err(), "Different path should fail");
483483+}
484484+485485+#[test]
486486+fn test_dpop_htu_must_be_full_url_not_path() {
487487+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
488488+ let full_url = "https://pds.example/xrpc/com.atproto.server.getSession";
489489+ let path_only = "/xrpc/com.atproto.server.getSession";
490490+491491+ let (proof_with_path, _) = create_dpop_proof("GET", path_only, 0, "ES256", None, None);
492492+ let result = verifier.verify_proof(&proof_with_path, "GET", full_url, None);
493493+ assert!(
494494+ result.is_err(),
495495+ "htu with path-only should not match full URL"
496496+ );
497497+498498+ let (proof_with_full, _) = create_dpop_proof("GET", full_url, 0, "ES256", None, None);
499499+ let result = verifier.verify_proof(&proof_with_full, "GET", full_url, None);
500500+ assert!(result.is_ok(), "htu with full URL should match");
501501+}
502502+503503+#[test]
504504+fn test_dpop_htu_scheme_must_match() {
505505+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
506506+ let https_url = "https://pds.example/xrpc/foo";
507507+ let http_url = "http://pds.example/xrpc/foo";
508508+509509+ let (proof, _) = create_dpop_proof("GET", http_url, 0, "ES256", None, None);
510510+ let result = verifier.verify_proof(&proof, "GET", https_url, None);
511511+ assert!(result.is_err(), "HTTP vs HTTPS scheme mismatch should fail");
512512+}
513513+514514+#[test]
515515+fn test_dpop_htu_host_must_match() {
516516+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
517517+ let url1 = "https://pds1.example/xrpc/foo";
518518+ let url2 = "https://pds2.example/xrpc/foo";
519519+520520+ let (proof, _) = create_dpop_proof("GET", url1, 0, "ES256", None, None);
521521+ let result = verifier.verify_proof(&proof, "GET", url2, None);
522522+ assert!(result.is_err(), "Different host should fail");
523523+}
524524+525525+#[test]
526526+fn test_dpop_server_must_check_full_url_not_path() {
527527+ let verifier = DPoPVerifier::new(b"test-secret-32-bytes-long!!!!!!!");
528528+ let full_url = "https://pds.example/xrpc/com.atproto.server.getSession";
529529+ let path_only = "/xrpc/com.atproto.server.getSession";
530530+531531+ let (proof, _) = create_dpop_proof("GET", full_url, 0, "ES256", None, None);
532532+ let result = verifier.verify_proof(&proof, "GET", path_only, None);
533533+ assert!(
534534+ result.is_err(),
535535+ "Server checking path-only against full URL htu should fail"
536536+ );
537537+}
+8-8
tests/email_update.rs
···5959 let base_url = common::base_url().await;
6060 let handle = format!("er{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
6161 let email = format!("{}@example.com", handle);
6262- let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
6262+ let (access_jwt, _) = create_verified_account(&client, base_url, &handle, &email).await;
63636464 let res = client
6565 .post(format!(
···8282 let pool = common::get_test_db_pool().await;
8383 let handle = format!("eu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
8484 let email = format!("{}@example.com", handle);
8585- let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await;
8585+ let (access_jwt, did) = create_verified_account(&client, base_url, &handle, &email).await;
8686 let new_email = format!("new_{}@example.com", handle);
87878888 let res = client
···126126 let base_url = common::base_url().await;
127127 let handle = format!("ed{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
128128 let email = format!("{}@example.com", handle);
129129- let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
129129+ let (access_jwt, _) = create_verified_account(&client, base_url, &handle, &email).await;
130130 let new_email = format!("direct_{}@example.com", handle);
131131132132 let res = client
···147147 let base_url = common::base_url().await;
148148 let handle = format!("es{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
149149 let email = format!("{}@example.com", handle);
150150- let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
150150+ let (access_jwt, _) = create_verified_account(&client, base_url, &handle, &email).await;
151151152152 let res = client
153153 .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
···169169 let base_url = common::base_url().await;
170170 let handle = format!("eb{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
171171 let email = format!("{}@example.com", handle);
172172- let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
172172+ let (access_jwt, _) = create_verified_account(&client, base_url, &handle, &email).await;
173173 let new_email = format!("badtok_{}@example.com", handle);
174174175175 let res = client
···220220 let base_url = common::base_url().await;
221221 let handle = format!("ef{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
222222 let email = format!("{}@example.com", handle);
223223- let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await;
223223+ let (access_jwt, _) = create_verified_account(&client, base_url, &handle, &email).await;
224224225225 let res = client
226226 .post(format!("{}/xrpc/com.atproto.server.updateEmail", base_url))
···470470471471 let handle1 = format!("d1{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
472472 let email1 = format!("{}@example.com", handle1);
473473- let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await;
473473+ let (_, _) = create_verified_account(&client, base_url, &handle1, &email1).await;
474474475475 let handle2 = format!("d2{}", &uuid::Uuid::new_v4().simple().to_string()[..12]);
476476 let email2 = format!("{}@example.com", handle2);
477477- let (access_jwt2, did2) = create_verified_account(&client, &base_url, &handle2, &email2).await;
477477+ let (access_jwt2, did2) = create_verified_account(&client, base_url, &handle2, &email2).await;
478478479479 let res = client
480480 .post(format!(
+30-30
tests/firehose_validation.rs
···232232 tungstenite::Message::Binary(bin) => bin,
233233 _ => continue,
234234 };
235235- if let Ok((h, f)) = parse_frame(&raw_bytes) {
236236- if f.repo == did {
237237- frame_opt = Some((h, f));
238238- break;
239239- }
235235+ if let Ok((h, f)) = parse_frame(&raw_bytes)
236236+ && f.repo == did
237237+ {
238238+ frame_opt = Some((h, f));
239239+ break;
240240 }
241241 }
242242 })
···427427 tungstenite::Message::Binary(bin) => bin,
428428 _ => continue,
429429 };
430430- if let Ok((_, f)) = parse_frame(&raw_bytes) {
431431- if f.repo == did {
432432- frame_opt = Some(f);
433433- break;
434434- }
430430+ if let Ok((_, f)) = parse_frame(&raw_bytes)
431431+ && f.repo == did
432432+ {
433433+ frame_opt = Some(f);
434434+ break;
435435 }
436436 }
437437 })
···504504 tungstenite::Message::Binary(bin) => bin,
505505 _ => continue,
506506 };
507507- if let Ok((_, f)) = parse_frame(&raw_bytes) {
508508- if f.repo == did {
509509- first_frame_opt = Some(f);
510510- break;
511511- }
507507+ if let Ok((_, f)) = parse_frame(&raw_bytes)
508508+ && f.repo == did
509509+ {
510510+ first_frame_opt = Some(f);
511511+ break;
512512 }
513513 }
514514 })
···554554 tungstenite::Message::Binary(bin) => bin,
555555 _ => continue,
556556 };
557557- if let Ok((_, f)) = parse_frame(&raw_bytes) {
558558- if f.repo == did {
559559- second_frame_opt = Some(f);
560560- break;
561561- }
557557+ if let Ok((_, f)) = parse_frame(&raw_bytes)
558558+ && f.repo == did
559559+ {
560560+ second_frame_opt = Some(f);
561561+ break;
562562 }
563563 }
564564 })
···626626 tungstenite::Message::Binary(bin) => bin,
627627 _ => continue,
628628 };
629629- if let Ok((_, f)) = parse_frame(&raw) {
630630- if f.repo == did {
631631- raw_bytes_opt = Some(raw.to_vec());
632632- break;
633633- }
629629+ if let Ok((_, f)) = parse_frame(&raw)
630630+ && f.repo == did
631631+ {
632632+ raw_bytes_opt = Some(raw.to_vec());
633633+ break;
634634 }
635635 }
636636 })
···826826 found_info = true;
827827 println!("Found OutdatedCursor info frame!");
828828 }
829829- } else if let Ok((_, frame)) = parse_frame(&bin) {
830830- if frame.repo == did {
831831- found_commit = true;
832832- println!("Found commit for our DID");
833833- }
829829+ } else if let Ok((_, frame)) = parse_frame(&bin)
830830+ && frame.repo == did
831831+ {
832832+ found_commit = true;
833833+ println!("Found commit for our DID");
834834 }
835835 if found_commit {
836836 break;
+1-1
tests/import_verification.rs
···307307 assert_eq!(res.status(), StatusCode::OK);
308308 let body: serde_json::Value = res.json().await.unwrap();
309309 let uri = body["uri"].as_str().unwrap();
310310- let rkey = uri.split('/').last().unwrap().to_string();
310310+ let rkey = uri.split('/').next_back().unwrap().to_string();
311311 rkeys.push(rkey);
312312 }
313313 for rkey in &rkeys {
+4-4
tests/import_with_verification.rs
···192192 let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
193193 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
194194 let pds_endpoint = format!("https://{}", hostname);
195195- let handle = did.split(':').last().unwrap_or("user");
195195+ let handle = did.split(':').next_back().unwrap_or("user");
196196 let did_doc = create_did_document(&did, handle, &signing_key, &pds_endpoint);
197197 let mock_plc = setup_mock_plc_directory(&did, did_doc).await;
198198 unsafe {
···236236 SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
237237 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
238238 let pds_endpoint = format!("https://{}", hostname);
239239- let handle = did.split(':').last().unwrap_or("user");
239239+ let handle = did.split(':').next_back().unwrap_or("user");
240240 let did_doc = create_did_document(&did, handle, &correct_signing_key, &pds_endpoint);
241241 let mock_plc = setup_mock_plc_directory(&did, did_doc).await;
242242 unsafe {
···285285 let wrong_did = "did:plc:wrongdidthatdoesnotmatch";
286286 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string());
287287 let pds_endpoint = format!("https://{}", hostname);
288288- let handle = did.split(':').last().unwrap_or("user");
288288+ let handle = did.split(':').next_back().unwrap_or("user");
289289 let did_doc = create_did_document(&did, handle, &signing_key, &pds_endpoint);
290290 let mock_plc = setup_mock_plc_directory(&did, did_doc).await;
291291 unsafe {
···370370 .await
371371 .expect("Failed to get user signing key");
372372 let signing_key = SigningKey::from_slice(&key_bytes).expect("Failed to create signing key");
373373- let handle = did.split(':').last().unwrap_or("user");
373373+ let handle = did.split(':').next_back().unwrap_or("user");
374374 let did_doc_without_key = json!({
375375 "@context": ["https://www.w3.org/ns/did/v1"],
376376 "id": did,
+6-6
tests/jwt_security.rs
···4444 let token = create_access_token(did, &key_bytes).expect("create token");
4545 let parts: Vec<&str> = token.split('.').collect();
46464747- let forged_signature = URL_SAFE_NO_PAD.encode(&[0u8; 64]);
4747+ let forged_signature = URL_SAFE_NO_PAD.encode([0u8; 64]);
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");
···121121 let mut mac = HmacSha256::new_from_slice(&key_bytes).unwrap();
122122 mac.update(message.as_bytes());
123123 let hmac_sig = mac.finalize().into_bytes();
124124- let hs256_token = format!("{}.{}", message, URL_SAFE_NO_PAD.encode(&hmac_sig));
124124+ let hs256_token = format!("{}.{}", message, URL_SAFE_NO_PAD.encode(hmac_sig));
125125 assert!(
126126 verify_access_token(&hs256_token, &key_bytes).is_err(),
127127 "HS256 substitution must be rejected"
···130130 for (alg, sig_len) in [("RS256", 256), ("ES256", 64)] {
131131 let header = json!({ "alg": alg, "typ": TOKEN_TYPE_ACCESS });
132132 let header_b64 = URL_SAFE_NO_PAD.encode(serde_json::to_string(&header).unwrap());
133133- let fake_sig = URL_SAFE_NO_PAD.encode(&vec![1u8; sig_len]);
133133+ let fake_sig = URL_SAFE_NO_PAD.encode(vec![1u8; sig_len]);
134134 let token = format!("{}.{}.{}", header_b64, claims_b64, fake_sig);
135135 assert!(
136136 verify_access_token(&token, &key_bytes).is_err(),
···335335336336 let invalid_header = URL_SAFE_NO_PAD.encode("{not valid json}");
337337 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"sub":"test"}"#);
338338- let fake_sig = URL_SAFE_NO_PAD.encode(&[1u8; 64]);
338338+ let fake_sig = URL_SAFE_NO_PAD.encode([1u8; 64]);
339339 assert!(
340340 verify_access_token(
341341 &format!("{}.{}.{}", invalid_header, claims_b64, fake_sig),
···439439440440 let header_b64 = URL_SAFE_NO_PAD.encode(r#"{"alg":"ES256K"}"#);
441441 let claims_b64 = URL_SAFE_NO_PAD.encode(r#"{"iss":"did:plc:iss","sub":"did:plc:sub"}"#);
442442- let fake_sig = URL_SAFE_NO_PAD.encode(&[0u8; 64]);
442442+ let fake_sig = URL_SAFE_NO_PAD.encode([0u8; 64]);
443443 let unverified = format!("{}.{}.{}", header_b64, claims_b64, fake_sig);
444444 assert_eq!(get_did_from_token(&unverified).unwrap(), "did:plc:sub");
445445···479479 "{}.{}.{}",
480480 parts[0],
481481 parts[1],
482482- URL_SAFE_NO_PAD.encode(&[0xFFu8; 64])
482482+ URL_SAFE_NO_PAD.encode([0xFFu8; 64])
483483 );
484484 let _ = verify_access_token(&almost_valid_token, &key_bytes);
485485 let _ = verify_access_token(&completely_invalid_token, &key_bytes);
···461461 let did = account["did"].as_str().unwrap().to_string();
462462 let jwt = verify_new_account(&client, &did).await;
463463 let (post_uri, _) = create_post(&client, &did, &jwt, "Post before deactivation").await;
464464- let post_rkey = post_uri.split('/').last().unwrap();
464464+ let post_rkey = post_uri.split('/').next_back().unwrap();
465465 let status_before = client
466466 .get(format!(
467467 "{}/xrpc/com.atproto.server.checkAccountStatus",
+4-4
tests/lifecycle_social.rs
···1414 let (post_uri, post_cid) =
1515 create_post(&client, &alice_did, &alice_jwt, "Like this post!").await;
1616 let (like_uri, _) = create_like(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await;
1717- let like_rkey = like_uri.split('/').last().unwrap();
1717+ let like_rkey = like_uri.split('/').next_back().unwrap();
1818 let get_like_res = client
1919 .get(format!(
2020 "{}/xrpc/com.atproto.repo.getRecord",
···7474 let (bob_did, bob_jwt) = setup_new_user("bob-repost").await;
7575 let (post_uri, post_cid) = create_post(&client, &alice_did, &alice_jwt, "Repost this!").await;
7676 let (repost_uri, _) = create_repost(&client, &bob_did, &bob_jwt, &post_uri, &post_cid).await;
7777- let repost_rkey = repost_uri.split('/').last().unwrap();
7777+ let repost_rkey = repost_uri.split('/').next_back().unwrap();
7878 let get_repost_res = client
7979 .get(format!(
8080 "{}/xrpc/com.atproto.repo.getRecord",
···119119 let (alice_did, _alice_jwt) = setup_new_user("alice-unfollow").await;
120120 let (bob_did, bob_jwt) = setup_new_user("bob-unfollow").await;
121121 let (follow_uri, _) = create_follow(&client, &bob_did, &bob_jwt, &alice_did).await;
122122- let follow_rkey = follow_uri.split('/').last().unwrap();
122122+ let follow_rkey = follow_uri.split('/').next_back().unwrap();
123123 let get_follow_res = client
124124 .get(format!(
125125 "{}/xrpc/com.atproto.repo.getRecord",
···240240 .query(&[
241241 ("repo", did.as_str()),
242242 ("collection", "app.bsky.feed.post"),
243243- ("rkey", post_uri.split('/').last().unwrap()),
243243+ ("rkey", post_uri.split('/').next_back().unwrap()),
244244 ])
245245 .send()
246246 .await
+3-3
tests/oauth.rs
···2121 let code_verifier = URL_SAFE_NO_PAD.encode(verifier_bytes);
2222 let mut hasher = Sha256::new();
2323 hasher.update(code_verifier.as_bytes());
2424- let code_challenge = URL_SAFE_NO_PAD.encode(&hasher.finalize());
2424+ let code_challenge = URL_SAFE_NO_PAD.encode(hasher.finalize());
2525 (code_verifier, code_challenge)
2626}
2727···10361036 );
10371037 let body: Value = create_res.json().await.unwrap();
10381038 let uri = body["uri"].as_str().expect("Should have uri");
10391039- let rkey = uri.split('/').last().unwrap();
10391039+ let rkey = uri.split('/').next_back().unwrap();
10401040 let delete_res = http_client
10411041 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", url))
10421042 .bearer_auth(&token)
···10921092 );
10931093 let body: Value = post_res.json().await.unwrap();
10941094 let uri = body["uri"].as_str().unwrap();
10951095- let rkey = uri.split('/').last().unwrap();
10951095+ let rkey = uri.split('/').next_back().unwrap();
10961096 let delete_res = http_client
10971097 .post(format!("{}/xrpc/com.atproto.repo.deleteRecord", url))
10981098 .bearer_auth(&token)
+3-3
tests/oauth_lifecycle.rs
···1717 let mut hasher = Sha256::new();
1818 hasher.update(code_verifier.as_bytes());
1919 let hash = hasher.finalize();
2020- let code_challenge = URL_SAFE_NO_PAD.encode(&hash);
2020+ let code_challenge = URL_SAFE_NO_PAD.encode(hash);
2121 (code_verifier, code_challenge)
2222}
2323···195195 );
196196 let create_body: Value = create_res.json().await.unwrap();
197197 let uri = create_body["uri"].as_str().unwrap();
198198- let rkey = uri.split('/').last().unwrap();
198198+ let rkey = uri.split('/').next_back().unwrap();
199199 let get_res = http_client
200200 .get(format!("{}/xrpc/com.atproto.repo.getRecord", url))
201201 .bearer_auth(&session.access_token)
···290290 assert_eq!(create_res.status(), StatusCode::OK);
291291 let create_body: Value = create_res.json().await.unwrap();
292292 let uri = create_body["uri"].as_str().unwrap();
293293- let rkey = uri.split('/').last().unwrap();
293293+ let rkey = uri.split('/').next_back().unwrap();
294294 let updated_text = "Updated post content via OAuth putRecord";
295295 let put_res = http_client
296296 .post(format!("{}/xrpc/com.atproto.repo.putRecord", url))
+2-2
tests/oauth_scopes.rs
···1717 let mut hasher = Sha256::new();
1818 hasher.update(code_verifier.as_bytes());
1919 let hash = hasher.finalize();
2020- let code_challenge = URL_SAFE_NO_PAD.encode(&hash);
2020+ let code_challenge = URL_SAFE_NO_PAD.encode(hash);
2121 (code_verifier, code_challenge)
2222}
2323···215215 .as_str()
216216 .unwrap()
217217 .split('/')
218218- .last()
218218+ .next_back()
219219 .unwrap();
220220221221 let put_res = http_client