this repo has no description

Reserved handles & misc fixes

lewis bc8890ef ece54ff2

+1
.gitignore
··· 5 reference-pds-hailey/ 6 reference-pds-bsky/ 7 reference-relay-indigo/ 8 # Frontend build artifacts 9 frontend/node_modules/ 10 frontend/dist/
··· 5 reference-pds-hailey/ 6 reference-pds-bsky/ 7 reference-relay-indigo/ 8 + pds-moover/ 9 # Frontend build artifacts 10 frontend/node_modules/ 11 frontend/dist/
+56 -13
src/api/actor/preferences.rs
··· 5 http::StatusCode, 6 response::{IntoResponse, Response}, 7 }; 8 use serde::{Deserialize, Serialize}; 9 use serde_json::{Value, json}; 10 11 const APP_BSKY_NAMESPACE: &str = "app.bsky"; 12 const MAX_PREFERENCES_COUNT: usize = 100; 13 const MAX_PREFERENCE_SIZE: usize = 10_000; 14 15 #[derive(Serialize)] 16 pub struct GetPreferencesOutput { ··· 43 .into_response(); 44 } 45 }; 46 let user_id: uuid::Uuid = 47 match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did) 48 .fetch_optional(&state.db) ··· 73 .into_response(); 74 } 75 }; 76 - let preferences: Vec<Value> = prefs 77 .into_iter() 78 .filter(|row| { 79 row.name == APP_BSKY_NAMESPACE 80 || row.name.starts_with(&format!("{}.", APP_BSKY_NAMESPACE)) 81 }) 82 .filter_map(|row| { 83 - if row.name == "app.bsky.actor.defs#declaredAgePref" { 84 return None; 85 } 86 serde_json::from_value(row.value_json).ok() 87 }) 88 .collect(); 89 (StatusCode::OK, Json(GetPreferencesOutput { preferences })).into_response() 90 } 91 ··· 121 .into_response(); 122 } 123 }; 124 - let (user_id, is_migration): (uuid::Uuid, bool) = match sqlx::query!( 125 - "SELECT id, deactivated_at FROM users WHERE did = $1", 126 auth_user.did 127 ) 128 .fetch_optional(&state.db) 129 .await 130 { 131 - Ok(Some(row)) => (row.id, row.deactivated_at.is_some()), 132 _ => { 133 return ( 134 StatusCode::INTERNAL_SERVER_ERROR, ··· 144 ) 145 .into_response(); 146 } 147 for pref in &input.preferences { 148 let pref_str = serde_json::to_string(pref).unwrap_or_default(); 149 if pref_str.len() > MAX_PREFERENCE_SIZE { ··· 158 None => { 159 return ( 160 StatusCode::BAD_REQUEST, 161 - Json(json!({"error": "InvalidRequest", "message": "Preference missing $type field"})), 162 ) 163 .into_response(); 164 } ··· 166 if !pref_type.starts_with(APP_BSKY_NAMESPACE) { 167 return ( 168 StatusCode::BAD_REQUEST, 169 - Json(json!({"error": "InvalidRequest", "message": format!("Invalid preference namespace: {}", pref_type)})), 170 ) 171 .into_response(); 172 } 173 - if pref_type == "app.bsky.actor.defs#declaredAgePref" && !is_migration { 174 - return ( 175 - StatusCode::BAD_REQUEST, 176 - Json(json!({"error": "InvalidRequest", "message": "declaredAgePref is read-only"})), 177 - ) 178 - .into_response(); 179 } 180 } 181 let mut tx = match state.db.begin().await { 182 Ok(tx) => tx, ··· 209 Some(t) => t, 210 None => continue, 211 }; 212 let insert_result = sqlx::query!( 213 "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)", 214 user_id,
··· 5 http::StatusCode, 6 response::{IntoResponse, Response}, 7 }; 8 + use chrono::{Datelike, NaiveDate, Utc}; 9 use serde::{Deserialize, Serialize}; 10 use serde_json::{Value, json}; 11 12 const APP_BSKY_NAMESPACE: &str = "app.bsky"; 13 const MAX_PREFERENCES_COUNT: usize = 100; 14 const MAX_PREFERENCE_SIZE: usize = 10_000; 15 + const PERSONAL_DETAILS_PREF: &str = "app.bsky.actor.defs#personalDetailsPref"; 16 + const DECLARED_AGE_PREF: &str = "app.bsky.actor.defs#declaredAgePref"; 17 + 18 + fn get_age_from_datestring(birth_date: &str) -> Option<i32> { 19 + let bday = NaiveDate::parse_from_str(birth_date, "%Y-%m-%d").ok()?; 20 + let today = Utc::now().date_naive(); 21 + let mut age = today.year() - bday.year(); 22 + let m = today.month() as i32 - bday.month() as i32; 23 + if m < 0 || (m == 0 && today.day() < bday.day()) { 24 + age -= 1; 25 + } 26 + Some(age) 27 + } 28 29 #[derive(Serialize)] 30 pub struct GetPreferencesOutput { ··· 57 .into_response(); 58 } 59 }; 60 + let has_full_access = auth_user.permissions().has_full_access(); 61 let user_id: uuid::Uuid = 62 match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", auth_user.did) 63 .fetch_optional(&state.db) ··· 88 .into_response(); 89 } 90 }; 91 + let mut personal_details_pref: Option<Value> = None; 92 + let mut preferences: Vec<Value> = prefs 93 .into_iter() 94 .filter(|row| { 95 row.name == APP_BSKY_NAMESPACE 96 || row.name.starts_with(&format!("{}.", APP_BSKY_NAMESPACE)) 97 }) 98 .filter_map(|row| { 99 + if row.name == DECLARED_AGE_PREF { 100 return None; 101 } 102 + if row.name == PERSONAL_DETAILS_PREF { 103 + if !has_full_access { 104 + return None; 105 + } 106 + personal_details_pref = serde_json::from_value(row.value_json.clone()).ok(); 107 + } 108 serde_json::from_value(row.value_json).ok() 109 }) 110 .collect(); 111 + if let Some(ref pref) = personal_details_pref { 112 + if let Some(birth_date) = pref.get("birthDate").and_then(|v| v.as_str()) { 113 + if let Some(age) = get_age_from_datestring(birth_date) { 114 + let declared_age_pref = json!({ 115 + "$type": DECLARED_AGE_PREF, 116 + "isOverAge13": age >= 13, 117 + "isOverAge16": age >= 16, 118 + "isOverAge18": age >= 18, 119 + }); 120 + preferences.push(declared_age_pref); 121 + } 122 + } 123 + } 124 (StatusCode::OK, Json(GetPreferencesOutput { preferences })).into_response() 125 } 126 ··· 156 .into_response(); 157 } 158 }; 159 + let has_full_access = auth_user.permissions().has_full_access(); 160 + let user_id: uuid::Uuid = match sqlx::query_scalar!( 161 + "SELECT id FROM users WHERE did = $1", 162 auth_user.did 163 ) 164 .fetch_optional(&state.db) 165 .await 166 { 167 + Ok(Some(id)) => id, 168 _ => { 169 return ( 170 StatusCode::INTERNAL_SERVER_ERROR, ··· 180 ) 181 .into_response(); 182 } 183 + let mut forbidden_prefs: Vec<String> = Vec::new(); 184 for pref in &input.preferences { 185 let pref_str = serde_json::to_string(pref).unwrap_or_default(); 186 if pref_str.len() > MAX_PREFERENCE_SIZE { ··· 195 None => { 196 return ( 197 StatusCode::BAD_REQUEST, 198 + Json(json!({"error": "InvalidRequest", "message": "Preference is missing a $type"})), 199 ) 200 .into_response(); 201 } ··· 203 if !pref_type.starts_with(APP_BSKY_NAMESPACE) { 204 return ( 205 StatusCode::BAD_REQUEST, 206 + Json(json!({"error": "InvalidRequest", "message": format!("Some preferences are not in the {} namespace", APP_BSKY_NAMESPACE)})), 207 ) 208 .into_response(); 209 } 210 + if pref_type == PERSONAL_DETAILS_PREF && !has_full_access { 211 + forbidden_prefs.push(pref_type.to_string()); 212 } 213 + } 214 + if !forbidden_prefs.is_empty() { 215 + return ( 216 + StatusCode::BAD_REQUEST, 217 + Json(json!({"error": "InvalidRequest", "message": format!("Do not have authorization to set preferences: {}", forbidden_prefs.join(", "))})), 218 + ) 219 + .into_response(); 220 } 221 let mut tx = match state.db.begin().await { 222 Ok(tx) => tx, ··· 249 Some(t) => t, 250 None => continue, 251 }; 252 + if pref_type == DECLARED_AGE_PREF { 253 + continue; 254 + } 255 let insert_result = sqlx::query!( 256 "INSERT INTO account_preferences (user_id, name, value_json) VALUES ($1, $2, $3)", 257 user_id,
+7
src/api/identity/account.rs
··· 188 }; 189 match crate::api::validation::validate_short_handle(handle_to_validate) { 190 Ok(h) => h, 191 Err(e) => { 192 return ( 193 StatusCode::BAD_REQUEST,
··· 188 }; 189 match crate::api::validation::validate_short_handle(handle_to_validate) { 190 Ok(h) => h, 191 + Err(crate::api::validation::HandleValidationError::Reserved) => { 192 + return ( 193 + StatusCode::BAD_REQUEST, 194 + Json(json!({"error": "HandleNotAvailable", "message": "Reserved handle"})), 195 + ) 196 + .into_response(); 197 + } 198 Err(e) => { 199 return ( 200 StatusCode::BAD_REQUEST,
+35
src/api/proxy.rs
··· 10 use serde_json::json; 11 use tracing::{error, info, warn}; 12 13 pub async fn proxy_handler( 14 State(state): State<AppState>, 15 Path(method): Path<String>, ··· 18 RawQuery(query): RawQuery, 19 body: Bytes, 20 ) -> Response { 21 let proxy_header = match headers.get("atproto-proxy").and_then(|h| h.to_str().ok()) { 22 Some(h) => h.to_string(), 23 None => {
··· 10 use serde_json::json; 11 use tracing::{error, info, warn}; 12 13 + const PROTECTED_METHODS: &[&str] = &[ 14 + "com.atproto.admin.sendEmail", 15 + "com.atproto.identity.requestPlcOperationSignature", 16 + "com.atproto.identity.signPlcOperation", 17 + "com.atproto.identity.updateHandle", 18 + "com.atproto.server.activateAccount", 19 + "com.atproto.server.confirmEmail", 20 + "com.atproto.server.createAppPassword", 21 + "com.atproto.server.deactivateAccount", 22 + "com.atproto.server.getAccountInviteCodes", 23 + "com.atproto.server.getSession", 24 + "com.atproto.server.listAppPasswords", 25 + "com.atproto.server.requestAccountDelete", 26 + "com.atproto.server.requestEmailConfirmation", 27 + "com.atproto.server.requestEmailUpdate", 28 + "com.atproto.server.revokeAppPassword", 29 + "com.atproto.server.updateEmail", 30 + ]; 31 + 32 + fn is_protected_method(method: &str) -> bool { 33 + PROTECTED_METHODS.contains(&method) 34 + } 35 + 36 pub async fn proxy_handler( 37 State(state): State<AppState>, 38 Path(method): Path<String>, ··· 41 RawQuery(query): RawQuery, 42 body: Bytes, 43 ) -> Response { 44 + if is_protected_method(&method) { 45 + warn!(method = %method, "Attempted to proxy protected method"); 46 + return ( 47 + StatusCode::BAD_REQUEST, 48 + Json(json!({ 49 + "error": "InvalidRequest", 50 + "message": format!("Cannot proxy protected method: {}", method) 51 + })), 52 + ) 53 + .into_response(); 54 + } 55 + 56 let proxy_header = match headers.get("atproto-proxy").and_then(|h| h.to_str().ok()) { 57 Some(h) => h.to_string(), 58 None => {
+78 -2
src/api/validation.rs
··· 6 7 pub const MIN_HANDLE_LENGTH: usize = 3; 8 pub const MAX_HANDLE_LENGTH: usize = 253; 9 10 #[derive(Debug, PartialEq)] 11 pub enum HandleValidationError { ··· 17 EndsWithInvalidChar, 18 ContainsSpaces, 19 BannedWord, 20 } 21 22 impl std::fmt::Display for HandleValidationError { ··· 31 Self::TooLong => write!( 32 f, 33 "Handle exceeds maximum length of {} characters", 34 - MAX_HANDLE_LENGTH 35 ), 36 Self::InvalidCharacters => write!( 37 f, ··· 43 Self::EndsWithInvalidChar => write!(f, "Handle cannot end with a hyphen"), 44 Self::ContainsSpaces => write!(f, "Handle cannot contain spaces"), 45 Self::BannedWord => write!(f, "Inappropriate language in handle"), 46 } 47 } 48 } 49 50 pub fn validate_short_handle(handle: &str) -> Result<String, HandleValidationError> { 51 let handle = handle.trim(); 52 53 if handle.is_empty() { ··· 62 return Err(HandleValidationError::TooShort); 63 } 64 65 - if handle.len() > MAX_HANDLE_LENGTH { 66 return Err(HandleValidationError::TooLong); 67 } 68 ··· 86 87 if crate::moderation::has_explicit_slur(handle) { 88 return Err(HandleValidationError::BannedWord); 89 } 90 91 Ok(handle.to_lowercase()) ··· 221 #[test] 222 fn test_handle_trimming() { 223 assert_eq!(validate_short_handle(" alice "), Ok("alice".to_string())); 224 } 225 226 #[test]
··· 6 7 pub const MIN_HANDLE_LENGTH: usize = 3; 8 pub const MAX_HANDLE_LENGTH: usize = 253; 9 + pub const MAX_SERVICE_HANDLE_LOCAL_PART: usize = 18; 10 11 #[derive(Debug, PartialEq)] 12 pub enum HandleValidationError { ··· 18 EndsWithInvalidChar, 19 ContainsSpaces, 20 BannedWord, 21 + Reserved, 22 } 23 24 impl std::fmt::Display for HandleValidationError { ··· 33 Self::TooLong => write!( 34 f, 35 "Handle exceeds maximum length of {} characters", 36 + MAX_SERVICE_HANDLE_LOCAL_PART 37 ), 38 Self::InvalidCharacters => write!( 39 f, ··· 45 Self::EndsWithInvalidChar => write!(f, "Handle cannot end with a hyphen"), 46 Self::ContainsSpaces => write!(f, "Handle cannot contain spaces"), 47 Self::BannedWord => write!(f, "Inappropriate language in handle"), 48 + Self::Reserved => write!(f, "Reserved handle"), 49 } 50 } 51 } 52 53 pub fn validate_short_handle(handle: &str) -> Result<String, HandleValidationError> { 54 + validate_service_handle(handle, false) 55 + } 56 + 57 + pub fn validate_service_handle( 58 + handle: &str, 59 + allow_reserved: bool, 60 + ) -> Result<String, HandleValidationError> { 61 let handle = handle.trim(); 62 63 if handle.is_empty() { ··· 72 return Err(HandleValidationError::TooShort); 73 } 74 75 + if handle.len() > MAX_SERVICE_HANDLE_LOCAL_PART { 76 return Err(HandleValidationError::TooLong); 77 } 78 ··· 96 97 if crate::moderation::has_explicit_slur(handle) { 98 return Err(HandleValidationError::BannedWord); 99 + } 100 + 101 + if !allow_reserved && crate::handle::reserved::is_reserved_subdomain(handle) { 102 + return Err(HandleValidationError::Reserved); 103 } 104 105 Ok(handle.to_lowercase()) ··· 235 #[test] 236 fn test_handle_trimming() { 237 assert_eq!(validate_short_handle(" alice "), Ok("alice".to_string())); 238 + } 239 + 240 + #[test] 241 + fn test_handle_max_length() { 242 + assert_eq!( 243 + validate_short_handle("exactly18charslol"), 244 + Ok("exactly18charslol".to_string()) 245 + ); 246 + assert_eq!( 247 + validate_short_handle("exactly18charslol1"), 248 + Ok("exactly18charslol1".to_string()) 249 + ); 250 + assert_eq!( 251 + validate_short_handle("exactly19characters"), 252 + Err(HandleValidationError::TooLong) 253 + ); 254 + assert_eq!( 255 + validate_short_handle("waytoolongusername123456789"), 256 + Err(HandleValidationError::TooLong) 257 + ); 258 + } 259 + 260 + #[test] 261 + fn test_reserved_subdomains() { 262 + assert_eq!( 263 + validate_short_handle("admin"), 264 + Err(HandleValidationError::Reserved) 265 + ); 266 + assert_eq!( 267 + validate_short_handle("api"), 268 + Err(HandleValidationError::Reserved) 269 + ); 270 + assert_eq!( 271 + validate_short_handle("bsky"), 272 + Err(HandleValidationError::Reserved) 273 + ); 274 + assert_eq!( 275 + validate_short_handle("barackobama"), 276 + Err(HandleValidationError::Reserved) 277 + ); 278 + assert_eq!( 279 + validate_short_handle("ADMIN"), 280 + Err(HandleValidationError::Reserved) 281 + ); 282 + assert_eq!(validate_short_handle("alice"), Ok("alice".to_string())); 283 + assert_eq!( 284 + validate_short_handle("notreserved"), 285 + Ok("notreserved".to_string()) 286 + ); 287 + } 288 + 289 + #[test] 290 + fn test_allow_reserved() { 291 + assert_eq!( 292 + validate_service_handle("admin", true), 293 + Ok("admin".to_string()) 294 + ); 295 + assert_eq!(validate_service_handle("api", true), Ok("api".to_string())); 296 + assert_eq!( 297 + validate_service_handle("admin", false), 298 + Err(HandleValidationError::Reserved) 299 + ); 300 } 301 302 #[test]
+2
src/handle/mod.rs
··· 1 use hickory_resolver::TokioAsyncResolver; 2 use hickory_resolver::config::{ResolverConfig, ResolverOpts}; 3 use reqwest::Client;
··· 1 + pub mod reserved; 2 + 3 use hickory_resolver::TokioAsyncResolver; 4 use hickory_resolver::config::{ResolverConfig, ResolverOpts}; 5 use reqwest::Client;
+1097
src/handle/reserved.rs
···
··· 1 + use std::collections::HashSet; 2 + use std::sync::LazyLock; 3 + 4 + const ATP_SPECIFIC: &[&str] = &[ 5 + "at", 6 + "atp", 7 + "plc", 8 + "pds", 9 + "did", 10 + "repo", 11 + "tid", 12 + "nsid", 13 + "xrpc", 14 + "lex", 15 + "lexicon", 16 + "bsky", 17 + "bluesky", 18 + "handle", 19 + ]; 20 + 21 + const COMMONLY_RESERVED: &[&str] = &[ 22 + "about", 23 + "abuse", 24 + "access", 25 + "account", 26 + "accounts", 27 + "acme", 28 + "activate", 29 + "activities", 30 + "activity", 31 + "ad", 32 + "add", 33 + "address", 34 + "adm", 35 + "admanager", 36 + "admin", 37 + "administration", 38 + "administrator", 39 + "administrators", 40 + "admins", 41 + "ads", 42 + "adsense", 43 + "adult", 44 + "advertising", 45 + "adwords", 46 + "affiliate", 47 + "affiliatepage", 48 + "affiliates", 49 + "afp", 50 + "ajax", 51 + "all", 52 + "alpha", 53 + "analysis", 54 + "analytics", 55 + "android", 56 + "anon", 57 + "anonymous", 58 + "answer", 59 + "answers", 60 + "ap", 61 + "api", 62 + "apis", 63 + "app", 64 + "appengine", 65 + "appnews", 66 + "apps", 67 + "archive", 68 + "archives", 69 + "article", 70 + "asdf", 71 + "asset", 72 + "assets", 73 + "auth", 74 + "authentication", 75 + "avatar", 76 + "backup", 77 + "bank", 78 + "banner", 79 + "banners", 80 + "base", 81 + "beginners", 82 + "beta", 83 + "billing", 84 + "bin", 85 + "binaries", 86 + "binary", 87 + "blackberry", 88 + "blog", 89 + "blogs", 90 + "blogsearch", 91 + "board", 92 + "book", 93 + "bookmark", 94 + "bookmarks", 95 + "books", 96 + "bot", 97 + "bots", 98 + "bug", 99 + "bugs", 100 + "business", 101 + "buy", 102 + "buzz", 103 + "cache", 104 + "calendar", 105 + "call", 106 + "campaign", 107 + "cancel", 108 + "captcha", 109 + "career", 110 + "careers", 111 + "cart", 112 + "catalog", 113 + "catalogs", 114 + "categories", 115 + "category", 116 + "cdn", 117 + "cgi", 118 + "cgi-bin", 119 + "changelog", 120 + "chart", 121 + "charts", 122 + "chat", 123 + "check", 124 + "checked", 125 + "checking", 126 + "checkout", 127 + "client", 128 + "cliente", 129 + "clients", 130 + "clients1", 131 + "cnarne", 132 + "code", 133 + "comercial", 134 + "comment", 135 + "comments", 136 + "communities", 137 + "community", 138 + "company", 139 + "compare", 140 + "compras", 141 + "config", 142 + "configuration", 143 + "confirm", 144 + "confirmation", 145 + "connect", 146 + "contact", 147 + "contacts", 148 + "contactus", 149 + "contact-us", 150 + "contact_us", 151 + "content", 152 + "contest", 153 + "contribute", 154 + "contributor", 155 + "contributors", 156 + "coppa", 157 + "copyright", 158 + "copyrights", 159 + "core", 160 + "corp", 161 + "countries", 162 + "country", 163 + "cpanel", 164 + "create", 165 + "css", 166 + "cssproxy", 167 + "customise", 168 + "customize", 169 + "dashboard", 170 + "data", 171 + "db", 172 + "default", 173 + "delete", 174 + "demo", 175 + "design", 176 + "designer", 177 + "desktop", 178 + "destroy", 179 + "dev", 180 + "devel", 181 + "developer", 182 + "developers", 183 + "devs", 184 + "diagram", 185 + "diary", 186 + "dict", 187 + "dictionary", 188 + "die", 189 + "dir", 190 + "directory", 191 + "direct_messages", 192 + "direct-messages", 193 + "dist", 194 + "diversity", 195 + "dl", 196 + "dmca", 197 + "doc", 198 + "docs", 199 + "documentation", 200 + "documentations", 201 + "documents", 202 + "domain", 203 + "domains", 204 + "donate", 205 + "download", 206 + "downloads", 207 + "e", 208 + "e-mail", 209 + "earth", 210 + "ecommerce", 211 + "edit", 212 + "edits", 213 + "editor", 214 + "edu", 215 + "education", 216 + "email", 217 + "embed", 218 + "embedded", 219 + "employment", 220 + "employments", 221 + "empty", 222 + "enable", 223 + "encrypted", 224 + "end", 225 + "engine", 226 + "enterprise", 227 + "enterprises", 228 + "entries", 229 + "entry", 230 + "error", 231 + "errorlog", 232 + "errors", 233 + "eval", 234 + "event", 235 + "example", 236 + "examplecommunity", 237 + "exampleopenid", 238 + "examplesyn", 239 + "examplesyndicated", 240 + "exampleusername", 241 + "exchange", 242 + "exit", 243 + "explore", 244 + "faq", 245 + "faqs", 246 + "favorite", 247 + "favorites", 248 + "favourite", 249 + "favourites", 250 + "feature", 251 + "features", 252 + "feed", 253 + "feedback", 254 + "feedburner", 255 + "feedproxy", 256 + "feeds", 257 + "file", 258 + "files", 259 + "finance", 260 + "folder", 261 + "folders", 262 + "first", 263 + "following", 264 + "forgot", 265 + "form", 266 + "forms", 267 + "forum", 268 + "forums", 269 + "founder", 270 + "free", 271 + "friend", 272 + "friends", 273 + "ftp", 274 + "fuck", 275 + "fun", 276 + "fusion", 277 + "gadget", 278 + "gadgets", 279 + "game", 280 + "games", 281 + "gears", 282 + "general", 283 + "geographic", 284 + "get", 285 + "gettingstarted", 286 + "gift", 287 + "gifts", 288 + "gist", 289 + "git", 290 + "github", 291 + "gmail", 292 + "go", 293 + "golang", 294 + "goto", 295 + "gov", 296 + "graph", 297 + "graphs", 298 + "group", 299 + "groups", 300 + "guest", 301 + "guests", 302 + "guide", 303 + "guides", 304 + "hack", 305 + "hacks", 306 + "head", 307 + "help", 308 + "home", 309 + "homepage", 310 + "host", 311 + "hosting", 312 + "hostmaster", 313 + "hostname", 314 + "howto", 315 + "how-to", 316 + "how_to", 317 + "html", 318 + "htrnl", 319 + "http", 320 + "httpd", 321 + "https", 322 + "i", 323 + "iamges", 324 + "icon", 325 + "icons", 326 + "id", 327 + "idea", 328 + "ideas", 329 + "im", 330 + "image", 331 + "images", 332 + "img", 333 + "imap", 334 + "inbox", 335 + "inboxes", 336 + "index", 337 + "indexes", 338 + "info", 339 + "information", 340 + "inquiry", 341 + "intranet", 342 + "investor", 343 + "investors", 344 + "invitation", 345 + "invitations", 346 + "invite", 347 + "invoice", 348 + "invoices", 349 + "imac", 350 + "ios", 351 + "ipad", 352 + "iphone", 353 + "irc", 354 + "irnages", 355 + "irng", 356 + "is", 357 + "issue", 358 + "issues", 359 + "it", 360 + "item", 361 + "items", 362 + "java", 363 + "javascript", 364 + "job", 365 + "jobs", 366 + "join", 367 + "js", 368 + "json", 369 + "jump", 370 + "kb", 371 + "knowledge-base", 372 + "knowledgebase", 373 + "lab", 374 + "labs", 375 + "language", 376 + "languages", 377 + "last", 378 + "ldap_status", 379 + "ldap-status", 380 + "ldapstatus", 381 + "legal", 382 + "license", 383 + "licenses", 384 + "link", 385 + "links", 386 + "linux", 387 + "list", 388 + "lists", 389 + "livejournal", 390 + "lj", 391 + "local", 392 + "locale", 393 + "location", 394 + "log", 395 + "log-in", 396 + "log-out", 397 + "login", 398 + "logout", 399 + "logs", 400 + "log_in", 401 + "log_out", 402 + "m", 403 + "mac", 404 + "macos", 405 + "macosx", 406 + "mac-os", 407 + "mac-os-x", 408 + "mac_os_x", 409 + "mail", 410 + "mailer", 411 + "mailing", 412 + "main", 413 + "maintenance", 414 + "manage", 415 + "manager", 416 + "manual", 417 + "map", 418 + "maps", 419 + "marketing", 420 + "master", 421 + "me", 422 + "media", 423 + "member", 424 + "members", 425 + "memories", 426 + "memory", 427 + "merchandise", 428 + "message", 429 + "messages", 430 + "messenger", 431 + "mg", 432 + "microblog", 433 + "microblogs", 434 + "mine", 435 + "mis", 436 + "misc", 437 + "mms", 438 + "mob", 439 + "mobile", 440 + "model", 441 + "models", 442 + "money", 443 + "movie", 444 + "movies", 445 + "mp3", 446 + "mp4", 447 + "msg", 448 + "msn", 449 + "music", 450 + "mx", 451 + "my", 452 + "mymme", 453 + "mysql", 454 + "name", 455 + "named", 456 + "nan", 457 + "navi", 458 + "navigation", 459 + "net", 460 + "network", 461 + "networks", 462 + "new", 463 + "news", 464 + "newsletter", 465 + "nick", 466 + "nickname", 467 + "nil", 468 + "none", 469 + "notes", 470 + "noticias", 471 + "notification", 472 + "notifications", 473 + "notify", 474 + "ns", 475 + "ns1", 476 + "ns2", 477 + "ns3", 478 + "ns4", 479 + "ns5", 480 + "null", 481 + "oauth", 482 + "oauth-clients", 483 + "oauth_clients", 484 + "ocsp", 485 + "offer", 486 + "offers", 487 + "official", 488 + "old", 489 + "online", 490 + "openid", 491 + "operator", 492 + "option", 493 + "options", 494 + "order", 495 + "orders", 496 + "org", 497 + "organization", 498 + "organizations", 499 + "other", 500 + "overview", 501 + "owner", 502 + "owners", 503 + "p0rn", 504 + "pack", 505 + "page", 506 + "pager", 507 + "pages", 508 + "paid", 509 + "panel", 510 + "partner", 511 + "partnerpage", 512 + "partners", 513 + "password", 514 + "patch", 515 + "pay", 516 + "payment", 517 + "people", 518 + "perl", 519 + "person", 520 + "phone", 521 + "photo", 522 + "photoalbum", 523 + "photos", 524 + "php", 525 + "phpmyadmin", 526 + "phppgadmin", 527 + "phpredisadmin", 528 + "pic", 529 + "pics", 530 + "picture", 531 + "pictures", 532 + "ping", 533 + "pixel", 534 + "places", 535 + "plan", 536 + "plans", 537 + "plugin", 538 + "plugins", 539 + "podcasts", 540 + "policies", 541 + "policy", 542 + "pop", 543 + "pop3", 544 + "popular", 545 + "porn", 546 + "portal", 547 + "portals", 548 + "post", 549 + "postfix", 550 + "postmaster", 551 + "posts", 552 + "pr", 553 + "pr0n", 554 + "premium", 555 + "press", 556 + "price", 557 + "pricing", 558 + "principles", 559 + "print", 560 + "privacy", 561 + "privacy-policy", 562 + "privacypolicy", 563 + "privacy_policy", 564 + "private", 565 + "prod", 566 + "product", 567 + "production", 568 + "products", 569 + "profile", 570 + "profiles", 571 + "project", 572 + "projects", 573 + "promo", 574 + "promotions", 575 + "proxies", 576 + "proxy", 577 + "pub", 578 + "public", 579 + "purchase", 580 + "purpose", 581 + "put", 582 + "python", 583 + "queries", 584 + "query", 585 + "radio", 586 + "random", 587 + "ranking", 588 + "read", 589 + "reader", 590 + "readme", 591 + "recent", 592 + "recruit", 593 + "recruitment", 594 + "redirect", 595 + "register", 596 + "registration", 597 + "release", 598 + "remove", 599 + "replies", 600 + "report", 601 + "reports", 602 + "repositories", 603 + "repository", 604 + "req", 605 + "request", 606 + "requests", 607 + "research", 608 + "reset", 609 + "resolve", 610 + "resolver", 611 + "review", 612 + "rnail", 613 + "rnicrosoft", 614 + "roc", 615 + "root", 616 + "rss", 617 + "ruby", 618 + "rule", 619 + "sag", 620 + "sale", 621 + "sales", 622 + "sample", 623 + "samples", 624 + "sandbox", 625 + "save", 626 + "scholar", 627 + "school", 628 + "schools", 629 + "script", 630 + "scripts", 631 + "search", 632 + "secure", 633 + "security", 634 + "self", 635 + "seminars", 636 + "send", 637 + "server", 638 + "server-info", 639 + "server_info", 640 + "server-status", 641 + "server_status", 642 + "servers", 643 + "service", 644 + "services", 645 + "session", 646 + "sessions", 647 + "setting", 648 + "settings", 649 + "setup", 650 + "share", 651 + "shop", 652 + "shopping", 653 + "shortcut", 654 + "shortcuts", 655 + "show", 656 + "sign-in", 657 + "sign-up", 658 + "signin", 659 + "signout", 660 + "signup", 661 + "sign_in", 662 + "sign_up", 663 + "site", 664 + "sitemap", 665 + "sitemaps", 666 + "sitenews", 667 + "sites", 668 + "sketchup", 669 + "sky", 670 + "slash", 671 + "slashinvoice", 672 + "slut", 673 + "smartphone", 674 + "sms", 675 + "smtp", 676 + "soap", 677 + "software", 678 + "sorry", 679 + "source", 680 + "spec", 681 + "special", 682 + "spreadsheet", 683 + "spreadsheets", 684 + "sql", 685 + "src", 686 + "srntp", 687 + "ssh", 688 + "ssl", 689 + "ssladmin", 690 + "ssladministrator", 691 + "sslwebmaster", 692 + "ssytem", 693 + "staff", 694 + "stage", 695 + "staging", 696 + "start", 697 + "stat", 698 + "state", 699 + "static", 700 + "statistics", 701 + "stats", 702 + "status", 703 + "store", 704 + "stores", 705 + "stories", 706 + "style", 707 + "styleguide", 708 + "styles", 709 + "stylesheet", 710 + "stylesheets", 711 + "subdomain", 712 + "subscribe", 713 + "subscription", 714 + "subscriptions", 715 + "suggest", 716 + "suggestqueries", 717 + "support", 718 + "survey", 719 + "surveys", 720 + "surveytool", 721 + "svn", 722 + "swf", 723 + "syn", 724 + "sync", 725 + "syndicated", 726 + "sys", 727 + "sysadmin", 728 + "sysadministrator", 729 + "sysadmins", 730 + "system", 731 + "tablet", 732 + "tablets", 733 + "tag", 734 + "tags", 735 + "talk", 736 + "talkgadget", 737 + "task", 738 + "tasks", 739 + "team", 740 + "teams", 741 + "tech", 742 + "telnet", 743 + "term", 744 + "terms", 745 + "terms-of-service", 746 + "termsofservice", 747 + "terms_of_service", 748 + "test", 749 + "testing", 750 + "tests", 751 + "text", 752 + "theme", 753 + "themes", 754 + "thread", 755 + "threads", 756 + "ticket", 757 + "tickets", 758 + "tmp", 759 + "todo", 760 + "to-do", 761 + "to_do", 762 + "toml", 763 + "tool", 764 + "toolbar", 765 + "toolbars", 766 + "tools", 767 + "top", 768 + "topic", 769 + "topics", 770 + "tos", 771 + "tour", 772 + "trac", 773 + "translate", 774 + "trace", 775 + "translation", 776 + "translations", 777 + "translator", 778 + "trends", 779 + "tutorial", 780 + "tux", 781 + "tv", 782 + "twitter", 783 + "txt", 784 + "ul", 785 + "undef", 786 + "unfollow", 787 + "unsubscribe", 788 + "update", 789 + "updates", 790 + "upgrade", 791 + "upgrades", 792 + "upi", 793 + "upload", 794 + "uploads", 795 + "url", 796 + "usage", 797 + "user", 798 + "username", 799 + "usernames", 800 + "users", 801 + "uuid", 802 + "validation", 803 + "validations", 804 + "ver", 805 + "version", 806 + "video", 807 + "videos", 808 + "video-stats", 809 + "visitor", 810 + "visitors", 811 + "voice", 812 + "volunteer", 813 + "volunteers", 814 + "w", 815 + "watch", 816 + "wave", 817 + "weather", 818 + "web", 819 + "webdisk", 820 + "webhook", 821 + "webhooks", 822 + "webmail", 823 + "webmaster", 824 + "webmasters", 825 + "webrnail", 826 + "website", 827 + "websites", 828 + "welcome", 829 + "whm", 830 + "whois", 831 + "widget", 832 + "widgets", 833 + "wifi", 834 + "wiki", 835 + "wikis", 836 + "win", 837 + "windows", 838 + "word", 839 + "work", 840 + "works", 841 + "workshop", 842 + "wpad", 843 + "ww", 844 + "wws", 845 + "www", 846 + "wwws", 847 + "wwww", 848 + "xfn", 849 + "xhtml", 850 + "xhtrnl", 851 + "xml", 852 + "xmpp", 853 + "xpg", 854 + "xxx", 855 + "yaml", 856 + "year", 857 + "yml", 858 + "you", 859 + "yourdomain", 860 + "yourname", 861 + "yoursite", 862 + "yourusername", 863 + ]; 864 + 865 + const FAMOUS_ACCOUNTS: &[&str] = &[ 866 + "10ronaldinho", 867 + "3gerardpique", 868 + "aclu", 869 + "adele", 870 + "akshaykumar", 871 + "aliaa08", 872 + "aliciakeys", 873 + "amitshah", 874 + "andresiniesta8", 875 + "anushkasharma", 876 + "arianagrande", 877 + "arrahman", 878 + "arvindkejriwal", 879 + "avrillavigne", 880 + "barackobama", 881 + "bbcbreaking", 882 + "bbcworld", 883 + "beingsalmankhan", 884 + "billgates", 885 + "britneyspears", 886 + "brunomars", 887 + "bts_bighit", 888 + "bts_twt", 889 + "championsleague", 890 + "chrisbrown", 891 + "cnnbrk", 892 + "coldplay", 893 + "conanobrien", 894 + "cristiano", 895 + "danieltosh", 896 + "davidguetta", 897 + "ddlovato", 898 + "deepikapadukone", 899 + "drake", 900 + "elisapie", 901 + "ellendegeneres", 902 + "elonmusk", 903 + "eminem", 904 + "emmawatson", 905 + "fcbarcelona", 906 + "foxnews", 907 + "harry_styles", 908 + "hillaryclinton", 909 + "iamsrk", 910 + "ihrithik", 911 + "imvkohli", 912 + "instagram", 913 + "jimmyfallon", 914 + "jlo", 915 + "joebiden", 916 + "jtimberlake", 917 + "justinbieber", 918 + "kaka", 919 + "kanyewest", 920 + "katyperry", 921 + "kendalljenner", 922 + "kevinhart4real", 923 + "khloekardashian", 924 + "kimkardashian", 925 + "kingjames", 926 + "kourtneykardash", 927 + "kyliejenner", 928 + "ladygaga", 929 + "liampayne", 930 + "liltunechi", 931 + "manutd", 932 + "mariahcarey", 933 + "mileycyrus", 934 + "mohamadalarefe", 935 + "narendramodi", 936 + "nasa", 937 + "nba", 938 + "neymarjr", 939 + "nfl", 940 + "niallofficial", 941 + "nickiminaj", 942 + "npr", 943 + "nytimes", 944 + "onedirection", 945 + "oprah", 946 + "pink", 947 + "pitbull", 948 + "playstation", 949 + "pmoindia", 950 + "premierleague", 951 + "priyankachopra", 952 + "realdonaldtrump", 953 + "ricky_martin", 954 + "rihanna", 955 + "sachin_rt", 956 + "selenagomez", 957 + "shakira", 958 + "shawnmendes", 959 + "sportscenter", 960 + "srbachchan", 961 + "subhisharma100", 962 + "taylorswift13", 963 + "theeconomist", 964 + "twitter", 965 + "virendersehwag", 966 + "whitehouse45", 967 + "wizkhalifa", 968 + "youtube", 969 + "zaynmalik", 970 + "beyonce", 971 + "billieeilish", 972 + "leomessi", 973 + "natgeo", 974 + "nike", 975 + "snoopdogg", 976 + "taylorswift", 977 + "therock", 978 + "10downingstreet", 979 + "aoc", 980 + "carterjwm", 981 + "dril", 982 + "gretathunberg", 983 + "kamalaharris", 984 + "kremlinrussia_e", 985 + "potus", 986 + "rondesantisfl", 987 + "ukraine", 988 + "washingtonpost", 989 + "yousuck2020", 990 + "zelenskyyua", 991 + "akiko_lawson", 992 + "ariyoshihiroiki", 993 + "asahi", 994 + "dozle_official", 995 + "famima_now", 996 + "ff_xiv_jp", 997 + "fujitv", 998 + "gigazine", 999 + "hajimesyacho", 1000 + "hikakin", 1001 + "jocx", 1002 + "jotx", 1003 + "kiyo_saiore", 1004 + "mainichi", 1005 + "matsu_bouzu", 1006 + "naomiosaka", 1007 + "nhk", 1008 + "nikkei", 1009 + "nintendo", 1010 + "ntv", 1011 + "oowareware1945", 1012 + "pamyurin", 1013 + "poke_times", 1014 + "rolaworld", 1015 + "seikintv", 1016 + "starbucksjapan", 1017 + "tbs", 1018 + "tbs_pr", 1019 + "tvasahi", 1020 + "tvtokyo", 1021 + "yokoono", 1022 + "yomiuri_online", 1023 + "brasildefato", 1024 + "claudialeitte", 1025 + "correio", 1026 + "em_com", 1027 + "estadao", 1028 + "folha", 1029 + "gazetadopovo", 1030 + "ivetesangalo", 1031 + "jairbolsonaro", 1032 + "jornaldobrasil", 1033 + "jornaloglobo", 1034 + "lucianohuck", 1035 + "lulaoficial", 1036 + "marcosmion", 1037 + "paulocoelho", 1038 + "portalr7", 1039 + "rede_globo", 1040 + "zerohora", 1041 + ]; 1042 + 1043 + pub static RESERVED_SUBDOMAINS: LazyLock<HashSet<&'static str>> = LazyLock::new(|| { 1044 + let mut set = HashSet::with_capacity( 1045 + ATP_SPECIFIC.len() + COMMONLY_RESERVED.len() + FAMOUS_ACCOUNTS.len(), 1046 + ); 1047 + for s in ATP_SPECIFIC { 1048 + set.insert(*s); 1049 + } 1050 + for s in COMMONLY_RESERVED { 1051 + set.insert(*s); 1052 + } 1053 + for s in FAMOUS_ACCOUNTS { 1054 + set.insert(*s); 1055 + } 1056 + set 1057 + }); 1058 + 1059 + pub fn is_reserved_subdomain(subdomain: &str) -> bool { 1060 + RESERVED_SUBDOMAINS.contains(subdomain.to_lowercase().as_str()) 1061 + } 1062 + 1063 + #[cfg(test)] 1064 + mod tests { 1065 + use super::*; 1066 + 1067 + #[test] 1068 + fn test_atp_specific_reserved() { 1069 + assert!(is_reserved_subdomain("admin")); 1070 + assert!(is_reserved_subdomain("api")); 1071 + assert!(is_reserved_subdomain("bsky")); 1072 + assert!(is_reserved_subdomain("plc")); 1073 + assert!(is_reserved_subdomain("xrpc")); 1074 + } 1075 + 1076 + #[test] 1077 + fn test_famous_accounts_reserved() { 1078 + assert!(is_reserved_subdomain("barackobama")); 1079 + assert!(is_reserved_subdomain("elonmusk")); 1080 + assert!(is_reserved_subdomain("taylorswift")); 1081 + assert!(is_reserved_subdomain("nintendo")); 1082 + } 1083 + 1084 + #[test] 1085 + fn test_case_insensitive() { 1086 + assert!(is_reserved_subdomain("ADMIN")); 1087 + assert!(is_reserved_subdomain("Admin")); 1088 + assert!(is_reserved_subdomain("BARACKOBAMA")); 1089 + } 1090 + 1091 + #[test] 1092 + fn test_not_reserved() { 1093 + assert!(!is_reserved_subdomain("alice")); 1094 + assert!(!is_reserved_subdomain("bob123")); 1095 + assert!(!is_reserved_subdomain("randomuser")); 1096 + } 1097 + }
+1
src/oauth/endpoints/metadata.rs
··· 81 "atproto".to_string(), 82 "transition:generic".to_string(), 83 "transition:chat.bsky".to_string(), 84 "repo:*".to_string(), 85 "repo:*?action=create".to_string(), 86 "repo:*?action=read".to_string(),
··· 81 "atproto".to_string(), 82 "transition:generic".to_string(), 83 "transition:chat.bsky".to_string(), 84 + "transition:email".to_string(), 85 "repo:*".to_string(), 86 "repo:*?action=create".to_string(), 87 "repo:*?action=read".to_string(),
+39 -62
src/sync/deprecated.rs
··· 1 use crate::state::AppState; 2 use crate::sync::car::encode_car_header; 3 use axum::{ 4 Json, 5 extract::{Query, State}, 6 - http::StatusCode, 7 response::{IntoResponse, Response}, 8 }; 9 use cid::Cid; ··· 13 use serde_json::json; 14 use std::io::Write; 15 use std::str::FromStr; 16 - use tracing::error; 17 18 const MAX_REPO_BLOCKS_TRAVERSAL: usize = 20_000; 19 20 #[derive(Deserialize)] 21 pub struct GetHeadParams { 22 pub did: String, ··· 29 30 pub async fn get_head( 31 State(state): State<AppState>, 32 Query(params): Query<GetHeadParams>, 33 ) -> Response { 34 let did = params.did.trim(); ··· 39 ) 40 .into_response(); 41 } 42 - let result = sqlx::query!( 43 - r#" 44 - SELECT r.repo_root_cid 45 - FROM repos r 46 - JOIN users u ON r.user_id = u.id 47 - WHERE u.did = $1 48 - "#, 49 - did 50 - ) 51 - .fetch_optional(&state.db) 52 - .await; 53 - match result { 54 - Ok(Some(row)) => ( 55 - StatusCode::OK, 56 - Json(GetHeadOutput { 57 - root: row.repo_root_cid, 58 - }), 59 - ) 60 - .into_response(), 61 - Ok(None) => ( 62 StatusCode::BAD_REQUEST, 63 - Json(json!({"error": "HeadNotFound", "message": "Could not find root for DID"})), 64 ) 65 .into_response(), 66 - Err(e) => { 67 - error!("DB error in get_head: {:?}", e); 68 - ( 69 - StatusCode::INTERNAL_SERVER_ERROR, 70 - Json(json!({"error": "InternalError"})), 71 - ) 72 - .into_response() 73 - } 74 } 75 } 76 ··· 81 82 pub async fn get_checkout( 83 State(state): State<AppState>, 84 Query(params): Query<GetCheckoutParams>, 85 ) -> Response { 86 let did = params.did.trim(); ··· 91 ) 92 .into_response(); 93 } 94 - let repo_row = sqlx::query!( 95 - r#" 96 - SELECT r.repo_root_cid 97 - FROM repos r 98 - JOIN users u ON u.id = r.user_id 99 - WHERE u.did = $1 100 - "#, 101 - did 102 - ) 103 - .fetch_optional(&state.db) 104 - .await 105 - .unwrap_or(None); 106 - let head_str = match repo_row { 107 - Some(r) => r.repo_root_cid, 108 None => { 109 - let user_exists = sqlx::query!("SELECT id FROM users WHERE did = $1", did) 110 - .fetch_optional(&state.db) 111 - .await 112 - .unwrap_or(None); 113 - if user_exists.is_none() { 114 - return ( 115 - StatusCode::NOT_FOUND, 116 - Json(json!({"error": "RepoNotFound", "message": "Repo not found"})), 117 - ) 118 - .into_response(); 119 - } else { 120 - return ( 121 - StatusCode::NOT_FOUND, 122 - Json(json!({"error": "RepoNotFound", "message": "Repo not initialized"})), 123 - ) 124 - .into_response(); 125 - } 126 } 127 }; 128 let head_cid = match Cid::from_str(&head_str) {
··· 1 + use crate::auth::{extract_bearer_token_from_header, validate_bearer_token_allow_takendown}; 2 use crate::state::AppState; 3 use crate::sync::car::encode_car_header; 4 + use crate::sync::util::assert_repo_availability; 5 use axum::{ 6 Json, 7 extract::{Query, State}, 8 + http::{HeaderMap, StatusCode}, 9 response::{IntoResponse, Response}, 10 }; 11 use cid::Cid; ··· 15 use serde_json::json; 16 use std::io::Write; 17 use std::str::FromStr; 18 19 const MAX_REPO_BLOCKS_TRAVERSAL: usize = 20_000; 20 21 + async fn check_admin_or_self(state: &AppState, headers: &HeaderMap, did: &str) -> bool { 22 + let token = match extract_bearer_token_from_header( 23 + headers.get("Authorization").and_then(|h| h.to_str().ok()), 24 + ) { 25 + Some(t) => t, 26 + None => return false, 27 + }; 28 + match validate_bearer_token_allow_takendown(&state.db, &token).await { 29 + Ok(auth_user) => auth_user.is_admin || auth_user.did == did, 30 + Err(_) => false, 31 + } 32 + } 33 + 34 #[derive(Deserialize)] 35 pub struct GetHeadParams { 36 pub did: String, ··· 43 44 pub async fn get_head( 45 State(state): State<AppState>, 46 + headers: HeaderMap, 47 Query(params): Query<GetHeadParams>, 48 ) -> Response { 49 let did = params.did.trim(); ··· 54 ) 55 .into_response(); 56 } 57 + let is_admin_or_self = check_admin_or_self(&state, &headers, did).await; 58 + let account = match assert_repo_availability(&state.db, did, is_admin_or_self).await { 59 + Ok(a) => a, 60 + Err(e) => return e.into_response(), 61 + }; 62 + match account.repo_root_cid { 63 + Some(root) => (StatusCode::OK, Json(GetHeadOutput { root })).into_response(), 64 + None => ( 65 StatusCode::BAD_REQUEST, 66 + Json(json!({"error": "HeadNotFound", "message": format!("Could not find root for DID: {}", did)})), 67 ) 68 .into_response(), 69 } 70 } 71 ··· 76 77 pub async fn get_checkout( 78 State(state): State<AppState>, 79 + headers: HeaderMap, 80 Query(params): Query<GetCheckoutParams>, 81 ) -> Response { 82 let did = params.did.trim(); ··· 87 ) 88 .into_response(); 89 } 90 + let is_admin_or_self = check_admin_or_self(&state, &headers, did).await; 91 + let account = match assert_repo_availability(&state.db, did, is_admin_or_self).await { 92 + Ok(a) => a, 93 + Err(e) => return e.into_response(), 94 + }; 95 + let head_str = match account.repo_root_cid { 96 + Some(r) => r, 97 None => { 98 + return ( 99 + StatusCode::BAD_REQUEST, 100 + Json(json!({"error": "RepoNotFound", "message": "Repo not initialized"})), 101 + ) 102 + .into_response(); 103 } 104 }; 105 let head_cid = match Cid::from_str(&head_str) {
+3 -3
tests/account_lifecycle.rs
··· 154 let client = client(); 155 let base = base_url().await; 156 157 - let handle = format!("diddoctest-{}", uuid::Uuid::new_v4()); 158 let payload = json!({ 159 "handle": handle, 160 "email": format!("{}@example.com", handle), ··· 185 let client = client(); 186 let base = base_url().await; 187 188 - let handle = format!("tokentest-{}", uuid::Uuid::new_v4()); 189 let payload = json!({ 190 "handle": handle, 191 "email": format!("{}@example.com", handle), ··· 243 let client = client(); 244 let base = base_url().await; 245 246 - let handle = format!("pwdlentest-{}", uuid::Uuid::new_v4()); 247 let payload = json!({ 248 "handle": handle, 249 "email": format!("{}@example.com", handle),
··· 154 let client = client(); 155 let base = base_url().await; 156 157 + let handle = format!("dd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 158 let payload = json!({ 159 "handle": handle, 160 "email": format!("{}@example.com", handle), ··· 185 let client = client(); 186 let base = base_url().await; 187 188 + let handle = format!("tt{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 189 let payload = json!({ 190 "handle": handle, 191 "email": format!("{}@example.com", handle), ··· 243 let client = client(); 244 let base = base_url().await; 245 246 + let handle = format!("pl{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 247 let payload = json!({ 248 "handle": handle, 249 "email": format!("{}@example.com", handle),
+105 -4
tests/actor.rs
··· 140 } 141 142 #[tokio::test] 143 - async fn test_put_preferences_read_only_rejected() { 144 let client = client(); 145 let base = base_url().await; 146 let (token, _did) = create_account_and_login(&client).await; ··· 149 { 150 "$type": "app.bsky.actor.defs#declaredAgePref", 151 "isOverAge18": true 152 } 153 ] 154 }); ··· 159 .send() 160 .await 161 .unwrap(); 162 - assert_eq!(resp.status(), 400); 163 - let body: Value = resp.json().await.unwrap(); 164 - assert_eq!(body["error"], "InvalidRequest"); 165 } 166 167 #[tokio::test] ··· 328 let body: Value = resp.json().await.unwrap(); 329 assert!(body["preferences"].as_array().unwrap().is_empty()); 330 }
··· 140 } 141 142 #[tokio::test] 143 + async fn test_put_preferences_read_only_silently_filtered() { 144 let client = client(); 145 let base = base_url().await; 146 let (token, _did) = create_account_and_login(&client).await; ··· 149 { 150 "$type": "app.bsky.actor.defs#declaredAgePref", 151 "isOverAge18": true 152 + }, 153 + { 154 + "$type": "app.bsky.actor.defs#adultContentPref", 155 + "enabled": true 156 } 157 ] 158 }); ··· 163 .send() 164 .await 165 .unwrap(); 166 + assert_eq!(resp.status(), 200); 167 + let get_resp = client 168 + .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) 169 + .header("Authorization", format!("Bearer {}", token)) 170 + .send() 171 + .await 172 + .unwrap(); 173 + assert_eq!(get_resp.status(), 200); 174 + let body: Value = get_resp.json().await.unwrap(); 175 + let prefs_arr = body["preferences"].as_array().unwrap(); 176 + assert_eq!(prefs_arr.len(), 1); 177 + assert_eq!(prefs_arr[0]["$type"], "app.bsky.actor.defs#adultContentPref"); 178 } 179 180 #[tokio::test] ··· 341 let body: Value = resp.json().await.unwrap(); 342 assert!(body["preferences"].as_array().unwrap().is_empty()); 343 } 344 + 345 + #[tokio::test] 346 + async fn test_declared_age_pref_computed_from_birth_date() { 347 + let client = client(); 348 + let base = base_url().await; 349 + let (token, _did) = create_account_and_login(&client).await; 350 + let prefs = json!({ 351 + "preferences": [ 352 + { 353 + "$type": "app.bsky.actor.defs#personalDetailsPref", 354 + "birthDate": "1990-01-15" 355 + } 356 + ] 357 + }); 358 + let resp = client 359 + .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) 360 + .header("Authorization", format!("Bearer {}", token)) 361 + .json(&prefs) 362 + .send() 363 + .await 364 + .unwrap(); 365 + assert_eq!(resp.status(), 200); 366 + let get_resp = client 367 + .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) 368 + .header("Authorization", format!("Bearer {}", token)) 369 + .send() 370 + .await 371 + .unwrap(); 372 + assert_eq!(get_resp.status(), 200); 373 + let body: Value = get_resp.json().await.unwrap(); 374 + let prefs_arr = body["preferences"].as_array().unwrap(); 375 + assert_eq!(prefs_arr.len(), 2); 376 + let personal_details = prefs_arr 377 + .iter() 378 + .find(|p| p["$type"] == "app.bsky.actor.defs#personalDetailsPref"); 379 + assert!(personal_details.is_some()); 380 + assert_eq!(personal_details.unwrap()["birthDate"], "1990-01-15"); 381 + let declared_age = prefs_arr 382 + .iter() 383 + .find(|p| p["$type"] == "app.bsky.actor.defs#declaredAgePref"); 384 + assert!(declared_age.is_some()); 385 + let declared_age = declared_age.unwrap(); 386 + assert_eq!(declared_age["isOverAge13"], true); 387 + assert_eq!(declared_age["isOverAge16"], true); 388 + assert_eq!(declared_age["isOverAge18"], true); 389 + } 390 + 391 + #[tokio::test] 392 + async fn test_declared_age_pref_computed_under_18() { 393 + let client = client(); 394 + let base = base_url().await; 395 + let (token, _did) = create_account_and_login(&client).await; 396 + let current_year = chrono::Utc::now().format("%Y").to_string().parse::<i32>().unwrap(); 397 + let birth_year = current_year - 15; 398 + let prefs = json!({ 399 + "preferences": [ 400 + { 401 + "$type": "app.bsky.actor.defs#personalDetailsPref", 402 + "birthDate": format!("{}-06-15", birth_year) 403 + } 404 + ] 405 + }); 406 + let resp = client 407 + .post(format!("{}/xrpc/app.bsky.actor.putPreferences", base)) 408 + .header("Authorization", format!("Bearer {}", token)) 409 + .json(&prefs) 410 + .send() 411 + .await 412 + .unwrap(); 413 + assert_eq!(resp.status(), 200); 414 + let get_resp = client 415 + .get(format!("{}/xrpc/app.bsky.actor.getPreferences", base)) 416 + .header("Authorization", format!("Bearer {}", token)) 417 + .send() 418 + .await 419 + .unwrap(); 420 + assert_eq!(get_resp.status(), 200); 421 + let body: Value = get_resp.json().await.unwrap(); 422 + let prefs_arr = body["preferences"].as_array().unwrap(); 423 + let declared_age = prefs_arr 424 + .iter() 425 + .find(|p| p["$type"] == "app.bsky.actor.defs#declaredAgePref"); 426 + assert!(declared_age.is_some()); 427 + let declared_age = declared_age.unwrap(); 428 + assert_eq!(declared_age["isOverAge13"], true); 429 + assert_eq!(declared_age["isOverAge16"], false); 430 + assert_eq!(declared_age["isOverAge18"], false); 431 + }
+6 -6
tests/banned_words.rs
··· 31 #[tokio::test] 32 async fn test_handle_with_slur_rejected() { 33 let client = client(); 34 - let timestamp = chrono::Utc::now().timestamp_millis(); 35 let slur = decode("bmlnZ2Vy"); 36 - let offensive_handle = format!("{}{}", slur, timestamp); 37 38 let create_payload = json!({ 39 "handle": offensive_handle, 40 - "email": format!("test{}@example.com", timestamp), 41 "password": "TestPassword123!" 42 }); 43 ··· 65 #[tokio::test] 66 async fn test_handle_with_normalized_slur_rejected() { 67 let client = client(); 68 - let timestamp = chrono::Utc::now().timestamp_millis(); 69 let slur = decode("bi1pLWctZy1lLXI="); 70 - let offensive_handle = format!("{}{}", slur, timestamp); 71 72 let create_payload = json!({ 73 "handle": offensive_handle, 74 - "email": format!("test{}@example.com", timestamp), 75 "password": "TestPassword123!" 76 }); 77
··· 31 #[tokio::test] 32 async fn test_handle_with_slur_rejected() { 33 let client = client(); 34 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 35 let slur = decode("bmlnZ2Vy"); 36 + let offensive_handle = format!("{}{}", slur, suffix); 37 38 let create_payload = json!({ 39 "handle": offensive_handle, 40 + "email": format!("test{}@example.com", suffix), 41 "password": "TestPassword123!" 42 }); 43 ··· 65 #[tokio::test] 66 async fn test_handle_with_normalized_slur_rejected() { 67 let client = client(); 68 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..6]; 69 let slur = decode("bi1pLWctZy1lLXI="); 70 + let offensive_handle = format!("{}{}", slur, suffix); 71 72 let create_payload = json!({ 73 "handle": offensive_handle, 74 + "email": format!("test{}@example.com", suffix), 75 "password": "TestPassword123!" 76 }); 77
+1 -1
tests/common/mod.rs
··· 440 if attempt > 0 { 441 tokio::time::sleep(Duration::from_millis(100 * (attempt as u64 + 1))).await; 442 } 443 - let handle = format!("user-{}", uuid::Uuid::new_v4()); 444 let payload = json!({ 445 "handle": handle, 446 "email": format!("{}@example.com", handle),
··· 440 if attempt > 0 { 441 tokio::time::sleep(Duration::from_millis(100 * (attempt as u64 + 1))).await; 442 } 443 + let handle = format!("u{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 444 let payload = json!({ 445 "handle": handle, 446 "email": format!("{}@example.com", handle),
+7 -7
tests/did_web.rs
··· 11 #[tokio::test] 12 async fn test_create_self_hosted_did_web() { 13 let client = client(); 14 - let handle = format!("selfweb-{}", uuid::Uuid::new_v4()); 15 let payload = json!({ 16 "handle": handle, 17 "email": format!("{}@example.com", handle), ··· 98 let mock_uri = mock_server.uri(); 99 let mock_addr = mock_uri.trim_start_matches("http://"); 100 let did = format!("did:web:{}", mock_addr.replace(":", "%3A")); 101 - let handle = format!("extweb-{}", uuid::Uuid::new_v4()); 102 let pds_endpoint = base_url().await.replace("http://", "https://"); 103 104 let reserve_res = client ··· 180 #[tokio::test] 181 async fn test_plc_operations_blocked_for_did_web() { 182 let client = client(); 183 - let handle = format!("plcblock-{}", uuid::Uuid::new_v4()); 184 let payload = json!({ 185 "handle": handle, 186 "email": format!("{}@example.com", handle), ··· 245 #[tokio::test] 246 async fn test_get_recommended_did_credentials_no_rotation_keys_for_did_web() { 247 let client = client(); 248 - let handle = format!("creds-{}", uuid::Uuid::new_v4()); 249 let payload = json!({ 250 "handle": handle, 251 "email": format!("{}@example.com", handle), ··· 294 #[tokio::test] 295 async fn test_did_plc_still_works_with_did_type_param() { 296 let client = client(); 297 - let handle = format!("plctype-{}", uuid::Uuid::new_v4()); 298 let payload = json!({ 299 "handle": handle, 300 "email": format!("{}@example.com", handle), ··· 323 #[tokio::test] 324 async fn test_external_did_web_requires_did_field() { 325 let client = client(); 326 - let handle = format!("nodid-{}", uuid::Uuid::new_v4()); 327 let payload = json!({ 328 "handle": handle, 329 "email": format!("{}@example.com", handle), ··· 392 mock_addr.replace(":", "%3A"), 393 unique_id 394 ); 395 - let handle = format!("byod-{}", uuid::Uuid::new_v4()); 396 let pds_endpoint = base_url().await.replace("http://", "https://"); 397 let pds_did = format!("did:web:{}", pds_endpoint.trim_start_matches("https://")); 398
··· 11 #[tokio::test] 12 async fn test_create_self_hosted_did_web() { 13 let client = client(); 14 + let handle = format!("sw{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 15 let payload = json!({ 16 "handle": handle, 17 "email": format!("{}@example.com", handle), ··· 98 let mock_uri = mock_server.uri(); 99 let mock_addr = mock_uri.trim_start_matches("http://"); 100 let did = format!("did:web:{}", mock_addr.replace(":", "%3A")); 101 + let handle = format!("xw{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 102 let pds_endpoint = base_url().await.replace("http://", "https://"); 103 104 let reserve_res = client ··· 180 #[tokio::test] 181 async fn test_plc_operations_blocked_for_did_web() { 182 let client = client(); 183 + let handle = format!("pb{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 184 let payload = json!({ 185 "handle": handle, 186 "email": format!("{}@example.com", handle), ··· 245 #[tokio::test] 246 async fn test_get_recommended_did_credentials_no_rotation_keys_for_did_web() { 247 let client = client(); 248 + let handle = format!("cr{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 249 let payload = json!({ 250 "handle": handle, 251 "email": format!("{}@example.com", handle), ··· 294 #[tokio::test] 295 async fn test_did_plc_still_works_with_did_type_param() { 296 let client = client(); 297 + let handle = format!("pt{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 298 let payload = json!({ 299 "handle": handle, 300 "email": format!("{}@example.com", handle), ··· 323 #[tokio::test] 324 async fn test_external_did_web_requires_did_field() { 325 let client = client(); 326 + let handle = format!("nd{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 327 let payload = json!({ 328 "handle": handle, 329 "email": format!("{}@example.com", handle), ··· 392 mock_addr.replace(":", "%3A"), 393 unique_id 394 ); 395 + let handle = format!("by{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 396 let pds_endpoint = base_url().await.replace("http://", "https://"); 397 let pds_did = format!("did:web:{}", pds_endpoint.trim_start_matches("https://")); 398
+12 -12
tests/email_update.rs
··· 57 async fn test_request_email_update_returns_token_required() { 58 let client = common::client(); 59 let base_url = common::base_url().await; 60 - let handle = format!("emailreq-{}", uuid::Uuid::new_v4()); 61 let email = format!("{}@example.com", handle); 62 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 63 ··· 80 let client = common::client(); 81 let base_url = common::base_url().await; 82 let pool = common::get_test_db_pool().await; 83 - let handle = format!("emailup-{}", uuid::Uuid::new_v4()); 84 let email = format!("{}@example.com", handle); 85 let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await; 86 let new_email = format!("new_{}@example.com", handle); ··· 123 async fn test_update_email_requires_token_when_verified() { 124 let client = common::client(); 125 let base_url = common::base_url().await; 126 - let handle = format!("emailup-direct-{}", uuid::Uuid::new_v4()); 127 let email = format!("{}@example.com", handle); 128 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 129 let new_email = format!("direct_{}@example.com", handle); ··· 144 async fn test_update_email_same_email_noop() { 145 let client = common::client(); 146 let base_url = common::base_url().await; 147 - let handle = format!("emailup-same-{}", uuid::Uuid::new_v4()); 148 let email = format!("{}@example.com", handle); 149 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 150 ··· 166 async fn test_update_email_invalid_token() { 167 let client = common::client(); 168 let base_url = common::base_url().await; 169 - let handle = format!("emailup-badtok-{}", uuid::Uuid::new_v4()); 170 let email = format!("{}@example.com", handle); 171 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 172 let new_email = format!("badtok_{}@example.com", handle); ··· 217 async fn test_update_email_invalid_format() { 218 let client = common::client(); 219 let base_url = common::base_url().await; 220 - let handle = format!("emailup-fmt-{}", uuid::Uuid::new_v4()); 221 let email = format!("{}@example.com", handle); 222 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 223 ··· 236 let client = common::client(); 237 let base_url = common::base_url().await; 238 let pool = common::get_test_db_pool().await; 239 - let handle = format!("emailconfirm-{}", uuid::Uuid::new_v4()); 240 let email = format!("{}@example.com", handle); 241 242 let res = client ··· 298 let client = common::client(); 299 let base_url = common::base_url().await; 300 let pool = common::get_test_db_pool().await; 301 - let handle = format!("emailconf-wrong-{}", uuid::Uuid::new_v4()); 302 let email = format!("{}@example.com", handle); 303 304 let res = client ··· 352 async fn test_confirm_email_invalid_token() { 353 let client = common::client(); 354 let base_url = common::base_url().await; 355 - let handle = format!("emailconf-inv-{}", uuid::Uuid::new_v4()); 356 let email = format!("{}@example.com", handle); 357 358 let res = client ··· 392 let client = common::client(); 393 let base_url = common::base_url().await; 394 let pool = common::get_test_db_pool().await; 395 - let handle = format!("emailup-unverified-{}", uuid::Uuid::new_v4()); 396 let email = format!("{}@example.com", handle); 397 398 let res = client ··· 457 let base_url = common::base_url().await; 458 let pool = common::get_test_db_pool().await; 459 460 - let handle1 = format!("emailup-dup1-{}", uuid::Uuid::new_v4()); 461 let email1 = format!("{}@example.com", handle1); 462 let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await; 463 464 - let handle2 = format!("emailup-dup2-{}", uuid::Uuid::new_v4()); 465 let email2 = format!("{}@example.com", handle2); 466 let (access_jwt2, did2) = create_verified_account(&client, &base_url, &handle2, &email2).await; 467
··· 57 async fn test_request_email_update_returns_token_required() { 58 let client = common::client(); 59 let base_url = common::base_url().await; 60 + let handle = format!("er{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 61 let email = format!("{}@example.com", handle); 62 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 63 ··· 80 let client = common::client(); 81 let base_url = common::base_url().await; 82 let pool = common::get_test_db_pool().await; 83 + let handle = format!("eu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 84 let email = format!("{}@example.com", handle); 85 let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await; 86 let new_email = format!("new_{}@example.com", handle); ··· 123 async fn test_update_email_requires_token_when_verified() { 124 let client = common::client(); 125 let base_url = common::base_url().await; 126 + let handle = format!("ed{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 127 let email = format!("{}@example.com", handle); 128 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 129 let new_email = format!("direct_{}@example.com", handle); ··· 144 async fn test_update_email_same_email_noop() { 145 let client = common::client(); 146 let base_url = common::base_url().await; 147 + let handle = format!("es{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 148 let email = format!("{}@example.com", handle); 149 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 150 ··· 166 async fn test_update_email_invalid_token() { 167 let client = common::client(); 168 let base_url = common::base_url().await; 169 + let handle = format!("eb{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 170 let email = format!("{}@example.com", handle); 171 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 172 let new_email = format!("badtok_{}@example.com", handle); ··· 217 async fn test_update_email_invalid_format() { 218 let client = common::client(); 219 let base_url = common::base_url().await; 220 + let handle = format!("ef{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 221 let email = format!("{}@example.com", handle); 222 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 223 ··· 236 let client = common::client(); 237 let base_url = common::base_url().await; 238 let pool = common::get_test_db_pool().await; 239 + let handle = format!("ec{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 240 let email = format!("{}@example.com", handle); 241 242 let res = client ··· 298 let client = common::client(); 299 let base_url = common::base_url().await; 300 let pool = common::get_test_db_pool().await; 301 + let handle = format!("ew{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 302 let email = format!("{}@example.com", handle); 303 304 let res = client ··· 352 async fn test_confirm_email_invalid_token() { 353 let client = common::client(); 354 let base_url = common::base_url().await; 355 + let handle = format!("ei{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 356 let email = format!("{}@example.com", handle); 357 358 let res = client ··· 392 let client = common::client(); 393 let base_url = common::base_url().await; 394 let pool = common::get_test_db_pool().await; 395 + let handle = format!("ev{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 396 let email = format!("{}@example.com", handle); 397 398 let res = client ··· 457 let base_url = common::base_url().await; 458 let pool = common::get_test_db_pool().await; 459 460 + let handle1 = format!("d1{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 461 let email1 = format!("{}@example.com", handle1); 462 let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await; 463 464 + let handle2 = format!("d2{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 465 let email2 = format!("{}@example.com", handle2); 466 let (access_jwt2, did2) = create_verified_account(&client, &base_url, &handle2, &email2).await; 467
+4 -4
tests/identity.rs
··· 8 #[tokio::test] 9 async fn test_resolve_handle_success() { 10 let client = client(); 11 - let short_handle = format!("resolvetest-{}", uuid::Uuid::new_v4()); 12 let payload = json!({ 13 "handle": short_handle, 14 "email": format!("{}@example.com", short_handle), ··· 98 let mock_uri = mock_server.uri(); 99 let mock_addr = mock_uri.trim_start_matches("http://"); 100 let did = format!("did:web:{}", mock_addr.replace(":", "%3A")); 101 - let handle = format!("webuser-{}", uuid::Uuid::new_v4()); 102 let pds_endpoint = base_url().await.replace("http://", "https://"); 103 104 let reserve_res = client ··· 183 #[tokio::test] 184 async fn test_create_account_duplicate_handle() { 185 let client = client(); 186 - let handle = format!("dupe-{}", uuid::Uuid::new_v4()); 187 let email = format!("{}@example.com", handle); 188 let payload = json!({ 189 "handle": handle, ··· 220 let mock_server = MockServer::start().await; 221 let mock_uri = mock_server.uri(); 222 let mock_addr = mock_uri.trim_start_matches("http://"); 223 - let handle = format!("lifecycle-{}", uuid::Uuid::new_v4()); 224 let did = format!("did:web:{}:u:{}", mock_addr.replace(":", "%3A"), handle); 225 let email = format!("{}@test.com", handle); 226 let pds_endpoint = base_url().await.replace("http://", "https://");
··· 8 #[tokio::test] 9 async fn test_resolve_handle_success() { 10 let client = client(); 11 + let short_handle = format!("rt{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 12 let payload = json!({ 13 "handle": short_handle, 14 "email": format!("{}@example.com", short_handle), ··· 98 let mock_uri = mock_server.uri(); 99 let mock_addr = mock_uri.trim_start_matches("http://"); 100 let did = format!("did:web:{}", mock_addr.replace(":", "%3A")); 101 + let handle = format!("wu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 102 let pds_endpoint = base_url().await.replace("http://", "https://"); 103 104 let reserve_res = client ··· 183 #[tokio::test] 184 async fn test_create_account_duplicate_handle() { 185 let client = client(); 186 + let handle = format!("dp{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 187 let email = format!("{}@example.com", handle); 188 let payload = json!({ 189 "handle": handle, ··· 220 let mock_server = MockServer::start().await; 221 let mock_uri = mock_server.uri(); 222 let mock_addr = mock_uri.trim_start_matches("http://"); 223 + let handle = format!("lc{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 224 let did = format!("did:web:{}:u:{}", mock_addr.replace(":", "%3A"), handle); 225 let email = format!("{}@test.com", handle); 226 let pds_endpoint = base_url().await.replace("http://", "https://");
+3 -3
tests/jwt_security.rs
··· 669 async fn test_refresh_token_replay_protection() { 670 let url = base_url().await; 671 let http_client = client(); 672 - let ts = Utc::now().timestamp_millis(); 673 - let handle = format!("rt-replay-jwt-{}", ts); 674 - let email = format!("rt-replay-jwt-{}@example.com", ts); 675 676 let create_res = http_client 677 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
··· 669 async fn test_refresh_token_replay_protection() { 670 let url = base_url().await; 671 let http_client = client(); 672 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 673 + let handle = format!("rr{}", suffix); 674 + let email = format!("rr{}@example.com", suffix); 675 676 let create_res = http_client 677 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
+22 -22
tests/oauth.rs
··· 191 async fn test_full_oauth_flow() { 192 let url = base_url().await; 193 let http_client = client(); 194 - let ts = Utc::now().timestamp_millis(); 195 - let handle = format!("oauth-test-{}", ts); 196 - let email = format!("oauth-test-{}@example.com", ts); 197 let password = "Oauthtest123!"; 198 let create_res = http_client 199 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 209 let mock_client = setup_mock_client_metadata(redirect_uri).await; 210 let client_id = mock_client.uri(); 211 let (code_verifier, code_challenge) = generate_pkce(); 212 - let state = format!("state-{}", ts); 213 let par_res = http_client 214 .post(format!("{}/oauth/par", url)) 215 .form(&[ ··· 349 async fn test_oauth_error_cases() { 350 let url = base_url().await; 351 let http_client = client(); 352 - let ts = Utc::now().timestamp_millis(); 353 - let handle = format!("wrong-creds-{}", ts); 354 - let email = format!("wrong-creds-{}@example.com", ts); 355 http_client 356 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 357 .json(&json!({ "handle": handle, "email": email, "password": "Correct123!" })) ··· 435 async fn test_oauth_2fa_flow() { 436 let url = base_url().await; 437 let http_client = client(); 438 - let ts = Utc::now().timestamp_millis(); 439 - let handle = format!("2fa-test-{}", ts); 440 - let email = format!("2fa-test-{}@example.com", ts); 441 let password = "Twofa123test!"; 442 let create_res = http_client 443 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 557 async fn test_oauth_2fa_lockout() { 558 let url = base_url().await; 559 let http_client = client(); 560 - let ts = Utc::now().timestamp_millis(); 561 - let handle = format!("2fa-lockout-{}", ts); 562 - let email = format!("2fa-lockout-{}@example.com", ts); 563 let password = "Twofa123test!"; 564 let create_res = http_client 565 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 649 async fn test_account_selector_with_2fa() { 650 let url = base_url().await; 651 let http_client = client(); 652 - let ts = Utc::now().timestamp_millis(); 653 - let handle = format!("selector-2fa-{}", ts); 654 - let email = format!("selector-2fa-{}@example.com", ts); 655 let password = "Selector2fa123!"; 656 let create_res = http_client 657 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 835 async fn test_oauth_state_encoding() { 836 let url = base_url().await; 837 let http_client = client(); 838 - let ts = Utc::now().timestamp_millis(); 839 - let handle = format!("state-special-{}", ts); 840 - let email = format!("state-special-{}@example.com", ts); 841 let password = "State123special!"; 842 let create_res = http_client 843 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 914 async fn get_oauth_token_with_scope(scope: &str) -> (String, String, String) { 915 let url = base_url().await; 916 let http_client = client(); 917 - let ts = Utc::now().timestamp_millis(); 918 - let handle = format!("scope-test-{}", ts); 919 - let email = format!("scope-test-{}@example.com", ts); 920 let password = "Scopetest123!"; 921 let create_res = http_client 922 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
··· 191 async fn test_full_oauth_flow() { 192 let url = base_url().await; 193 let http_client = client(); 194 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 195 + let handle = format!("ot{}", suffix); 196 + let email = format!("ot{}@example.com", suffix); 197 let password = "Oauthtest123!"; 198 let create_res = http_client 199 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 209 let mock_client = setup_mock_client_metadata(redirect_uri).await; 210 let client_id = mock_client.uri(); 211 let (code_verifier, code_challenge) = generate_pkce(); 212 + let state = format!("state-{}", suffix); 213 let par_res = http_client 214 .post(format!("{}/oauth/par", url)) 215 .form(&[ ··· 349 async fn test_oauth_error_cases() { 350 let url = base_url().await; 351 let http_client = client(); 352 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 353 + let handle = format!("wc{}", suffix); 354 + let email = format!("wc{}@example.com", suffix); 355 http_client 356 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 357 .json(&json!({ "handle": handle, "email": email, "password": "Correct123!" })) ··· 435 async fn test_oauth_2fa_flow() { 436 let url = base_url().await; 437 let http_client = client(); 438 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 439 + let handle = format!("ft{}", suffix); 440 + let email = format!("ft{}@example.com", suffix); 441 let password = "Twofa123test!"; 442 let create_res = http_client 443 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 557 async fn test_oauth_2fa_lockout() { 558 let url = base_url().await; 559 let http_client = client(); 560 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 561 + let handle = format!("fl{}", suffix); 562 + let email = format!("fl{}@example.com", suffix); 563 let password = "Twofa123test!"; 564 let create_res = http_client 565 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 649 async fn test_account_selector_with_2fa() { 650 let url = base_url().await; 651 let http_client = client(); 652 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 653 + let handle = format!("sf{}", suffix); 654 + let email = format!("sf{}@example.com", suffix); 655 let password = "Selector2fa123!"; 656 let create_res = http_client 657 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 835 async fn test_oauth_state_encoding() { 836 let url = base_url().await; 837 let http_client = client(); 838 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 839 + let handle = format!("ss{}", suffix); 840 + let email = format!("ss{}@example.com", suffix); 841 let password = "State123special!"; 842 let create_res = http_client 843 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 914 async fn get_oauth_token_with_scope(scope: &str) -> (String, String, String) { 915 let url = base_url().await; 916 let http_client = client(); 917 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 918 + let handle = format!("st{}", suffix); 919 + let email = format!("st{}@example.com", suffix); 920 let password = "Scopetest123!"; 921 let create_res = http_client 922 .post(format!("{}/xrpc/com.atproto.server.createAccount", url))
+10 -10
tests/oauth_lifecycle.rs
··· 54 ) -> (OAuthSession, MockServer) { 55 let url = base_url().await; 56 let http_client = client(); 57 - let ts = Utc::now().timestamp_millis(); 58 - let handle = format!("{}-{}", handle_prefix, ts); 59 - let email = format!("{}-{}@example.com", handle_prefix, ts); 60 let password = format!("{}Pass123!", handle_prefix); 61 let create_res = http_client 62 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 269 let url = base_url().await; 270 let http_client = client(); 271 let (session, _mock) = 272 - create_user_and_oauth_session("oauth-lifecycle", "https://example.com/callback").await; 273 let collection = "app.bsky.feed.post"; 274 let original_text = "Original post content"; 275 let create_res = http_client ··· 439 let url = base_url().await; 440 let http_client = client(); 441 let (session, _mock) = 442 - create_user_and_oauth_session("oauth-refresh-access", "https://example.com/callback").await; 443 let collection = "app.bsky.feed.post"; 444 let create_res = http_client 445 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) ··· 520 let url = base_url().await; 521 let http_client = client(); 522 let (session, _mock) = 523 - create_user_and_oauth_session("oauth-revoke-access", "https://example.com/callback").await; 524 let collection = "app.bsky.feed.post"; 525 let create_res = http_client 526 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) ··· 574 async fn test_oauth_multiple_clients_same_user() { 575 let url = base_url().await; 576 let http_client = client(); 577 - let ts = Utc::now().timestamp_millis(); 578 - let handle = format!("multi-client-{}", ts); 579 - let email = format!("multi-client-{}@example.com", ts); 580 let password = "MultiClient123!"; 581 let create_res = http_client 582 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 949 let url = base_url().await; 950 let http_client = client(); 951 let (alice, _mock_alice) = 952 - create_user_and_oauth_session("alice-isolation", "https://alice.example.com/callback") 953 .await; 954 let (bob, _mock_bob) = 955 create_user_and_oauth_session("bob-isolation", "https://bob.example.com/callback").await;
··· 54 ) -> (OAuthSession, MockServer) { 55 let url = base_url().await; 56 let http_client = client(); 57 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..4]; 58 + let handle = format!("{}{}", handle_prefix, suffix); 59 + let email = format!("{}{}@example.com", handle_prefix, suffix); 60 let password = format!("{}Pass123!", handle_prefix); 61 let create_res = http_client 62 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 269 let url = base_url().await; 270 let http_client = client(); 271 let (session, _mock) = 272 + create_user_and_oauth_session("oauthlife", "https://example.com/callback").await; 273 let collection = "app.bsky.feed.post"; 274 let original_text = "Original post content"; 275 let create_res = http_client ··· 439 let url = base_url().await; 440 let http_client = client(); 441 let (session, _mock) = 442 + create_user_and_oauth_session("oauth-refr", "https://example.com/callback").await; 443 let collection = "app.bsky.feed.post"; 444 let create_res = http_client 445 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) ··· 520 let url = base_url().await; 521 let http_client = client(); 522 let (session, _mock) = 523 + create_user_and_oauth_session("oauth-revo", "https://example.com/callback").await; 524 let collection = "app.bsky.feed.post"; 525 let create_res = http_client 526 .post(format!("{}/xrpc/com.atproto.repo.createRecord", url)) ··· 574 async fn test_oauth_multiple_clients_same_user() { 575 let url = base_url().await; 576 let http_client = client(); 577 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 578 + let handle = format!("mc{}", suffix); 579 + let email = format!("mc{}@example.com", suffix); 580 let password = "MultiClient123!"; 581 let create_res = http_client 582 .post(format!("{}/xrpc/com.atproto.server.createAccount", url)) ··· 949 let url = base_url().await; 950 let http_client = client(); 951 let (alice, _mock_alice) = 952 + create_user_and_oauth_session("alice-isol", "https://alice.example.com/callback") 953 .await; 954 let (bob, _mock_bob) = 955 create_user_and_oauth_session("bob-isolation", "https://bob.example.com/callback").await;
+13 -13
tests/oauth_scopes.rs
··· 58 ) -> (OAuthSession, MockServer) { 59 let url = base_url().await; 60 let http_client = client(); 61 - let ts = Utc::now().timestamp_millis(); 62 - let handle = format!("{}-{}", handle_prefix, ts); 63 - let email = format!("{}-{}@example.com", handle_prefix, ts); 64 let password = format!("{}Pass123!", handle_prefix); 65 66 let create_res = http_client ··· 345 let url = base_url().await; 346 let http_client = client(); 347 let (session, _mock) = create_user_and_oauth_session_with_scope( 348 - "scope-transition", 349 "https://example.com/callback", 350 "atproto transition:generic", 351 ) ··· 380 let url = base_url().await; 381 let http_client = client(); 382 383 - let ts = Utc::now().timestamp_millis(); 384 - let handle = format!("consent-test-{}", ts); 385 - let email = format!("consent-{}@example.com", ts); 386 let password = "Consent123!"; 387 let redirect_uri = "https://consent-test.example.com/callback"; 388 ··· 476 let url = base_url().await; 477 let http_client = client(); 478 479 - let ts = Utc::now().timestamp_millis(); 480 - let handle = format!("consent-post-{}", ts); 481 - let email = format!("consent-post-{}@example.com", ts); 482 let password = "ConsentPost123!"; 483 let redirect_uri = "https://consent-post.example.com/callback"; 484 ··· 590 let url = base_url().await; 591 let http_client = client(); 592 593 - let ts = Utc::now().timestamp_millis(); 594 - let handle = format!("consent-req-{}", ts); 595 - let email = format!("consent-req-{}@example.com", ts); 596 let password = "ConsentReq123!"; 597 let redirect_uri = "https://consent-req.example.com/callback"; 598
··· 58 ) -> (OAuthSession, MockServer) { 59 let url = base_url().await; 60 let http_client = client(); 61 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..4]; 62 + let handle = format!("{}{}", handle_prefix, suffix); 63 + let email = format!("{}{}@example.com", handle_prefix, suffix); 64 let password = format!("{}Pass123!", handle_prefix); 65 66 let create_res = http_client ··· 345 let url = base_url().await; 346 let http_client = client(); 347 let (session, _mock) = create_user_and_oauth_session_with_scope( 348 + "scope-trans", 349 "https://example.com/callback", 350 "atproto transition:generic", 351 ) ··· 380 let url = base_url().await; 381 let http_client = client(); 382 383 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 384 + let handle = format!("ct{}", suffix); 385 + let email = format!("ct{}@example.com", suffix); 386 let password = "Consent123!"; 387 let redirect_uri = "https://consent-test.example.com/callback"; 388 ··· 476 let url = base_url().await; 477 let http_client = client(); 478 479 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 480 + let handle = format!("cp{}", suffix); 481 + let email = format!("cp{}@example.com", suffix); 482 let password = "ConsentPost123!"; 483 let redirect_uri = "https://consent-post.example.com/callback"; 484 ··· 590 let url = base_url().await; 591 let http_client = client(); 592 593 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 594 + let handle = format!("cq{}", suffix); 595 + let email = format!("cq{}@example.com", suffix); 596 let password = "ConsentReq123!"; 597 let redirect_uri = "https://consent-req.example.com/callback"; 598
+12 -12
tests/oauth_security.rs
··· 41 } 42 43 async fn get_oauth_tokens(http_client: &reqwest::Client, url: &str) -> (String, String, String) { 44 - let ts = Utc::now().timestamp_millis(); 45 - let handle = format!("sec-test-{}", ts); 46 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 47 .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Security123!" })) 48 .send().await.unwrap(); ··· 255 StatusCode::BAD_REQUEST, 256 "Missing PKCE challenge should be rejected" 257 ); 258 - let ts = Utc::now().timestamp_millis(); 259 - let handle = format!("pkce-attack-{}", ts); 260 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 261 .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Pkce123pass!" })) 262 .send().await.unwrap(); ··· 326 async fn test_replay_attacks() { 327 let url = base_url().await; 328 let http_client = client(); 329 - let ts = Utc::now().timestamp_millis(); 330 - let handle = format!("replay-{}", ts); 331 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 332 .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Replay123pass!" })) 333 .send().await.unwrap(); ··· 532 StatusCode::BAD_REQUEST, 533 "Unregistered redirect_uri should be rejected" 534 ); 535 - let ts = Utc::now().timestamp_millis(); 536 - let handle = format!("deact-{}", ts); 537 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 538 .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Deact123pass!" })) 539 .send().await.unwrap(); ··· 576 let client_id_a = mock_a.uri(); 577 let mock_b = setup_mock_client_metadata("https://app-b.com/callback").await; 578 let client_id_b = mock_b.uri(); 579 - let ts2 = Utc::now().timestamp_millis(); 580 - let handle2 = format!("cross-{}", ts2); 581 let create_res2 = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 582 .json(&json!({ "handle": handle2, "email": format!("{}@example.com", handle2), "password": "Cross123pass!" })) 583 .send().await.unwrap(); ··· 1110 async fn test_delegation_viewer_scope_cannot_write() { 1111 let url = base_url().await; 1112 let http_client = client(); 1113 - let ts = Utc::now().timestamp_millis(); 1114 1115 let (controller_jwt, controller_did) = create_account_and_login(&http_client).await; 1116 1117 - let delegated_handle = format!("deleg-{}", ts); 1118 let delegated_res = http_client 1119 .post(format!( 1120 "{}/xrpc/com.tranquil.delegation.createDelegatedAccount",
··· 41 } 42 43 async fn get_oauth_tokens(http_client: &reqwest::Client, url: &str) -> (String, String, String) { 44 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 45 + let handle = format!("se{}", suffix); 46 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 47 .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Security123!" })) 48 .send().await.unwrap(); ··· 255 StatusCode::BAD_REQUEST, 256 "Missing PKCE challenge should be rejected" 257 ); 258 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 259 + let handle = format!("pa{}", suffix); 260 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 261 .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Pkce123pass!" })) 262 .send().await.unwrap(); ··· 326 async fn test_replay_attacks() { 327 let url = base_url().await; 328 let http_client = client(); 329 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 330 + let handle = format!("rp{}", suffix); 331 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 332 .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Replay123pass!" })) 333 .send().await.unwrap(); ··· 532 StatusCode::BAD_REQUEST, 533 "Unregistered redirect_uri should be rejected" 534 ); 535 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 536 + let handle = format!("da{}", suffix); 537 let create_res = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 538 .json(&json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Deact123pass!" })) 539 .send().await.unwrap(); ··· 576 let client_id_a = mock_a.uri(); 577 let mock_b = setup_mock_client_metadata("https://app-b.com/callback").await; 578 let client_id_b = mock_b.uri(); 579 + let suffix2 = &uuid::Uuid::new_v4().simple().to_string()[..8]; 580 + let handle2 = format!("cr{}", suffix2); 581 let create_res2 = http_client.post(format!("{}/xrpc/com.atproto.server.createAccount", url)) 582 .json(&json!({ "handle": handle2, "email": format!("{}@example.com", handle2), "password": "Cross123pass!" })) 583 .send().await.unwrap(); ··· 1110 async fn test_delegation_viewer_scope_cannot_write() { 1111 let url = base_url().await; 1112 let http_client = client(); 1113 + let suffix = &uuid::Uuid::new_v4().simple().to_string()[..8]; 1114 1115 let (controller_jwt, controller_did) = create_account_and_login(&http_client).await; 1116 1117 + let delegated_handle = format!("dg{}", suffix); 1118 let delegated_res = http_client 1119 .post(format!( 1120 "{}/xrpc/com.tranquil.delegation.createDelegatedAccount",
+5 -5
tests/password_reset.rs
··· 9 let client = common::client(); 10 let base_url = common::base_url().await; 11 let pool = common::get_test_db_pool().await; 12 - let handle = format!("pwreset-{}", uuid::Uuid::new_v4()); 13 let email = format!("{}@example.com", handle); 14 let payload = json!({ 15 "handle": handle, ··· 71 let client = common::client(); 72 let base_url = common::base_url().await; 73 let pool = common::get_test_db_pool().await; 74 - let handle = format!("pwreset2-{}", uuid::Uuid::new_v4()); 75 let email = format!("{}@example.com", handle); 76 let old_password = "Oldpass123!"; 77 let new_password = "Newpass456!"; ··· 187 let client = common::client(); 188 let base_url = common::base_url().await; 189 let pool = common::get_test_db_pool().await; 190 - let handle = format!("pwreset3-{}", uuid::Uuid::new_v4()); 191 let email = format!("{}@example.com", handle); 192 let payload = json!({ 193 "handle": handle, ··· 251 let client = common::client(); 252 let base_url = common::base_url().await; 253 let pool = common::get_test_db_pool().await; 254 - let handle = format!("pwreset4-{}", uuid::Uuid::new_v4()); 255 let email = format!("{}@example.com", handle); 256 let payload = json!({ 257 "handle": handle, ··· 341 let pool = common::get_test_db_pool().await; 342 let client = common::client(); 343 let base_url = common::base_url().await; 344 - let handle = format!("pwreset5-{}", uuid::Uuid::new_v4()); 345 let email = format!("{}@example.com", handle); 346 let payload = json!({ 347 "handle": handle,
··· 9 let client = common::client(); 10 let base_url = common::base_url().await; 11 let pool = common::get_test_db_pool().await; 12 + let handle = format!("pr{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 13 let email = format!("{}@example.com", handle); 14 let payload = json!({ 15 "handle": handle, ··· 71 let client = common::client(); 72 let base_url = common::base_url().await; 73 let pool = common::get_test_db_pool().await; 74 + let handle = format!("pr2{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 75 let email = format!("{}@example.com", handle); 76 let old_password = "Oldpass123!"; 77 let new_password = "Newpass456!"; ··· 187 let client = common::client(); 188 let base_url = common::base_url().await; 189 let pool = common::get_test_db_pool().await; 190 + let handle = format!("pr3{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 191 let email = format!("{}@example.com", handle); 192 let payload = json!({ 193 "handle": handle, ··· 251 let client = common::client(); 252 let base_url = common::base_url().await; 253 let pool = common::get_test_db_pool().await; 254 + let handle = format!("pr4{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 255 let email = format!("{}@example.com", handle); 256 let payload = json!({ 257 "handle": handle, ··· 341 let pool = common::get_test_db_pool().await; 342 let client = common::client(); 343 let base_url = common::base_url().await; 344 + let handle = format!("pr5{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 345 let email = format!("{}@example.com", handle); 346 let payload = json!({ 347 "handle": handle,
+1 -1
tests/server.rs
··· 26 async fn test_account_and_session_lifecycle() { 27 let client = client(); 28 let base = base_url().await; 29 - let handle = format!("user-{}", uuid::Uuid::new_v4()); 30 let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Testpass123!" }); 31 let create_res = client 32 .post(format!("{}/xrpc/com.atproto.server.createAccount", base))
··· 26 async fn test_account_and_session_lifecycle() { 27 let client = client(); 28 let base = base_url().await; 29 + let handle = format!("u{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 30 let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Testpass123!" }); 31 let create_res = client 32 .post(format!("{}/xrpc/com.atproto.server.createAccount", base))
+5 -5
tests/signing_key.rs
··· 164 assert_eq!(res.status(), StatusCode::OK); 165 let body: Value = res.json().await.unwrap(); 166 let signing_key = body["signingKey"].as_str().unwrap(); 167 - let handle = format!("reserved-key-user-{}", uuid::Uuid::new_v4()); 168 let res = client 169 .post(format!( 170 "{}/xrpc/com.atproto.server.createAccount", ··· 202 async fn test_create_account_with_invalid_signing_key() { 203 let client = common::client(); 204 let base_url = common::base_url().await; 205 - let handle = format!("bad-key-user-{}", uuid::Uuid::new_v4()); 206 let res = client 207 .post(format!( 208 "{}/xrpc/com.atproto.server.createAccount", ··· 238 assert_eq!(res.status(), StatusCode::OK); 239 let body: Value = res.json().await.unwrap(); 240 let signing_key = body["signingKey"].as_str().unwrap(); 241 - let handle1 = format!("reuse-key-user1-{}", uuid::Uuid::new_v4()); 242 let res = client 243 .post(format!( 244 "{}/xrpc/com.atproto.server.createAccount", ··· 254 .await 255 .expect("Failed to create first account"); 256 assert_eq!(res.status(), StatusCode::OK); 257 - let handle2 = format!("reuse-key-user2-{}", uuid::Uuid::new_v4()); 258 let res = client 259 .post(format!( 260 "{}/xrpc/com.atproto.server.createAccount", ··· 291 assert_eq!(res.status(), StatusCode::OK); 292 let body: Value = res.json().await.unwrap(); 293 let signing_key = body["signingKey"].as_str().unwrap(); 294 - let handle = format!("token-test-user-{}", uuid::Uuid::new_v4()); 295 let res = client 296 .post(format!( 297 "{}/xrpc/com.atproto.server.createAccount",
··· 164 assert_eq!(res.status(), StatusCode::OK); 165 let body: Value = res.json().await.unwrap(); 166 let signing_key = body["signingKey"].as_str().unwrap(); 167 + let handle = format!("rk{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 168 let res = client 169 .post(format!( 170 "{}/xrpc/com.atproto.server.createAccount", ··· 202 async fn test_create_account_with_invalid_signing_key() { 203 let client = common::client(); 204 let base_url = common::base_url().await; 205 + let handle = format!("bk{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 206 let res = client 207 .post(format!( 208 "{}/xrpc/com.atproto.server.createAccount", ··· 238 assert_eq!(res.status(), StatusCode::OK); 239 let body: Value = res.json().await.unwrap(); 240 let signing_key = body["signingKey"].as_str().unwrap(); 241 + let handle1 = format!("r1{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 242 let res = client 243 .post(format!( 244 "{}/xrpc/com.atproto.server.createAccount", ··· 254 .await 255 .expect("Failed to create first account"); 256 assert_eq!(res.status(), StatusCode::OK); 257 + let handle2 = format!("r2{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 258 let res = client 259 .post(format!( 260 "{}/xrpc/com.atproto.server.createAccount", ··· 291 assert_eq!(res.status(), StatusCode::OK); 292 let body: Value = res.json().await.unwrap(); 293 let signing_key = body["signingKey"].as_str().unwrap(); 294 + let handle = format!("tu{}", &uuid::Uuid::new_v4().simple().to_string()[..12]); 295 let res = client 296 .post(format!( 297 "{}/xrpc/com.atproto.server.createAccount",
+165 -2
tests/sync_deprecated.rs
··· 62 .expect("Failed to send request"); 63 assert_eq!(not_found_res.status(), StatusCode::BAD_REQUEST); 64 let error_body: Value = not_found_res.json().await.unwrap(); 65 - assert_eq!(error_body["error"], "HeadNotFound"); 66 let missing_res = client 67 .get(format!( 68 "{}/xrpc/com.atproto.sync.getHead", ··· 165 .send() 166 .await 167 .expect("Failed to send request"); 168 - assert_eq!(not_found_res.status(), StatusCode::NOT_FOUND); 169 let error_body: Value = not_found_res.json().await.unwrap(); 170 assert_eq!(error_body["error"], "RepoNotFound"); 171 let missing_res = client ··· 188 .expect("Failed to send request"); 189 assert_eq!(empty_did_res.status(), StatusCode::BAD_REQUEST); 190 }
··· 62 .expect("Failed to send request"); 63 assert_eq!(not_found_res.status(), StatusCode::BAD_REQUEST); 64 let error_body: Value = not_found_res.json().await.unwrap(); 65 + assert_eq!(error_body["error"], "RepoNotFound"); 66 let missing_res = client 67 .get(format!( 68 "{}/xrpc/com.atproto.sync.getHead", ··· 165 .send() 166 .await 167 .expect("Failed to send request"); 168 + assert_eq!(not_found_res.status(), StatusCode::BAD_REQUEST); 169 let error_body: Value = not_found_res.json().await.unwrap(); 170 assert_eq!(error_body["error"], "RepoNotFound"); 171 let missing_res = client ··· 188 .expect("Failed to send request"); 189 assert_eq!(empty_did_res.status(), StatusCode::BAD_REQUEST); 190 } 191 + 192 + #[tokio::test] 193 + async fn test_get_head_deactivated_account_returns_error() { 194 + let client = client(); 195 + let base = base_url().await; 196 + let (did, jwt) = setup_new_user("deactheadtest").await; 197 + let res = client 198 + .get(format!("{}/xrpc/com.atproto.sync.getHead", base)) 199 + .query(&[("did", did.as_str())]) 200 + .send() 201 + .await 202 + .unwrap(); 203 + assert_eq!(res.status(), StatusCode::OK); 204 + client 205 + .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", base)) 206 + .bearer_auth(&jwt) 207 + .json(&serde_json::json!({})) 208 + .send() 209 + .await 210 + .unwrap(); 211 + let deact_res = client 212 + .get(format!("{}/xrpc/com.atproto.sync.getHead", base)) 213 + .query(&[("did", did.as_str())]) 214 + .send() 215 + .await 216 + .unwrap(); 217 + assert_eq!(deact_res.status(), StatusCode::BAD_REQUEST); 218 + let body: Value = deact_res.json().await.unwrap(); 219 + assert_eq!(body["error"], "RepoDeactivated"); 220 + } 221 + 222 + #[tokio::test] 223 + async fn test_get_head_takendown_account_returns_error() { 224 + let client = client(); 225 + let base = base_url().await; 226 + let (admin_jwt, _) = create_admin_account_and_login(&client).await; 227 + let (_, target_did) = create_account_and_login(&client).await; 228 + let res = client 229 + .get(format!("{}/xrpc/com.atproto.sync.getHead", base)) 230 + .query(&[("did", target_did.as_str())]) 231 + .send() 232 + .await 233 + .unwrap(); 234 + assert_eq!(res.status(), StatusCode::OK); 235 + client 236 + .post(format!("{}/xrpc/com.atproto.admin.updateSubjectStatus", base)) 237 + .bearer_auth(&admin_jwt) 238 + .json(&serde_json::json!({ 239 + "subject": { 240 + "$type": "com.atproto.admin.defs#repoRef", 241 + "did": target_did 242 + }, 243 + "takedown": { 244 + "applied": true, 245 + "ref": "test-takedown" 246 + } 247 + })) 248 + .send() 249 + .await 250 + .unwrap(); 251 + let takedown_res = client 252 + .get(format!("{}/xrpc/com.atproto.sync.getHead", base)) 253 + .query(&[("did", target_did.as_str())]) 254 + .send() 255 + .await 256 + .unwrap(); 257 + assert_eq!(takedown_res.status(), StatusCode::BAD_REQUEST); 258 + let body: Value = takedown_res.json().await.unwrap(); 259 + assert_eq!(body["error"], "RepoTakendown"); 260 + } 261 + 262 + #[tokio::test] 263 + async fn test_get_head_admin_can_access_deactivated() { 264 + let client = client(); 265 + let base = base_url().await; 266 + let (admin_jwt, _) = create_admin_account_and_login(&client).await; 267 + let (user_jwt, did) = create_account_and_login(&client).await; 268 + client 269 + .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", base)) 270 + .bearer_auth(&user_jwt) 271 + .json(&serde_json::json!({})) 272 + .send() 273 + .await 274 + .unwrap(); 275 + let res = client 276 + .get(format!("{}/xrpc/com.atproto.sync.getHead", base)) 277 + .bearer_auth(&admin_jwt) 278 + .query(&[("did", did.as_str())]) 279 + .send() 280 + .await 281 + .unwrap(); 282 + assert_eq!(res.status(), StatusCode::OK); 283 + } 284 + 285 + #[tokio::test] 286 + async fn test_get_checkout_deactivated_account_returns_error() { 287 + let client = client(); 288 + let base = base_url().await; 289 + let (did, jwt) = setup_new_user("deactcheckouttest").await; 290 + let res = client 291 + .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base)) 292 + .query(&[("did", did.as_str())]) 293 + .send() 294 + .await 295 + .unwrap(); 296 + assert_eq!(res.status(), StatusCode::OK); 297 + client 298 + .post(format!("{}/xrpc/com.atproto.server.deactivateAccount", base)) 299 + .bearer_auth(&jwt) 300 + .json(&serde_json::json!({})) 301 + .send() 302 + .await 303 + .unwrap(); 304 + let deact_res = client 305 + .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base)) 306 + .query(&[("did", did.as_str())]) 307 + .send() 308 + .await 309 + .unwrap(); 310 + assert_eq!(deact_res.status(), StatusCode::BAD_REQUEST); 311 + let body: Value = deact_res.json().await.unwrap(); 312 + assert_eq!(body["error"], "RepoDeactivated"); 313 + } 314 + 315 + #[tokio::test] 316 + async fn test_get_checkout_takendown_account_returns_error() { 317 + let client = client(); 318 + let base = base_url().await; 319 + let (admin_jwt, _) = create_admin_account_and_login(&client).await; 320 + let (_, target_did) = create_account_and_login(&client).await; 321 + let res = client 322 + .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base)) 323 + .query(&[("did", target_did.as_str())]) 324 + .send() 325 + .await 326 + .unwrap(); 327 + assert_eq!(res.status(), StatusCode::OK); 328 + client 329 + .post(format!("{}/xrpc/com.atproto.admin.updateSubjectStatus", base)) 330 + .bearer_auth(&admin_jwt) 331 + .json(&serde_json::json!({ 332 + "subject": { 333 + "$type": "com.atproto.admin.defs#repoRef", 334 + "did": target_did 335 + }, 336 + "takedown": { 337 + "applied": true, 338 + "ref": "test-takedown" 339 + } 340 + })) 341 + .send() 342 + .await 343 + .unwrap(); 344 + let takedown_res = client 345 + .get(format!("{}/xrpc/com.atproto.sync.getCheckout", base)) 346 + .query(&[("did", target_did.as_str())]) 347 + .send() 348 + .await 349 + .unwrap(); 350 + assert_eq!(takedown_res.status(), StatusCode::BAD_REQUEST); 351 + let body: Value = takedown_res.json().await.unwrap(); 352 + assert_eq!(body["error"], "RepoTakendown"); 353 + }