From 452d5538872e139b0b983e2365af8ff22e5ca3ff Mon Sep 17 00:00:00 2001 From: lewis Date: Tue, 30 Dec 2025 21:24:44 +0200 Subject: [PATCH] Reserved handles & misc fixes --- .gitignore | 1 + src/api/actor/preferences.rs | 69 +- src/api/identity/account.rs | 7 + src/api/proxy.rs | 35 + src/api/validation.rs | 80 ++- src/handle/mod.rs | 2 + src/handle/reserved.rs | 1097 +++++++++++++++++++++++++++++++ src/oauth/endpoints/metadata.rs | 1 + src/sync/deprecated.rs | 101 ++- tests/account_lifecycle.rs | 6 +- tests/actor.rs | 109 ++- tests/banned_words.rs | 12 +- tests/common/mod.rs | 2 +- tests/did_web.rs | 14 +- tests/email_update.rs | 24 +- tests/identity.rs | 8 +- tests/jwt_security.rs | 6 +- tests/oauth.rs | 44 +- tests/oauth_lifecycle.rs | 20 +- tests/oauth_scopes.rs | 26 +- tests/oauth_security.rs | 24 +- tests/password_reset.rs | 10 +- tests/server.rs | 2 +- tests/signing_key.rs | 10 +- tests/sync_deprecated.rs | 167 ++++- 25 files changed, 1690 insertions(+), 187 deletions(-) create mode 100644 src/handle/reserved.rs diff --git a/.gitignore b/.gitignore index cc26a7e..2163d71 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ reference-pds-hailey/ reference-pds-bsky/ reference-relay-indigo/ +pds-moover/ # Frontend build artifacts frontend/node_modules/ frontend/dist/ diff --git a/src/api/actor/preferences.rs b/src/api/actor/preferences.rs index 77b5ef5..baac0ed 100644 --- a/src/api/actor/preferences.rs +++ b/src/api/actor/preferences.rs @@ -5,12 +5,26 @@ use axum::{ http::StatusCode, response::{IntoResponse, Response}, }; +use chrono::{Datelike, NaiveDate, Utc}; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; const APP_BSKY_NAMESPACE: &str = "app.bsky"; const MAX_PREFERENCES_COUNT: usize = 100; const MAX_PREFERENCE_SIZE: usize = 10_000; +const PERSONAL_DETAILS_PREF: &str = "app.bsky.actor.defs#personalDetailsPref"; +const DECLARED_AGE_PREF: &str = "app.bsky.actor.defs#declaredAgePref"; + +fn get_age_from_datestring(birth_date: &str) -> Option { + let bday = NaiveDate::parse_from_str(birth_date, "%Y-%m-%d").ok()?; + let today = Utc::now().date_naive(); + let mut age = today.year() - bday.year(); + let m = today.month() as i32 - bday.month() as i32; + if m < 0 || (m == 0 && today.day() < bday.day()) { + age -= 1; + } + Some(age) +} #[derive(Serialize)] pub struct GetPreferencesOutput { @@ -43,6 +57,7 @@ pub async fn get_preferences( .into_response(); } }; + let has_full_access = auth_user.permissions().has_full_access(); let user_id: uuid::Uuid = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did) .fetch_optional(&state.db) @@ -73,19 +88,39 @@ pub async fn get_preferences( .into_response(); } }; - let preferences: Vec = prefs + let mut personal_details_pref: Option = None; + let mut preferences: Vec = prefs .into_iter() .filter(|row| { row.name == APP_BSKY_NAMESPACE || row.name.starts_with(&format!("{}.", APP_BSKY_NAMESPACE)) }) .filter_map(|row| { - if row.name == "app.bsky.actor.defs#declaredAgePref" { + if row.name == DECLARED_AGE_PREF { return None; } + if row.name == PERSONAL_DETAILS_PREF { + if !has_full_access { + return None; + } + personal_details_pref = serde_json::from_value(row.value_json.clone()).ok(); + } serde_json::from_value(row.value_json).ok() }) .collect(); + if let Some(ref pref) = personal_details_pref { + if let Some(birth_date) = pref.get("birthDate").and_then(|v| v.as_str()) { + if let Some(age) = get_age_from_datestring(birth_date) { + let declared_age_pref = json!({ + "$type": DECLARED_AGE_PREF, + "isOverAge13": age >= 13, + "isOverAge16": age >= 16, + "isOverAge18": age >= 18, + }); + preferences.push(declared_age_pref); + } + } + } (StatusCode::OK, Json(GetPreferencesOutput { preferences })).into_response() } @@ -121,14 +156,15 @@ pub async fn put_preferences( .into_response(); } }; - let (user_id, is_migration): (uuid::Uuid, bool) = match sqlx::query!( - "SELECT id, deactivated_at FROM users WHERE did = $1", + let has_full_access = auth_user.permissions().has_full_access(); + let user_id: uuid::Uuid = match sqlx::query_scalar!( + "SELECT id FROM users WHERE did = $1", auth_user.did ) .fetch_optional(&state.db) .await { - Ok(Some(row)) => (row.id, row.deactivated_at.is_some()), + Ok(Some(id)) => id, _ => { return ( StatusCode::INTERNAL_SERVER_ERROR, @@ -144,6 +180,7 @@ pub async fn put_preferences( ) .into_response(); } + let mut forbidden_prefs: Vec = Vec::new(); for pref in &input.preferences { let pref_str = serde_json::to_string(pref).unwrap_or_default(); if pref_str.len() > MAX_PREFERENCE_SIZE { @@ -158,7 +195,7 @@ pub async fn put_preferences( None => { return ( StatusCode::BAD_REQUEST, - Json(json!({"error": "InvalidRequest", "message": "Preference missing $type field"})), + Json(json!({"error": "InvalidRequest", "message": "Preference is missing a $type"})), ) .into_response(); } @@ -166,18 +203,21 @@ pub async fn put_preferences( if !pref_type.starts_with(APP_BSKY_NAMESPACE) { return ( StatusCode::BAD_REQUEST, - Json(json!({"error": "InvalidRequest", "message": format!("Invalid preference namespace: {}", pref_type)})), + Json(json!({"error": "InvalidRequest", "message": format!("Some preferences are not in the {} namespace", APP_BSKY_NAMESPACE)})), ) .into_response(); } - if pref_type == "app.bsky.actor.defs#declaredAgePref" && !is_migration { - return ( - StatusCode::BAD_REQUEST, - Json(json!({"error": "InvalidRequest", "message": "declaredAgePref is read-only"})), - ) - .into_response(); + if pref_type == PERSONAL_DETAILS_PREF && !has_full_access { + forbidden_prefs.push(pref_type.to_string()); } } + if !forbidden_prefs.is_empty() { + return ( + StatusCode::BAD_REQUEST, + Json(json!({"error": "InvalidRequest", "message": format!("Do not have authorization to set preferences: {}", forbidden_prefs.join(", "))})), + ) + .into_response(); + } let mut tx = match state.db.begin().await { Ok(tx) => tx, Err(_) => { @@ -209,6 +249,9 @@ pub async fn put_preferences( Some(t) => t, None => continue, }; + if pref_type == DECLARED_AGE_PREF { + continue; + } let insert_result = sqlx::query!( "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)", user_id, diff --git a/src/api/identity/account.rs b/src/api/identity/account.rs index d751aa6..805be63 100644 --- a/src/api/identity/account.rs +++ b/src/api/identity/account.rs @@ -188,6 +188,13 @@ pub async fn create_account( }; match crate::api::validation::validate_short_handle(handle_to_validate) { Ok(h) => h, + Err(crate::api::validation::HandleValidationError::Reserved) => { + return ( + StatusCode::BAD_REQUEST, + Json(json!({"error": "HandleNotAvailable", "message": "Reserved handle"})), + ) + .into_response(); + } Err(e) => { return ( StatusCode::BAD_REQUEST, diff --git a/src/api/proxy.rs b/src/api/proxy.rs index 8b5e32c..e994954 100644 --- a/src/api/proxy.rs +++ b/src/api/proxy.rs @@ -10,6 +10,29 @@ use axum::{ use serde_json::json; use tracing::{error, info, warn}; +const PROTECTED_METHODS: &[&str] = &[ + "com.atproto.admin.sendEmail", + "com.atproto.identity.requestPlcOperationSignature", + "com.atproto.identity.signPlcOperation", + "com.atproto.identity.updateHandle", + "com.atproto.server.activateAccount", + "com.atproto.server.confirmEmail", + "com.atproto.server.createAppPassword", + "com.atproto.server.deactivateAccount", + "com.atproto.server.getAccountInviteCodes", + "com.atproto.server.getSession", + "com.atproto.server.listAppPasswords", + "com.atproto.server.requestAccountDelete", + "com.atproto.server.requestEmailConfirmation", + "com.atproto.server.requestEmailUpdate", + "com.atproto.server.revokeAppPassword", + "com.atproto.server.updateEmail", +]; + +fn is_protected_method(method: &str) -> bool { + PROTECTED_METHODS.contains(&method) +} + pub async fn proxy_handler( State(state): State, Path(method): Path, @@ -18,6 +41,18 @@ pub async fn proxy_handler( RawQuery(query): RawQuery, body: Bytes, ) -> Response { + if is_protected_method(&method) { + warn!(method = %method, "Attempted to proxy protected method"); + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "error": "InvalidRequest", + "message": format!("Cannot proxy protected method: {}", method) + })), + ) + .into_response(); + } + let proxy_header = match headers.get("atproto-proxy").and_then(|h| h.to_str().ok()) { Some(h) => h.to_string(), None => { diff --git a/src/api/validation.rs b/src/api/validation.rs index d85a9b9..f0956cb 100644 --- a/src/api/validation.rs +++ b/src/api/validation.rs @@ -6,6 +6,7 @@ const EMAIL_LOCAL_SPECIAL_CHARS: &str = ".!#$%&'*+/=?^_`{|}~-"; pub const MIN_HANDLE_LENGTH: usize = 3; pub const MAX_HANDLE_LENGTH: usize = 253; +pub const MAX_SERVICE_HANDLE_LOCAL_PART: usize = 18; #[derive(Debug, PartialEq)] pub enum HandleValidationError { @@ -17,6 +18,7 @@ pub enum HandleValidationError { EndsWithInvalidChar, ContainsSpaces, BannedWord, + Reserved, } impl std::fmt::Display for HandleValidationError { @@ -31,7 +33,7 @@ impl std::fmt::Display for HandleValidationError { Self::TooLong => write!( f, "Handle exceeds maximum length of {} characters", - MAX_HANDLE_LENGTH + MAX_SERVICE_HANDLE_LOCAL_PART ), Self::InvalidCharacters => write!( f, @@ -43,11 +45,19 @@ impl std::fmt::Display for HandleValidationError { Self::EndsWithInvalidChar => write!(f, "Handle cannot end with a hyphen"), Self::ContainsSpaces => write!(f, "Handle cannot contain spaces"), Self::BannedWord => write!(f, "Inappropriate language in handle"), + Self::Reserved => write!(f, "Reserved handle"), } } } pub fn validate_short_handle(handle: &str) -> Result { + validate_service_handle(handle, false) +} + +pub fn validate_service_handle( + handle: &str, + allow_reserved: bool, +) -> Result { let handle = handle.trim(); if handle.is_empty() { @@ -62,7 +72,7 @@ pub fn validate_short_handle(handle: &str) -> Result MAX_HANDLE_LENGTH { + if handle.len() > MAX_SERVICE_HANDLE_LOCAL_PART { return Err(HandleValidationError::TooLong); } @@ -88,6 +98,10 @@ pub fn validate_short_handle(handle: &str) -> Result> = LazyLock::new(|| { + let mut set = HashSet::with_capacity( + ATP_SPECIFIC.len() + COMMONLY_RESERVED.len() + FAMOUS_ACCOUNTS.len(), + ); + for s in ATP_SPECIFIC { + set.insert(*s); + } + for s in COMMONLY_RESERVED { + set.insert(*s); + } + for s in FAMOUS_ACCOUNTS { + set.insert(*s); + } + set +}); + +pub fn is_reserved_subdomain(subdomain: &str) -> bool { + RESERVED_SUBDOMAINS.contains(subdomain.to_lowercase().as_str()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_atp_specific_reserved() { + assert!(is_reserved_subdomain("admin")); + assert!(is_reserved_subdomain("api")); + assert!(is_reserved_subdomain("bsky")); + assert!(is_reserved_subdomain("plc")); + assert!(is_reserved_subdomain("xrpc")); + } + + #[test] + fn test_famous_accounts_reserved() { + assert!(is_reserved_subdomain("barackobama")); + assert!(is_reserved_subdomain("elonmusk")); + assert!(is_reserved_subdomain("taylorswift")); + assert!(is_reserved_subdomain("nintendo")); + } + + #[test] + fn test_case_insensitive() { + assert!(is_reserved_subdomain("ADMIN")); + assert!(is_reserved_subdomain("Admin")); + assert!(is_reserved_subdomain("BARACKOBAMA")); + } + + #[test] + fn test_not_reserved() { + assert!(!is_reserved_subdomain("alice")); + assert!(!is_reserved_subdomain("bob123")); + assert!(!is_reserved_subdomain("randomuser")); + } +} diff --git a/src/oauth/endpoints/metadata.rs b/src/oauth/endpoints/metadata.rs index 942d514..87e390c 100644 --- a/src/oauth/endpoints/metadata.rs +++ b/src/oauth/endpoints/metadata.rs @@ -81,6 +81,7 @@ pub async fn oauth_authorization_server( "atproto".to_string(), "transition:generic".to_string(), "transition:chat.bsky".to_string(), + "transition:email".to_string(), "repo:*".to_string(), "repo:*?action=create".to_string(), "repo:*?action=read".to_string(), diff --git a/src/sync/deprecated.rs b/src/sync/deprecated.rs index 0439d29..2aad9a8 100644 --- a/src/sync/deprecated.rs +++ b/src/sync/deprecated.rs @@ -1,9 +1,11 @@ +use crate::auth::{extract_bearer_token_from_header, validate_bearer_token_allow_takendown}; use crate::state::AppState; use crate::sync::car::encode_car_header; +use crate::sync::util::assert_repo_availability; use axum::{ Json, extract::{Query, State}, - http::StatusCode, + http::{HeaderMap, StatusCode}, response::{IntoResponse, Response}, }; use cid::Cid; @@ -13,10 +15,22 @@ use serde::{Deserialize, Serialize}; use serde_json::json; use std::io::Write; use std::str::FromStr; -use tracing::error; const MAX_REPO_BLOCKS_TRAVERSAL: usize = 20_000; +async fn check_admin_or_self(state: &AppState, headers: &HeaderMap, did: &str) -> bool { + let token = match extract_bearer_token_from_header( + headers.get("Authorization").and_then(|h| h.to_str().ok()), + ) { + Some(t) => t, + None => return false, + }; + match validate_bearer_token_allow_takendown(&state.db, &token).await { + Ok(auth_user) => auth_user.is_admin || auth_user.did == did, + Err(_) => false, + } +} + #[derive(Deserialize)] pub struct GetHeadParams { pub did: String, @@ -29,6 +43,7 @@ pub struct GetHeadOutput { pub async fn get_head( State(state): State, + headers: HeaderMap, Query(params): Query, ) -> Response { let did = params.did.trim(); @@ -39,38 +54,18 @@ pub async fn get_head( ) .into_response(); } - let result = sqlx::query!( - r#" - SELECT r.repo_root_cid - FROM repos r - JOIN users u ON r.user_id = u.id - WHERE u.did = $1 - "#, - did - ) - .fetch_optional(&state.db) - .await; - match result { - Ok(Some(row)) => ( - StatusCode::OK, - Json(GetHeadOutput { - root: row.repo_root_cid, - }), - ) - .into_response(), - Ok(None) => ( + let is_admin_or_self = check_admin_or_self(&state, &headers, did).await; + let account = match assert_repo_availability(&state.db, did, is_admin_or_self).await { + Ok(a) => a, + Err(e) => return e.into_response(), + }; + match account.repo_root_cid { + Some(root) => (StatusCode::OK, Json(GetHeadOutput { root })).into_response(), + None => ( StatusCode::BAD_REQUEST, - Json(json!({"error": "HeadNotFound", "message": "Could not find root for DID"})), + Json(json!({"error": "HeadNotFound", "message": format!("Could not find root for DID: {}", did)})), ) .into_response(), - Err(e) => { - error!("DB error in get_head: {:?}", e); - ( - StatusCode::INTERNAL_SERVER_ERROR, - Json(json!({"error": "InternalError"})), - ) - .into_response() - } } } @@ -81,6 +76,7 @@ pub struct GetCheckoutParams { pub async fn get_checkout( State(state): State, + headers: HeaderMap, Query(params): Query, ) -> Response { let did = params.did.trim(); @@ -91,38 +87,19 @@ pub async fn get_checkout( ) .into_response(); } - let repo_row = sqlx::query!( - r#" - SELECT r.repo_root_cid - FROM repos r - JOIN users u ON u.id = r.user_id - WHERE u.did = $1 - "#, - did - ) - .fetch_optional(&state.db) - .await - .unwrap_or(None); - let head_str = match repo_row { - Some(r) => r.repo_root_cid, + let is_admin_or_self = check_admin_or_self(&state, &headers, did).await; + let account = match assert_repo_availability(&state.db, did, is_admin_or_self).await { + Ok(a) => a, + Err(e) => return e.into_response(), + }; + let head_str = match account.repo_root_cid { + Some(r) => r, None => { - let user_exists = sqlx::query!("SELECT id FROM users WHERE did = $1", did) - .fetch_optional(&state.db) - .await - .unwrap_or(None); - if user_exists.is_none() { - return ( - StatusCode::NOT_FOUND, - Json(json!({"error": "RepoNotFound", "message": "Repo not found"})), - ) - .into_response(); - } else { - return ( - StatusCode::NOT_FOUND, - Json(json!({"error": "RepoNotFound", "message": "Repo not initialized"})), - ) - .into_response(); - } + return ( + StatusCode::BAD_REQUEST, + Json(json!({"error": "RepoNotFound", "message": "Repo not initialized"})), + ) + .into_response(); } }; let head_cid = match Cid::from_str(&head_str) { diff --git a/tests/account_lifecycle.rs b/tests/account_lifecycle.rs index 187410d..e5bfbc6 100644 --- a/tests/account_lifecycle.rs +++ b/tests/account_lifecycle.rs @@ -154,7 +154,7 @@ async fn test_create_account_returns_did_doc() { let client = client(); let base = base_url().await; - let handle = format!("diddoctest-{}", uuid::Uuid::new_v4()); + let handle = format!("dd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), @@ -185,7 +185,7 @@ async fn test_create_account_always_returns_tokens() { let client = client(); let base = base_url().await; - let handle = format!("tokentest-{}", uuid::Uuid::new_v4()); + let handle = format!("tt{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), @@ -243,7 +243,7 @@ async fn test_delete_account_password_max_length() { let client = client(); let base = base_url().await; - let handle = format!("pwdlentest-{}", uuid::Uuid::new_v4()); + let handle = format!("pl{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), diff --git a/tests/actor.rs b/tests/actor.rs index 3cdc0af..dd67be8 100644 --- a/tests/actor.rs +++ b/tests/actor.rs @@ -140,7 +140,7 @@ async fn test_put_preferences_invalid_namespace() { } #[tokio::test] -async fn test_put_preferences_read_only_rejected() { +async fn test_put_preferences_read_only_silently_filtered() { let client = client(); let base = base_url().await; let (token, _did) = create_account_and_login(&client).await; @@ -149,6 +149,10 @@ async fn test_put_preferences_read_only_rejected() { { "$type": "app.bsky.actor.defs#declaredAgePref", "isOverAge18": true + }, + { + "$type": "app.bsky.actor.defs#adultContentPref", + "enabled": true } ] }); @@ -159,9 +163,18 @@ async fn test_put_preferences_read_only_rejected() { .send() .await .unwrap(); - assert_eq!(resp.status(), 400); - let body: Value = resp.json().await.unwrap(); - assert_eq!(body["error"], "InvalidRequest"); + assert_eq!(resp.status(), 200); + let get_resp = client + .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .unwrap(); + assert_eq!(get_resp.status(), 200); + let body: Value = get_resp.json().await.unwrap(); + let prefs_arr = body["preferences"].as_array().unwrap(); + assert_eq!(prefs_arr.len(), 1); + assert_eq!(prefs_arr[0]["$type"], "app.bsky.actor.defs#adultContentPref"); } #[tokio::test] @@ -328,3 +341,91 @@ async fn test_preferences_isolation_between_users() { let body: Value = resp.json().await.unwrap(); assert!(body["preferences"].as_array().unwrap().is_empty()); } + +#[tokio::test] +async fn test_declared_age_pref_computed_from_birth_date() { + let client = client(); + let base = base_url().await; + let (token, _did) = create_account_and_login(&client).await; + let prefs = json!({ + "preferences": [ + { + "$type": "app.bsky.actor.defs#personalDetailsPref", + "birthDate": "1990-01-15" + } + ] + }); + let resp = client + .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) + .header("Authorization", format!("Bearer {}", token)) + .json(&prefs) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); + let get_resp = client + .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .unwrap(); + assert_eq!(get_resp.status(), 200); + let body: Value = get_resp.json().await.unwrap(); + let prefs_arr = body["preferences"].as_array().unwrap(); + assert_eq!(prefs_arr.len(), 2); + let personal_details = prefs_arr + .iter() + .find(|p| p["$type"] == "app.bsky.actor.defs#personalDetailsPref"); + assert!(personal_details.is_some()); + assert_eq!(personal_details.unwrap()["birthDate"], "1990-01-15"); + let declared_age = prefs_arr + .iter() + .find(|p| p["$type"] == "app.bsky.actor.defs#declaredAgePref"); + assert!(declared_age.is_some()); + let declared_age = declared_age.unwrap(); + assert_eq!(declared_age["isOverAge13"], true); + assert_eq!(declared_age["isOverAge16"], true); + assert_eq!(declared_age["isOverAge18"], true); +} + +#[tokio::test] +async fn test_declared_age_pref_computed_under_18() { + let client = client(); + let base = base_url().await; + let (token, _did) = create_account_and_login(&client).await; + let current_year = chrono::Utc::now().format("%Y").to_string().parse::().unwrap(); + let birth_year = current_year - 15; + let prefs = json!({ + "preferences": [ + { + "$type": "app.bsky.actor.defs#personalDetailsPref", + "birthDate": format!("{}-06-15", birth_year) + } + ] + }); + let resp = client + .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) + .header("Authorization", format!("Bearer {}", token)) + .json(&prefs) + .send() + .await + .unwrap(); + assert_eq!(resp.status(), 200); + let get_resp = client + .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) + .header("Authorization", format!("Bearer {}", token)) + .send() + .await + .unwrap(); + assert_eq!(get_resp.status(), 200); + let body: Value = get_resp.json().await.unwrap(); + let prefs_arr = body["preferences"].as_array().unwrap(); + let declared_age = prefs_arr + .iter() + .find(|p| p["$type"] == "app.bsky.actor.defs#declaredAgePref"); + assert!(declared_age.is_some()); + let declared_age = declared_age.unwrap(); + assert_eq!(declared_age["isOverAge13"], true); + assert_eq!(declared_age["isOverAge16"], false); + assert_eq!(declared_age["isOverAge18"], false); +} diff --git a/tests/banned_words.rs b/tests/banned_words.rs index 4d5663f..4783d2e 100644 --- a/tests/banned_words.rs +++ b/tests/banned_words.rs @@ -31,13 +31,13 @@ fn decode(b64: &str) -> String { #[tokio::test] async fn test_handle_with_slur_rejected() { let client = client(); - let timestamp = chrono::Utc::now().timestamp_millis(); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; let slur = decode("bmlnZ2Vy"); - let offensive_handle = format!("{}{}", slur, timestamp); + let offensive_handle = format!("{}{}", slur, suffix); let create_payload = json!({ "handle": offensive_handle, - "email": format!("test{}@example.com", timestamp), + "email": format!("test{}@example.com", suffix), "password": "TestPassword123!" }); @@ -65,13 +65,13 @@ async fn test_handle_with_slur_rejected() { #[tokio::test] async fn test_handle_with_normalized_slur_rejected() { let client = client(); - let timestamp = chrono::Utc::now().timestamp_millis(); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..6]; let slur = decode("bi1pLWctZy1lLXI="); - let offensive_handle = format!("{}{}", slur, timestamp); + let offensive_handle = format!("{}{}", slur, suffix); let create_payload = json!({ "handle": offensive_handle, - "email": format!("test{}@example.com", timestamp), + "email": format!("test{}@example.com", suffix), "password": "TestPassword123!" }); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index a2a3c76..4607254 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -440,7 +440,7 @@ async fn create_account_and_login_internal(client: &Client, make_admin: bool) -> if attempt > 0 { tokio::time::sleep(Duration::from_millis(100 * (attempt as u64 + 1))).await; } - let handle = format!("user-{}", uuid::Uuid::new_v4()); + let handle = format!("u{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), diff --git a/tests/did_web.rs b/tests/did_web.rs index 26fa03c..fa5bec0 100644 --- a/tests/did_web.rs +++ b/tests/did_web.rs @@ -11,7 +11,7 @@ use wiremock::{Mock, MockServer, ResponseTemplate}; #[tokio::test] async fn test_create_self_hosted_did_web() { let client = client(); - let handle = format!("selfweb-{}", uuid::Uuid::new_v4()); + let handle = format!("sw{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), @@ -98,7 +98,7 @@ async fn test_external_did_web_no_local_doc() { let mock_uri = mock_server.uri(); let mock_addr = mock_uri.trim_start_matches("http://"); let did = format!("did:web:{}", mock_addr.replace(":", "%3A")); - let handle = format!("extweb-{}", uuid::Uuid::new_v4()); + let handle = format!("xw{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let pds_endpoint = base_url().await.replace("http://", "https://"); let reserve_res = client @@ -180,7 +180,7 @@ async fn test_external_did_web_no_local_doc() { #[tokio::test] async fn test_plc_operations_blocked_for_did_web() { let client = client(); - let handle = format!("plcblock-{}", uuid::Uuid::new_v4()); + let handle = format!("pb{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), @@ -245,7 +245,7 @@ async fn test_plc_operations_blocked_for_did_web() { #[tokio::test] async fn test_get_recommended_did_credentials_no_rotation_keys_for_did_web() { let client = client(); - let handle = format!("creds-{}", uuid::Uuid::new_v4()); + let handle = format!("cr{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), @@ -294,7 +294,7 @@ async fn test_get_recommended_did_credentials_no_rotation_keys_for_did_web() { #[tokio::test] async fn test_did_plc_still_works_with_did_type_param() { let client = client(); - let handle = format!("plctype-{}", uuid::Uuid::new_v4()); + let handle = format!("pt{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), @@ -323,7 +323,7 @@ async fn test_did_plc_still_works_with_did_type_param() { #[tokio::test] async fn test_external_did_web_requires_did_field() { let client = client(); - let handle = format!("nodid-{}", uuid::Uuid::new_v4()); + let handle = format!("nd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), @@ -392,7 +392,7 @@ async fn test_did_web_byod_flow() { mock_addr.replace(":", "%3A"), unique_id ); - let handle = format!("byod-{}", uuid::Uuid::new_v4()); + let handle = format!("by{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let pds_endpoint = base_url().await.replace("http://", "https://"); let pds_did = format!("did:web:{}", pds_endpoint.trim_start_matches("https://")); diff --git a/tests/email_update.rs b/tests/email_update.rs index 1bfd5b6..617da14 100644 --- a/tests/email_update.rs +++ b/tests/email_update.rs @@ -57,7 +57,7 @@ async fn create_verified_account( async fn test_request_email_update_returns_token_required() { let client = common::client(); let base_url = common::base_url().await; - let handle = format!("emailreq-{}", uuid::Uuid::new_v4()); + let handle = format!("er{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email = format!("{}@example.com", handle); let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; @@ -80,7 +80,7 @@ async fn test_update_email_flow_success() { let client = common::client(); let base_url = common::base_url().await; let pool = common::get_test_db_pool().await; - let handle = format!("emailup-{}", uuid::Uuid::new_v4()); + let handle = format!("eu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email = format!("{}@example.com", handle); let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await; let new_email = format!("new_{}@example.com", handle); @@ -123,7 +123,7 @@ async fn test_update_email_flow_success() { async fn test_update_email_requires_token_when_verified() { let client = common::client(); let base_url = common::base_url().await; - let handle = format!("emailup-direct-{}", uuid::Uuid::new_v4()); + let handle = format!("ed{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email = format!("{}@example.com", handle); let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; let new_email = format!("direct_{}@example.com", handle); @@ -144,7 +144,7 @@ async fn test_update_email_requires_token_when_verified() { async fn test_update_email_same_email_noop() { let client = common::client(); let base_url = common::base_url().await; - let handle = format!("emailup-same-{}", uuid::Uuid::new_v4()); + let handle = format!("es{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email = format!("{}@example.com", handle); let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; @@ -166,7 +166,7 @@ async fn test_update_email_same_email_noop() { async fn test_update_email_invalid_token() { let client = common::client(); let base_url = common::base_url().await; - let handle = format!("emailup-badtok-{}", uuid::Uuid::new_v4()); + let handle = format!("eb{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email = format!("{}@example.com", handle); let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; let new_email = format!("badtok_{}@example.com", handle); @@ -217,7 +217,7 @@ async fn test_update_email_no_auth() { async fn test_update_email_invalid_format() { let client = common::client(); let base_url = common::base_url().await; - let handle = format!("emailup-fmt-{}", uuid::Uuid::new_v4()); + let handle = format!("ef{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email = format!("{}@example.com", handle); let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; @@ -236,7 +236,7 @@ async fn test_confirm_email_confirms_existing_email() { let client = common::client(); let base_url = common::base_url().await; let pool = common::get_test_db_pool().await; - let handle = format!("emailconfirm-{}", uuid::Uuid::new_v4()); + let handle = format!("ec{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email = format!("{}@example.com", handle); let res = client @@ -298,7 +298,7 @@ async fn test_confirm_email_rejects_wrong_email() { let client = common::client(); let base_url = common::base_url().await; let pool = common::get_test_db_pool().await; - let handle = format!("emailconf-wrong-{}", uuid::Uuid::new_v4()); + let handle = format!("ew{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email = format!("{}@example.com", handle); let res = client @@ -352,7 +352,7 @@ async fn test_confirm_email_rejects_wrong_email() { async fn test_confirm_email_invalid_token() { let client = common::client(); let base_url = common::base_url().await; - let handle = format!("emailconf-inv-{}", uuid::Uuid::new_v4()); + let handle = format!("ei{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email = format!("{}@example.com", handle); let res = client @@ -392,7 +392,7 @@ async fn test_unverified_account_can_update_email_without_token() { let client = common::client(); let base_url = common::base_url().await; let pool = common::get_test_db_pool().await; - let handle = format!("emailup-unverified-{}", uuid::Uuid::new_v4()); + let handle = format!("ev{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email = format!("{}@example.com", handle); let res = client @@ -457,11 +457,11 @@ async fn test_update_email_taken_by_another_user() { let base_url = common::base_url().await; let pool = common::get_test_db_pool().await; - let handle1 = format!("emailup-dup1-{}", uuid::Uuid::new_v4()); + let handle1 = format!("d1{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email1 = format!("{}@example.com", handle1); let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await; - let handle2 = format!("emailup-dup2-{}", uuid::Uuid::new_v4()); + let handle2 = format!("d2{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email2 = format!("{}@example.com", handle2); let (access_jwt2, did2) = create_verified_account(&client, &base_url, &handle2, &email2).await; diff --git a/tests/identity.rs b/tests/identity.rs index d883547..336a90a 100644 --- a/tests/identity.rs +++ b/tests/identity.rs @@ -8,7 +8,7 @@ use wiremock::{Mock, MockServer, ResponseTemplate}; #[tokio::test] async fn test_resolve_handle_success() { let client = client(); - let short_handle = format!("resolvetest-{}", uuid::Uuid::new_v4()); + let short_handle = format!("rt{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let payload = json!({ "handle": short_handle, "email": format!("{}@example.com", short_handle), @@ -98,7 +98,7 @@ async fn test_create_did_web_account_and_resolve() { let mock_uri = mock_server.uri(); let mock_addr = mock_uri.trim_start_matches("http://"); let did = format!("did:web:{}", mock_addr.replace(":", "%3A")); - let handle = format!("webuser-{}", uuid::Uuid::new_v4()); + let handle = format!("wu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let pds_endpoint = base_url().await.replace("http://", "https://"); let reserve_res = client @@ -183,7 +183,7 @@ async fn test_create_did_web_account_and_resolve() { #[tokio::test] async fn test_create_account_duplicate_handle() { let client = client(); - let handle = format!("dupe-{}", uuid::Uuid::new_v4()); + let handle = format!("dp{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email = format!("{}@example.com", handle); let payload = json!({ "handle": handle, @@ -220,7 +220,7 @@ async fn test_did_web_lifecycle() { let mock_server = MockServer::start().await; let mock_uri = mock_server.uri(); let mock_addr = mock_uri.trim_start_matches("http://"); - let handle = format!("lifecycle-{}", uuid::Uuid::new_v4()); + let handle = format!("lc{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let did = format!("did:web:{}:u:{}", mock_addr.replace(":", "%3A"), handle); let email = format!("{}@test.com", handle); let pds_endpoint = base_url().await.replace("http://", "https://"); diff --git a/tests/jwt_security.rs b/tests/jwt_security.rs index f3c9540..e20972a 100644 --- a/tests/jwt_security.rs +++ b/tests/jwt_security.rs @@ -669,9 +669,9 @@ async fn test_deactivated_account_behavior() { async fn test_refresh_token_replay_protection() { let url = base_url().await; let http_client = client(); - let ts = Utc::now().timestamp_millis(); - let handle = format!("rt-replay-jwt-{}", ts); - let email = format!("rt-replay-jwt-{}@example.com", ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle = format!("rr{}", suffix); + let email = format!("rr{}@example.com", suffix); let create_res = http_client .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) diff --git a/tests/oauth.rs b/tests/oauth.rs index 752d2ca..3f0f70a 100644 --- a/tests/oauth.rs +++ b/tests/oauth.rs @@ -191,9 +191,9 @@ async fn test_par_and_authorize() { async fn test_full_oauth_flow() { let url = base_url().await; let http_client = client(); - let ts = Utc::now().timestamp_millis(); - let handle = format!("oauth-test-{}", ts); - let email = format!("oauth-test-{}@example.com", ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle = format!("ot{}", suffix); + let email = format!("ot{}@example.com", suffix); let password = "Oauthtest123!"; let create_res = http_client .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) @@ -209,7 +209,7 @@ async fn test_full_oauth_flow() { let mock_client = setup_mock_client_metadata(redirect_uri).await; let client_id = mock_client.uri(); let (code_verifier, code_challenge) = generate_pkce(); - let state = format!("state-{}", ts); + let state = format!("state-{}", suffix); let par_res = http_client .post(format!("{}/oauth/par", url)) .form(&[ @@ -349,9 +349,9 @@ async fn test_full_oauth_flow() { async fn test_oauth_error_cases() { let url = base_url().await; let http_client = client(); - let ts = Utc::now().timestamp_millis(); - let handle = format!("wrong-creds-{}", ts); - let email = format!("wrong-creds-{}@example.com", ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle = format!("wc{}", suffix); + let email = format!("wc{}@example.com", suffix); http_client .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) .json(&json!({ "handle": handle, "email": email, "password": "Correct123!" })) @@ -435,9 +435,9 @@ async fn test_oauth_error_cases() { async fn test_oauth_2fa_flow() { let url = base_url().await; let http_client = client(); - let ts = Utc::now().timestamp_millis(); - let handle = format!("2fa-test-{}", ts); - let email = format!("2fa-test-{}@example.com", ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle = format!("ft{}", suffix); + let email = format!("ft{}@example.com", suffix); let password = "Twofa123test!"; let create_res = http_client .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) @@ -557,9 +557,9 @@ async fn test_oauth_2fa_flow() { async fn test_oauth_2fa_lockout() { let url = base_url().await; let http_client = client(); - let ts = Utc::now().timestamp_millis(); - let handle = format!("2fa-lockout-{}", ts); - let email = format!("2fa-lockout-{}@example.com", ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle = format!("fl{}", suffix); + let email = format!("fl{}@example.com", suffix); let password = "Twofa123test!"; let create_res = http_client .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) @@ -649,9 +649,9 @@ async fn test_oauth_2fa_lockout() { async fn test_account_selector_with_2fa() { let url = base_url().await; let http_client = client(); - let ts = Utc::now().timestamp_millis(); - let handle = format!("selector-2fa-{}", ts); - let email = format!("selector-2fa-{}@example.com", ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle = format!("sf{}", suffix); + let email = format!("sf{}@example.com", suffix); let password = "Selector2fa123!"; let create_res = http_client .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) @@ -835,9 +835,9 @@ async fn test_account_selector_with_2fa() { async fn test_oauth_state_encoding() { let url = base_url().await; let http_client = client(); - let ts = Utc::now().timestamp_millis(); - let handle = format!("state-special-{}", ts); - let email = format!("state-special-{}@example.com", ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle = format!("ss{}", suffix); + let email = format!("ss{}@example.com", suffix); let password = "State123special!"; let create_res = http_client .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) @@ -914,9 +914,9 @@ async fn test_oauth_state_encoding() { async fn get_oauth_token_with_scope(scope: &str) -> (String, String, String) { let url = base_url().await; let http_client = client(); - let ts = Utc::now().timestamp_millis(); - let handle = format!("scope-test-{}", ts); - let email = format!("scope-test-{}@example.com", ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle = format!("st{}", suffix); + let email = format!("st{}@example.com", suffix); let password = "Scopetest123!"; let create_res = http_client .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) diff --git a/tests/oauth_lifecycle.rs b/tests/oauth_lifecycle.rs index 25adfd9..5468bc7 100644 --- a/tests/oauth_lifecycle.rs +++ b/tests/oauth_lifecycle.rs @@ -54,9 +54,9 @@ async fn create_user_and_oauth_session( ) -> (OAuthSession, MockServer) { let url = base_url().await; let http_client = client(); - let ts = Utc::now().timestamp_millis(); - let handle = format!("{}-{}", handle_prefix, ts); - let email = format!("{}-{}@example.com", handle_prefix, ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..4]; + let handle = format!("{}{}", handle_prefix, suffix); + let email = format!("{}{}@example.com", handle_prefix, suffix); let password = format!("{}Pass123!", handle_prefix); let create_res = http_client .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) @@ -269,7 +269,7 @@ async fn test_oauth_full_post_lifecycle_create_edit_delete() { let url = base_url().await; let http_client = client(); let (session, _mock) = - create_user_and_oauth_session("oauth-lifecycle", "https://example.com/callback").await; + create_user_and_oauth_session("oauthlife", "https://example.com/callback").await; let collection = "app.bsky.feed.post"; let original_text = "Original post content"; let create_res = http_client @@ -439,7 +439,7 @@ async fn test_oauth_token_refresh_maintains_access() { let url = base_url().await; let http_client = client(); let (session, _mock) = - create_user_and_oauth_session("oauth-refresh-access", "https://example.com/callback").await; + create_user_and_oauth_session("oauth-refr", "https://example.com/callback").await; let collection = "app.bsky.feed.post"; let create_res = http_client .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) @@ -520,7 +520,7 @@ async fn test_oauth_revoked_token_cannot_access_resources() { let url = base_url().await; let http_client = client(); let (session, _mock) = - create_user_and_oauth_session("oauth-revoke-access", "https://example.com/callback").await; + create_user_and_oauth_session("oauth-revo", "https://example.com/callback").await; let collection = "app.bsky.feed.post"; let create_res = http_client .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) @@ -574,9 +574,9 @@ async fn test_oauth_revoked_token_cannot_access_resources() { async fn test_oauth_multiple_clients_same_user() { let url = base_url().await; let http_client = client(); - let ts = Utc::now().timestamp_millis(); - let handle = format!("multi-client-{}", ts); - let email = format!("multi-client-{}@example.com", ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle = format!("mc{}", suffix); + let email = format!("mc{}@example.com", suffix); let password = "MultiClient123!"; let create_res = http_client .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) @@ -949,7 +949,7 @@ async fn test_oauth_session_isolation_between_users() { let url = base_url().await; let http_client = client(); let (alice, _mock_alice) = - create_user_and_oauth_session("alice-isolation", "https://alice.example.com/callback") + create_user_and_oauth_session("alice-isol", "https://alice.example.com/callback") .await; let (bob, _mock_bob) = create_user_and_oauth_session("bob-isolation", "https://bob.example.com/callback").await; diff --git a/tests/oauth_scopes.rs b/tests/oauth_scopes.rs index 40abc62..e1a0f18 100644 --- a/tests/oauth_scopes.rs +++ b/tests/oauth_scopes.rs @@ -58,9 +58,9 @@ async fn create_user_and_oauth_session_with_scope( ) -> (OAuthSession, MockServer) { let url = base_url().await; let http_client = client(); - let ts = Utc::now().timestamp_millis(); - let handle = format!("{}-{}", handle_prefix, ts); - let email = format!("{}-{}@example.com", handle_prefix, ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..4]; + let handle = format!("{}{}", handle_prefix, suffix); + let email = format!("{}{}@example.com", handle_prefix, suffix); let password = format!("{}Pass123!", handle_prefix); let create_res = http_client @@ -345,7 +345,7 @@ async fn test_transition_generic_scope_allows_access() { let url = base_url().await; let http_client = client(); let (session, _mock) = create_user_and_oauth_session_with_scope( - "scope-transition", + "scope-trans", "https://example.com/callback", "atproto transition:generic", ) @@ -380,9 +380,9 @@ async fn test_consent_endpoint_returns_scope_info() { let url = base_url().await; let http_client = client(); - let ts = Utc::now().timestamp_millis(); - let handle = format!("consent-test-{}", ts); - let email = format!("consent-{}@example.com", ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle = format!("ct{}", suffix); + let email = format!("ct{}@example.com", suffix); let password = "Consent123!"; let redirect_uri = "https://consent-test.example.com/callback"; @@ -476,9 +476,9 @@ async fn test_consent_post_generates_code() { let url = base_url().await; let http_client = client(); - let ts = Utc::now().timestamp_millis(); - let handle = format!("consent-post-{}", ts); - let email = format!("consent-post-{}@example.com", ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle = format!("cp{}", suffix); + let email = format!("cp{}@example.com", suffix); let password = "ConsentPost123!"; let redirect_uri = "https://consent-post.example.com/callback"; @@ -590,9 +590,9 @@ async fn test_consent_post_requires_atproto_scope() { let url = base_url().await; let http_client = client(); - let ts = Utc::now().timestamp_millis(); - let handle = format!("consent-req-{}", ts); - let email = format!("consent-req-{}@example.com", ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle = format!("cq{}", suffix); + let email = format!("cq{}@example.com", suffix); let password = "ConsentReq123!"; let redirect_uri = "https://consent-req.example.com/callback"; diff --git a/tests/oauth_security.rs b/tests/oauth_security.rs index 96e3ecf..0f6b05e 100644 --- a/tests/oauth_security.rs +++ b/tests/oauth_security.rs @@ -41,8 +41,8 @@ async fn setup_mock_client_metadata(redirect_uri: &str) -> MockServer { } async fn get_oauth_tokens(http_client: &reqwest::Client, url: &str) -> (String, String, String) { - let ts = Utc::now().timestamp_millis(); - let handle = format!("sec-test-{}", ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle = format!("se{}", suffix); let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Security123!" })) .send().await.unwrap(); @@ -255,8 +255,8 @@ async fn test_pkce_security() { StatusCode::BAD_REQUEST, "Missing PKCE challenge should be rejected" ); - let ts = Utc::now().timestamp_millis(); - let handle = format!("pkce-attack-{}", ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle = format!("pa{}", suffix); let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Pkce123pass!" })) .send().await.unwrap(); @@ -326,8 +326,8 @@ async fn test_pkce_security() { async fn test_replay_attacks() { let url = base_url().await; let http_client = client(); - let ts = Utc::now().timestamp_millis(); - let handle = format!("replay-{}", ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle = format!("rp{}", suffix); let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Replay123pass!" })) .send().await.unwrap(); @@ -532,8 +532,8 @@ async fn test_oauth_security_boundaries() { StatusCode::BAD_REQUEST, "Unregistered redirect_uri should be rejected" ); - let ts = Utc::now().timestamp_millis(); - let handle = format!("deact-{}", ts); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle = format!("da{}", suffix); let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Deact123pass!" })) .send().await.unwrap(); @@ -576,8 +576,8 @@ async fn test_oauth_security_boundaries() { let client_id_a = mock_a.uri(); let mock_b = setup_mock_client_metadata("https://app-b.com/callback").await; let client_id_b = mock_b.uri(); - let ts2 = Utc::now().timestamp_millis(); - let handle2 = format!("cross-{}", ts2); + let suffix2 = &uuid::Uuid::new_v4().simple().to_string()[..8]; + let handle2 = format!("cr{}", suffix2); let create_res2 = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) .json(&json!({ "handle": handle2, "email": format!("{}@example.com", handle2), "password": "Cross123pass!" })) .send().await.unwrap(); @@ -1110,11 +1110,11 @@ fn test_dpop_http_method_case() { async fn test_delegation_viewer_scope_cannot_write() { let url = base_url().await; let http_client = client(); - let ts = Utc::now().timestamp_millis(); + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; let (controller_jwt, controller_did) = create_account_and_login(&http_client).await; - let delegated_handle = format!("deleg-{}", ts); + let delegated_handle = format!("dg{}", suffix); let delegated_res = http_client .post(format!( "{}/xrpc/com.tranquil.delegation.createDelegatedAccount", diff --git a/tests/password_reset.rs b/tests/password_reset.rs index 3db9810..993d94f 100644 --- a/tests/password_reset.rs +++ b/tests/password_reset.rs @@ -9,7 +9,7 @@ async fn test_request_password_reset_creates_code() { let client = common::client(); let base_url = common::base_url().await; let pool = common::get_test_db_pool().await; - let handle = format!("pwreset-{}", uuid::Uuid::new_v4()); + let handle = format!("pr{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email = format!("{}@example.com", handle); let payload = json!({ "handle": handle, @@ -71,7 +71,7 @@ async fn test_reset_password_with_valid_token() { let client = common::client(); let base_url = common::base_url().await; let pool = common::get_test_db_pool().await; - let handle = format!("pwreset2-{}", uuid::Uuid::new_v4()); + let handle = format!("pr2{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email = format!("{}@example.com", handle); let old_password = "Oldpass123!"; let new_password = "Newpass456!"; @@ -187,7 +187,7 @@ async fn test_reset_password_with_expired_token() { let client = common::client(); let base_url = common::base_url().await; let pool = common::get_test_db_pool().await; - let handle = format!("pwreset3-{}", uuid::Uuid::new_v4()); + let handle = format!("pr3{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email = format!("{}@example.com", handle); let payload = json!({ "handle": handle, @@ -251,7 +251,7 @@ async fn test_reset_password_invalidates_sessions() { let client = common::client(); let base_url = common::base_url().await; let pool = common::get_test_db_pool().await; - let handle = format!("pwreset4-{}", uuid::Uuid::new_v4()); + let handle = format!("pr4{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email = format!("{}@example.com", handle); let payload = json!({ "handle": handle, @@ -341,7 +341,7 @@ async fn test_reset_password_creates_notification() { let pool = common::get_test_db_pool().await; let client = common::client(); let base_url = common::base_url().await; - let handle = format!("pwreset5-{}", uuid::Uuid::new_v4()); + let handle = format!("pr5{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let email = format!("{}@example.com", handle); let payload = json!({ "handle": handle, diff --git a/tests/server.rs b/tests/server.rs index 0dfe0c0..32329fb 100644 --- a/tests/server.rs +++ b/tests/server.rs @@ -26,7 +26,7 @@ async fn test_server_basics() { async fn test_account_and_session_lifecycle() { let client = client(); let base = base_url().await; - let handle = format!("user-{}", uuid::Uuid::new_v4()); + let handle = format!("u{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Testpass123!" }); let create_res = client .post(format!("{}/xrpc/com.atproto.server.createAccount", base)) diff --git a/tests/signing_key.rs b/tests/signing_key.rs index 79e5470..7353398 100644 --- a/tests/signing_key.rs +++ b/tests/signing_key.rs @@ -164,7 +164,7 @@ async fn test_create_account_with_reserved_signing_key() { assert_eq!(res.status(), StatusCode::OK); let body: Value = res.json().await.unwrap(); let signing_key = body["signingKey"].as_str().unwrap(); - let handle = format!("reserved-key-user-{}", uuid::Uuid::new_v4()); + let handle = format!("rk{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let res = client .post(format!( "{}/xrpc/com.atproto.server.createAccount", @@ -202,7 +202,7 @@ async fn test_create_account_with_reserved_signing_key() { async fn test_create_account_with_invalid_signing_key() { let client = common::client(); let base_url = common::base_url().await; - let handle = format!("bad-key-user-{}", uuid::Uuid::new_v4()); + let handle = format!("bk{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let res = client .post(format!( "{}/xrpc/com.atproto.server.createAccount", @@ -238,7 +238,7 @@ async fn test_create_account_cannot_reuse_signing_key() { assert_eq!(res.status(), StatusCode::OK); let body: Value = res.json().await.unwrap(); let signing_key = body["signingKey"].as_str().unwrap(); - let handle1 = format!("reuse-key-user1-{}", uuid::Uuid::new_v4()); + let handle1 = format!("r1{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let res = client .post(format!( "{}/xrpc/com.atproto.server.createAccount", @@ -254,7 +254,7 @@ async fn test_create_account_cannot_reuse_signing_key() { .await .expect("Failed to create first account"); assert_eq!(res.status(), StatusCode::OK); - let handle2 = format!("reuse-key-user2-{}", uuid::Uuid::new_v4()); + let handle2 = format!("r2{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let res = client .post(format!( "{}/xrpc/com.atproto.server.createAccount", @@ -291,7 +291,7 @@ async fn test_reserved_key_tokens_work() { assert_eq!(res.status(), StatusCode::OK); let body: Value = res.json().await.unwrap(); let signing_key = body["signingKey"].as_str().unwrap(); - let handle = format!("token-test-user-{}", uuid::Uuid::new_v4()); + let handle = format!("tu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); let res = client .post(format!( "{}/xrpc/com.atproto.server.createAccount", diff --git a/tests/sync_deprecated.rs b/tests/sync_deprecated.rs index 921374a..5bc92ba 100644 --- a/tests/sync_deprecated.rs +++ b/tests/sync_deprecated.rs @@ -62,7 +62,7 @@ async fn test_get_head_comprehensive() { .expect("Failed to send request"); assert_eq!(not_found_res.status(), StatusCode::BAD_REQUEST); let error_body: Value = not_found_res.json().await.unwrap(); - assert_eq!(error_body["error"], "HeadNotFound"); + assert_eq!(error_body["error"], "RepoNotFound"); let missing_res = client .get(format!( "{}/xrpc/com.atproto.sync.getHead", @@ -165,7 +165,7 @@ async fn test_get_checkout_comprehensive() { .send() .await .expect("Failed to send request"); - assert_eq!(not_found_res.status(), StatusCode::NOT_FOUND); + assert_eq!(not_found_res.status(), StatusCode::BAD_REQUEST); let error_body: Value = not_found_res.json().await.unwrap(); assert_eq!(error_body["error"], "RepoNotFound"); let missing_res = client @@ -188,3 +188,166 @@ async fn test_get_checkout_comprehensive() { .expect("Failed to send request"); assert_eq!(empty_did_res.status(), StatusCode::BAD_REQUEST); } + +#[tokio::test] +async fn test_get_head_deactivated_account_returns_error() { + let client = client(); + let base = base_url().await; + let (did, jwt) = setup_new_user("deactheadtest").await; + let res = client + .get(format!("{}/xrpc/com.atproto.sync.getHead", base)) + .query(&[("did", did.as_str())]) + .send() + .await + .unwrap(); + assert_eq!(res.status(), StatusCode::OK); + client + .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", base)) + .bearer_auth(&jwt) + .json(&serde_json::json!({})) + .send() + .await + .unwrap(); + let deact_res = client + .get(format!("{}/xrpc/com.atproto.sync.getHead", base)) + .query(&[("did", did.as_str())]) + .send() + .await + .unwrap(); + assert_eq!(deact_res.status(), StatusCode::BAD_REQUEST); + let body: Value = deact_res.json().await.unwrap(); + assert_eq!(body["error"], "RepoDeactivated"); +} + +#[tokio::test] +async fn test_get_head_takendown_account_returns_error() { + let client = client(); + let base = base_url().await; + let (admin_jwt, _) = create_admin_account_and_login(&client).await; + let (_, target_did) = create_account_and_login(&client).await; + let res = client + .get(format!("{}/xrpc/com.atproto.sync.getHead", base)) + .query(&[("did", target_did.as_str())]) + .send() + .await + .unwrap(); + assert_eq!(res.status(), StatusCode::OK); + client + .post(format!("{}/xrpc/com.atproto.admin.updateSubjectStatus", base)) + .bearer_auth(&admin_jwt) + .json(&serde_json::json!({ + "subject": { + "$type": "com.atproto.admin.defs#repoRef", + "did": target_did + }, + "takedown": { + "applied": true, + "ref": "test-takedown" + } + })) + .send() + .await + .unwrap(); + let takedown_res = client + .get(format!("{}/xrpc/com.atproto.sync.getHead", base)) + .query(&[("did", target_did.as_str())]) + .send() + .await + .unwrap(); + assert_eq!(takedown_res.status(), StatusCode::BAD_REQUEST); + let body: Value = takedown_res.json().await.unwrap(); + assert_eq!(body["error"], "RepoTakendown"); +} + +#[tokio::test] +async fn test_get_head_admin_can_access_deactivated() { + let client = client(); + let base = base_url().await; + let (admin_jwt, _) = create_admin_account_and_login(&client).await; + let (user_jwt, did) = create_account_and_login(&client).await; + client + .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", base)) + .bearer_auth(&user_jwt) + .json(&serde_json::json!({})) + .send() + .await + .unwrap(); + let res = client + .get(format!("{}/xrpc/com.atproto.sync.getHead", base)) + .bearer_auth(&admin_jwt) + .query(&[("did", did.as_str())]) + .send() + .await + .unwrap(); + assert_eq!(res.status(), StatusCode::OK); +} + +#[tokio::test] +async fn test_get_checkout_deactivated_account_returns_error() { + let client = client(); + let base = base_url().await; + let (did, jwt) = setup_new_user("deactcheckouttest").await; + let res = client + .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base)) + .query(&[("did", did.as_str())]) + .send() + .await + .unwrap(); + assert_eq!(res.status(), StatusCode::OK); + client + .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", base)) + .bearer_auth(&jwt) + .json(&serde_json::json!({})) + .send() + .await + .unwrap(); + let deact_res = client + .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base)) + .query(&[("did", did.as_str())]) + .send() + .await + .unwrap(); + assert_eq!(deact_res.status(), StatusCode::BAD_REQUEST); + let body: Value = deact_res.json().await.unwrap(); + assert_eq!(body["error"], "RepoDeactivated"); +} + +#[tokio::test] +async fn test_get_checkout_takendown_account_returns_error() { + let client = client(); + let base = base_url().await; + let (admin_jwt, _) = create_admin_account_and_login(&client).await; + let (_, target_did) = create_account_and_login(&client).await; + let res = client + .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base)) + .query(&[("did", target_did.as_str())]) + .send() + .await + .unwrap(); + assert_eq!(res.status(), StatusCode::OK); + client + .post(format!("{}/xrpc/com.atproto.admin.updateSubjectStatus", base)) + .bearer_auth(&admin_jwt) + .json(&serde_json::json!({ + "subject": { + "$type": "com.atproto.admin.defs#repoRef", + "did": target_did + }, + "takedown": { + "applied": true, + "ref": "test-takedown" + } + })) + .send() + .await + .unwrap(); + let takedown_res = client + .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base)) + .query(&[("did", target_did.as_str())]) + .send() + .await + .unwrap(); + assert_eq!(takedown_res.status(), StatusCode::BAD_REQUEST); + let body: Value = takedown_res.json().await.unwrap(); + assert_eq!(body["error"], "RepoTakendown"); +}