this repo has no description

Identity endpoint conformance vs ref

lewis 562287ef 4e58c5b0

+99 -23
src/api/identity/did.rs
··· 511 let rotation_keys = if auth_user.did.starts_with("did:web:") { 512 vec![] 513 } else { 514 - vec![did_key.clone()] 515 }; 516 ( 517 StatusCode::OK, ··· 559 return e; 560 } 561 let did = auth_user.did; 562 - let user_id = match sqlx::query_scalar!("SELECT id FROM users WHERE did = $1", did) 563 - .fetch_optional(&state.db) 564 .await 565 { 566 - Ok(Some(id)) => id, 567 _ => return ApiError::InternalError.into_response(), 568 }; 569 - let new_handle = input.handle.trim(); 570 if new_handle.is_empty() { 571 return ApiError::InvalidRequest("handle is required".into()).into_response(); 572 } 573 if !new_handle 574 .chars() 575 - .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_') 576 { 577 return ( 578 StatusCode::BAD_REQUEST, ··· 582 ) 583 .into_response(); 584 } 585 - if crate::moderation::has_explicit_slur(new_handle) { 586 return ( 587 StatusCode::BAD_REQUEST, 588 Json(json!({"error": "InvalidHandle", "message": "Inappropriate language in handle"})), ··· 591 } 592 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 593 let suffix = format!(".{}", hostname); 594 - let is_service_domain = crate::handle::is_service_domain_handle(new_handle, &hostname); 595 let handle = if is_service_domain { 596 let short_part = if new_handle.ends_with(&suffix) { 597 - new_handle.strip_suffix(&suffix).unwrap_or(new_handle) 598 } else { 599 - new_handle 600 }; 601 if short_part.contains('.') { 602 return ( 603 StatusCode::BAD_REQUEST, ··· 608 ) 609 .into_response(); 610 } 611 - if new_handle.ends_with(&suffix) { 612 - new_handle.to_string() 613 - } else { 614 - format!("{}.{}", new_handle, hostname) 615 } 616 } else { 617 - match crate::handle::verify_handle_ownership(new_handle, &did).await { 618 Ok(()) => {} 619 Err(crate::handle::HandleResolutionError::NotFound) => { 620 return ( ··· 649 .into_response(); 650 } 651 } 652 - new_handle.to_string() 653 }; 654 - let old_handle = sqlx::query_scalar!("SELECT handle FROM users WHERE id = $1", user_id) 655 - .fetch_optional(&state.db) 656 - .await 657 - .ok() 658 - .flatten(); 659 let existing = sqlx::query!( 660 "SELECT id FROM users WHERE handle = $1 AND id != $2", 661 handle, ··· 679 .await; 680 match result { 681 Ok(_) => { 682 - if let Some(old) = old_handle { 683 - let _ = state.cache.delete(&format!("handle:{}", old)).await; 684 } 685 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 686 if let Err(e) =
··· 511 let rotation_keys = if auth_user.did.starts_with("did:web:") { 512 vec![] 513 } else { 514 + let server_rotation_key = match std::env::var("PLC_ROTATION_KEY") { 515 + Ok(key) => key, 516 + Err(_) => { 517 + warn!("PLC_ROTATION_KEY not set, falling back to user's signing key for rotation key recommendation"); 518 + did_key.clone() 519 + } 520 + }; 521 + vec![server_rotation_key] 522 }; 523 ( 524 StatusCode::OK, ··· 566 return e; 567 } 568 let did = auth_user.did; 569 + if !state 570 + .check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did) 571 .await 572 { 573 + return ( 574 + StatusCode::TOO_MANY_REQUESTS, 575 + Json(json!({"error": "RateLimitExceeded", "message": "Too many handle updates. Try again later."})), 576 + ) 577 + .into_response(); 578 + } 579 + if !state 580 + .check_rate_limit(crate::state::RateLimitKind::HandleUpdateDaily, &did) 581 + .await 582 + { 583 + return ( 584 + StatusCode::TOO_MANY_REQUESTS, 585 + Json(json!({"error": "RateLimitExceeded", "message": "Daily handle update limit exceeded."})), 586 + ) 587 + .into_response(); 588 + } 589 + let user_row = match sqlx::query!( 590 + "SELECT id, handle FROM users WHERE did = $1", 591 + did 592 + ) 593 + .fetch_optional(&state.db) 594 + .await 595 + { 596 + Ok(Some(row)) => row, 597 _ => return ApiError::InternalError.into_response(), 598 }; 599 + let user_id = user_row.id; 600 + let current_handle = user_row.handle; 601 + let new_handle = input.handle.trim().to_ascii_lowercase(); 602 if new_handle.is_empty() { 603 return ApiError::InvalidRequest("handle is required".into()).into_response(); 604 } 605 if !new_handle 606 .chars() 607 + .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') 608 { 609 return ( 610 StatusCode::BAD_REQUEST, ··· 614 ) 615 .into_response(); 616 } 617 + for segment in new_handle.split('.') { 618 + if segment.is_empty() { 619 + return ( 620 + StatusCode::BAD_REQUEST, 621 + Json(json!({"error": "InvalidHandle", "message": "Handle contains empty segment"})), 622 + ) 623 + .into_response(); 624 + } 625 + if segment.starts_with('-') || segment.ends_with('-') { 626 + return ( 627 + StatusCode::BAD_REQUEST, 628 + Json(json!({"error": "InvalidHandle", "message": "Handle segment cannot start or end with hyphen"})), 629 + ) 630 + .into_response(); 631 + } 632 + } 633 + if crate::moderation::has_explicit_slur(&new_handle) { 634 return ( 635 StatusCode::BAD_REQUEST, 636 Json(json!({"error": "InvalidHandle", "message": "Inappropriate language in handle"})), ··· 639 } 640 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 641 let suffix = format!(".{}", hostname); 642 + let is_service_domain = crate::handle::is_service_domain_handle(&new_handle, &hostname); 643 let handle = if is_service_domain { 644 let short_part = if new_handle.ends_with(&suffix) { 645 + new_handle.strip_suffix(&suffix).unwrap_or(&new_handle) 646 } else { 647 + &new_handle 648 }; 649 + let full_handle = if new_handle.ends_with(&suffix) { 650 + new_handle.clone() 651 + } else { 652 + format!("{}.{}", new_handle, hostname) 653 + }; 654 + if full_handle == current_handle { 655 + if let Err(e) = 656 + crate::api::repo::record::sequence_identity_event(&state, &did, Some(&full_handle)) 657 + .await 658 + { 659 + warn!("Failed to sequence identity event for handle update: {}", e); 660 + } 661 + return (StatusCode::OK, Json(json!({}))).into_response(); 662 + } 663 if short_part.contains('.') { 664 return ( 665 StatusCode::BAD_REQUEST, ··· 670 ) 671 .into_response(); 672 } 673 + if short_part.len() < 3 { 674 + return ( 675 + StatusCode::BAD_REQUEST, 676 + Json(json!({"error": "InvalidHandle", "message": "Handle too short"})), 677 + ) 678 + .into_response(); 679 + } 680 + if short_part.len() > 18 { 681 + return ( 682 + StatusCode::BAD_REQUEST, 683 + Json(json!({"error": "InvalidHandle", "message": "Handle too long"})), 684 + ) 685 + .into_response(); 686 } 687 + full_handle 688 } else { 689 + if new_handle == current_handle { 690 + if let Err(e) = 691 + crate::api::repo::record::sequence_identity_event(&state, &did, Some(&new_handle)) 692 + .await 693 + { 694 + warn!("Failed to sequence identity event for handle update: {}", e); 695 + } 696 + return (StatusCode::OK, Json(json!({}))).into_response(); 697 + } 698 + match crate::handle::verify_handle_ownership(&new_handle, &did).await { 699 Ok(()) => {} 700 Err(crate::handle::HandleResolutionError::NotFound) => { 701 return ( ··· 730 .into_response(); 731 } 732 } 733 + new_handle.clone() 734 }; 735 let existing = sqlx::query!( 736 "SELECT id FROM users WHERE handle = $1 AND id != $2", 737 handle, ··· 755 .await; 756 match result { 757 Ok(_) => { 758 + if !current_handle.is_empty() { 759 + let _ = state.cache.delete(&format!("handle:{}", current_handle)).await; 760 } 761 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 762 if let Err(e) =
+28 -65
src/api/identity/plc/submit.rs
··· 23 headers: axum::http::HeaderMap, 24 Json(input): Json<SubmitPlcOperationInput>, 25 ) -> Response { 26 - info!("[MIGRATION] submitPlcOperation called"); 27 let bearer = match crate::auth::extract_bearer_token_from_header( 28 headers.get("Authorization").and_then(|h| h.to_str().ok()), 29 ) { 30 Some(t) => t, 31 None => { 32 - info!("[MIGRATION] submitPlcOperation: No bearer token"); 33 return ApiError::AuthenticationRequired.into_response(); 34 } 35 }; ··· 37 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &bearer).await { 38 Ok(user) => user, 39 Err(e) => { 40 - info!("[MIGRATION] submitPlcOperation: Auth failed: {:?}", e); 41 return ApiError::from(e).into_response(); 42 } 43 }; 44 - info!( 45 - "[MIGRATION] submitPlcOperation: Authenticated user did={}", 46 - auth_user.did 47 - ); 48 if let Err(e) = crate::auth::scope_check::check_identity_scope( 49 auth_user.is_oauth, 50 auth_user.scope.as_deref(), 51 crate::oauth::scopes::IdentityAttr::Wildcard, 52 ) { 53 - info!("[MIGRATION] submitPlcOperation: Scope check failed"); 54 return e; 55 } 56 let did = &auth_user.did; ··· 67 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 68 let public_url = format!("https://{}", hostname); 69 let user = match sqlx::query!( 70 - "SELECT id, handle, deactivated_at FROM users WHERE did = $1", 71 did 72 ) 73 .fetch_optional(&state.db) ··· 82 .into_response(); 83 } 84 }; 85 - let is_migration = user.deactivated_at.is_some(); 86 let key_row = match sqlx::query!( 87 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", 88 user.id ··· 123 } 124 }; 125 let user_did_key = signing_key_to_did_key(&signing_key); 126 - if !is_migration && let Some(rotation_keys) = op.get("rotationKeys").and_then(|v| v.as_array()) 127 - { 128 - let server_rotation_key = 129 - std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone()); 130 let has_server_key = rotation_keys 131 .iter() 132 .any(|k| k.as_str() == Some(&server_rotation_key)); ··· 167 .into_response(); 168 } 169 } 170 - if !is_migration { 171 - if let Some(verification_methods) = 172 - op.get("verificationMethods").and_then(|v| v.as_object()) 173 - && let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str()) 174 - && atproto_key != user_did_key 175 - { 176 - return ( 177 - StatusCode::BAD_REQUEST, 178 - Json(json!({ 179 - "error": "InvalidRequest", 180 - "message": "Incorrect signing key in verificationMethods" 181 - })), 182 - ) 183 - .into_response(); 184 - } 185 if let Some(also_known_as) = op.get("alsoKnownAs").and_then(|v| v.as_array()) { 186 let expected_handle = format!("at://{}", user.handle); 187 let first_aka = also_known_as.first().and_then(|v| v.as_str()); ··· 200 let plc_client = PlcClient::new(None); 201 let operation_clone = input.operation.clone(); 202 let did_clone = did.clone(); 203 - info!( 204 - "[MIGRATION] submitPlcOperation: Sending operation to PLC directory for did={}", 205 - did 206 - ); 207 - let plc_start = std::time::Instant::now(); 208 let result: Result<(), CircuitBreakerError<PlcError>> = 209 with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 210 plc_client ··· 213 }) 214 .await; 215 match result { 216 - Ok(()) => { 217 - info!( 218 - "[MIGRATION] submitPlcOperation: PLC directory accepted operation in {:?}", 219 - plc_start.elapsed() 220 - ); 221 - } 222 Err(CircuitBreakerError::CircuitOpen(e)) => { 223 - warn!( 224 - "[MIGRATION] submitPlcOperation: PLC directory circuit breaker open: {}", 225 - e 226 - ); 227 return ( 228 StatusCode::SERVICE_UNAVAILABLE, 229 Json(json!({ ··· 234 .into_response(); 235 } 236 Err(CircuitBreakerError::OperationFailed(e)) => { 237 - error!( 238 - "[MIGRATION] submitPlcOperation: PLC operation failed: {:?}", 239 - e 240 - ); 241 return ( 242 StatusCode::BAD_GATEWAY, 243 Json(json!({ ··· 248 .into_response(); 249 } 250 } 251 - info!( 252 - "[MIGRATION] submitPlcOperation: Sequencing identity event for did={}", 253 - did 254 - ); 255 match sqlx::query!( 256 "INSERT INTO repo_seq (did, event_type) VALUES ($1, 'identity') RETURNING seq", 257 did ··· 260 .await 261 { 262 Ok(row) => { 263 - info!( 264 - "[MIGRATION] submitPlcOperation: Identity event sequenced with seq={}", 265 - row.seq 266 - ); 267 if let Err(e) = sqlx::query(&format!("NOTIFY repo_updates, '{}'", row.seq)) 268 .execute(&state.db) 269 .await 270 { 271 - warn!( 272 - "[MIGRATION] submitPlcOperation: Failed to notify identity event: {:?}", 273 - e 274 - ); 275 } 276 } 277 Err(e) => { 278 - warn!( 279 - "[MIGRATION] submitPlcOperation: Failed to sequence identity event: {:?}", 280 - e 281 - ); 282 } 283 } 284 - info!("[MIGRATION] submitPlcOperation: SUCCESS for did={}", did); 285 (StatusCode::OK, Json(json!({}))).into_response() 286 }
··· 23 headers: axum::http::HeaderMap, 24 Json(input): Json<SubmitPlcOperationInput>, 25 ) -> Response { 26 let bearer = match crate::auth::extract_bearer_token_from_header( 27 headers.get("Authorization").and_then(|h| h.to_str().ok()), 28 ) { 29 Some(t) => t, 30 None => { 31 return ApiError::AuthenticationRequired.into_response(); 32 } 33 }; ··· 35 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &bearer).await { 36 Ok(user) => user, 37 Err(e) => { 38 return ApiError::from(e).into_response(); 39 } 40 }; 41 if let Err(e) = crate::auth::scope_check::check_identity_scope( 42 auth_user.is_oauth, 43 auth_user.scope.as_deref(), 44 crate::oauth::scopes::IdentityAttr::Wildcard, 45 ) { 46 return e; 47 } 48 let did = &auth_user.did; ··· 59 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 60 let public_url = format!("https://{}", hostname); 61 let user = match sqlx::query!( 62 + "SELECT id, handle FROM users WHERE did = $1", 63 did 64 ) 65 .fetch_optional(&state.db) ··· 74 .into_response(); 75 } 76 }; 77 let key_row = match sqlx::query!( 78 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", 79 user.id ··· 114 } 115 }; 116 let user_did_key = signing_key_to_did_key(&signing_key); 117 + let server_rotation_key = 118 + std::env::var("PLC_ROTATION_KEY").unwrap_or_else(|_| user_did_key.clone()); 119 + if let Some(rotation_keys) = op.get("rotationKeys").and_then(|v| v.as_array()) { 120 let has_server_key = rotation_keys 121 .iter() 122 .any(|k| k.as_str() == Some(&server_rotation_key)); ··· 157 .into_response(); 158 } 159 } 160 + if let Some(verification_methods) = op.get("verificationMethods").and_then(|v| v.as_object()) 161 + && let Some(atproto_key) = verification_methods.get("atproto").and_then(|v| v.as_str()) 162 + && atproto_key != user_did_key 163 + { 164 + return ( 165 + StatusCode::BAD_REQUEST, 166 + Json(json!({ 167 + "error": "InvalidRequest", 168 + "message": "Incorrect signing key in verificationMethods" 169 + })), 170 + ) 171 + .into_response(); 172 + } 173 + if !user.handle.is_empty() { 174 if let Some(also_known_as) = op.get("alsoKnownAs").and_then(|v| v.as_array()) { 175 let expected_handle = format!("at://{}", user.handle); 176 let first_aka = also_known_as.first().and_then(|v| v.as_str()); ··· 189 let plc_client = PlcClient::new(None); 190 let operation_clone = input.operation.clone(); 191 let did_clone = did.clone(); 192 let result: Result<(), CircuitBreakerError<PlcError>> = 193 with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 194 plc_client ··· 197 }) 198 .await; 199 match result { 200 + Ok(()) => {} 201 Err(CircuitBreakerError::CircuitOpen(e)) => { 202 + warn!("PLC directory circuit breaker open: {}", e); 203 return ( 204 StatusCode::SERVICE_UNAVAILABLE, 205 Json(json!({ ··· 210 .into_response(); 211 } 212 Err(CircuitBreakerError::OperationFailed(e)) => { 213 + error!("PLC operation failed: {:?}", e); 214 return ( 215 StatusCode::BAD_GATEWAY, 216 Json(json!({ ··· 221 .into_response(); 222 } 223 } 224 match sqlx::query!( 225 "INSERT INTO repo_seq (did, event_type) VALUES ($1, 'identity') RETURNING seq", 226 did ··· 229 .await 230 { 231 Ok(row) => { 232 if let Err(e) = sqlx::query(&format!("NOTIFY repo_updates, '{}'", row.seq)) 233 .execute(&state.db) 234 .await 235 { 236 + warn!("Failed to notify identity event: {:?}", e); 237 } 238 } 239 Err(e) => { 240 + warn!("Failed to sequence identity event: {:?}", e); 241 } 242 } 243 + let _ = state.cache.delete(&format!("handle:{}", user.handle)).await; 244 + if state.did_resolver.refresh_did(did).await.is_none() { 245 + warn!(did = %did, "Failed to refresh DID cache after PLC update"); 246 + } 247 + info!(did = %did, "PLC operation submitted successfully"); 248 (StatusCode::OK, Json(json!({}))).into_response() 249 }
+12 -12
src/api/validation.rs
··· 35 ), 36 Self::InvalidCharacters => write!( 37 f, 38 - "Handle contains invalid characters. Only alphanumeric, hyphens, and underscores are allowed" 39 ), 40 Self::StartsWithInvalidChar => { 41 - write!(f, "Handle cannot start with a hyphen or underscore") 42 } 43 - Self::EndsWithInvalidChar => write!(f, "Handle cannot end with a hyphen or underscore"), 44 Self::ContainsSpaces => write!(f, "Handle cannot contain spaces"), 45 Self::BannedWord => write!(f, "Inappropriate language in handle"), 46 } ··· 67 } 68 69 if let Some(first_char) = handle.chars().next() 70 - && (first_char == '-' || first_char == '_') 71 { 72 return Err(HandleValidationError::StartsWithInvalidChar); 73 } 74 75 if let Some(last_char) = handle.chars().last() 76 - && (last_char == '-' || last_char == '_') 77 { 78 return Err(HandleValidationError::EndsWithInvalidChar); 79 } 80 81 for c in handle.chars() { 82 - if !c.is_ascii_alphanumeric() && c != '-' && c != '_' { 83 return Err(HandleValidationError::InvalidCharacters); 84 } 85 } ··· 151 Ok("user-name".to_string()) 152 ); 153 assert_eq!( 154 - validate_short_handle("user_name"), 155 - Ok("user_name".to_string()) 156 - ); 157 - assert_eq!( 158 validate_short_handle("UPPERCASE"), 159 Ok("uppercase".to_string()) 160 ); ··· 194 ); 195 assert_eq!( 196 validate_short_handle("_starts"), 197 - Err(HandleValidationError::StartsWithInvalidChar) 198 ); 199 assert_eq!( 200 validate_short_handle("ends-"), ··· 202 ); 203 assert_eq!( 204 validate_short_handle("ends_"), 205 - Err(HandleValidationError::EndsWithInvalidChar) 206 ); 207 assert_eq!( 208 validate_short_handle("test@user"),
··· 35 ), 36 Self::InvalidCharacters => write!( 37 f, 38 + "Handle contains invalid characters. Only alphanumeric characters and hyphens are allowed" 39 ), 40 Self::StartsWithInvalidChar => { 41 + write!(f, "Handle cannot start with a hyphen") 42 } 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 } ··· 67 } 68 69 if let Some(first_char) = handle.chars().next() 70 + && first_char == '-' 71 { 72 return Err(HandleValidationError::StartsWithInvalidChar); 73 } 74 75 if let Some(last_char) = handle.chars().last() 76 + && last_char == '-' 77 { 78 return Err(HandleValidationError::EndsWithInvalidChar); 79 } 80 81 for c in handle.chars() { 82 + if !c.is_ascii_alphanumeric() && c != '-' { 83 return Err(HandleValidationError::InvalidCharacters); 84 } 85 } ··· 151 Ok("user-name".to_string()) 152 ); 153 assert_eq!( 154 validate_short_handle("UPPERCASE"), 155 Ok("uppercase".to_string()) 156 ); ··· 190 ); 191 assert_eq!( 192 validate_short_handle("_starts"), 193 + Err(HandleValidationError::InvalidCharacters) 194 ); 195 assert_eq!( 196 validate_short_handle("ends-"), ··· 198 ); 199 assert_eq!( 200 validate_short_handle("ends_"), 201 + Err(HandleValidationError::InvalidCharacters) 202 + ); 203 + assert_eq!( 204 + validate_short_handle("user_name"), 205 + Err(HandleValidationError::InvalidCharacters) 206 ); 207 assert_eq!( 208 validate_short_handle("test@user"),
+8
src/appview/mod.rs
··· 110 Some(resolved) 111 } 112 113 async fn resolve_did_internal(&self, did: &str) -> Option<ResolvedService> { 114 let did_doc = if did.starts_with("did:web:") { 115 self.resolve_did_web(did).await
··· 110 Some(resolved) 111 } 112 113 + pub async fn refresh_did(&self, did: &str) -> Option<ResolvedService> { 114 + { 115 + let mut cache = self.did_cache.write().await; 116 + cache.remove(did); 117 + } 118 + self.resolve_did(did).await 119 + } 120 + 121 async fn resolve_did_internal(&self, did: &str) -> Option<ResolvedService> { 122 let did_doc = if did.starts_with("did:web:") { 123 self.resolve_did_web(did).await
+4
src/handle/mod.rs
··· 93 } 94 95 pub fn is_service_domain_handle(handle: &str, hostname: &str) -> bool { 96 let service_domains: Vec<String> = std::env::var("PDS_SERVICE_HANDLE_DOMAINS") 97 .map(|s| s.split(',').map(|d| d.trim().to_string()).collect()) 98 .unwrap_or_else(|_| vec![hostname.to_string()]); ··· 115 fn test_is_service_domain_handle() { 116 assert!(is_service_domain_handle("user.example.com", "example.com")); 117 assert!(is_service_domain_handle("example.com", "example.com")); 118 assert!(!is_service_domain_handle("user.other.com", "example.com")); 119 assert!(!is_service_domain_handle("myhandle.xyz", "example.com")); 120 }
··· 93 } 94 95 pub fn is_service_domain_handle(handle: &str, hostname: &str) -> bool { 96 + if !handle.contains('.') { 97 + return true; 98 + } 99 let service_domains: Vec<String> = std::env::var("PDS_SERVICE_HANDLE_DOMAINS") 100 .map(|s| s.split(',').map(|d| d.trim().to_string()).collect()) 101 .unwrap_or_else(|_| vec![hostname.to_string()]); ··· 118 fn test_is_service_domain_handle() { 119 assert!(is_service_domain_handle("user.example.com", "example.com")); 120 assert!(is_service_domain_handle("example.com", "example.com")); 121 + assert!(is_service_domain_handle("myhandle", "example.com")); 122 assert!(!is_service_domain_handle("user.other.com", "example.com")); 123 assert!(!is_service_domain_handle("myhandle.xyz", "example.com")); 124 }
+12
src/rate_limit.rs
··· 30 pub app_password: Arc<KeyedRateLimiter>, 31 pub email_update: Arc<KeyedRateLimiter>, 32 pub totp_verify: Arc<KeyedRateLimiter>, 33 } 34 35 impl Default for RateLimiters { ··· 78 Quota::with_period(std::time::Duration::from_secs(60)) 79 .unwrap() 80 .allow_burst(NonZeroU32::new(5).unwrap()), 81 )), 82 } 83 }
··· 30 pub app_password: Arc<KeyedRateLimiter>, 31 pub email_update: Arc<KeyedRateLimiter>, 32 pub totp_verify: Arc<KeyedRateLimiter>, 33 + pub handle_update: Arc<KeyedRateLimiter>, 34 + pub handle_update_daily: Arc<KeyedRateLimiter>, 35 } 36 37 impl Default for RateLimiters { ··· 80 Quota::with_period(std::time::Duration::from_secs(60)) 81 .unwrap() 82 .allow_burst(NonZeroU32::new(5).unwrap()), 83 + )), 84 + handle_update: Arc::new(RateLimiter::keyed( 85 + Quota::with_period(std::time::Duration::from_secs(30)) 86 + .unwrap() 87 + .allow_burst(NonZeroU32::new(10).unwrap()), 88 + )), 89 + handle_update_daily: Arc::new(RateLimiter::keyed( 90 + Quota::with_period(std::time::Duration::from_secs(1728)) 91 + .unwrap() 92 + .allow_burst(NonZeroU32::new(50).unwrap()), 93 )), 94 } 95 }
+8
src/state.rs
··· 37 AppPassword, 38 EmailUpdate, 39 TotpVerify, 40 } 41 42 impl RateLimitKind { ··· 54 Self::AppPassword => "app_password", 55 Self::EmailUpdate => "email_update", 56 Self::TotpVerify => "totp_verify", 57 } 58 } 59 ··· 71 Self::AppPassword => (10, 60_000), 72 Self::EmailUpdate => (5, 3_600_000), 73 Self::TotpVerify => (5, 300_000), 74 } 75 } 76 } ··· 191 RateLimitKind::AppPassword => &self.rate_limiters.app_password, 192 RateLimitKind::EmailUpdate => &self.rate_limiters.email_update, 193 RateLimitKind::TotpVerify => &self.rate_limiters.totp_verify, 194 }; 195 196 let ok = limiter.check_key(&client_ip.to_string()).is_ok();
··· 37 AppPassword, 38 EmailUpdate, 39 TotpVerify, 40 + HandleUpdate, 41 + HandleUpdateDaily, 42 } 43 44 impl RateLimitKind { ··· 56 Self::AppPassword => "app_password", 57 Self::EmailUpdate => "email_update", 58 Self::TotpVerify => "totp_verify", 59 + Self::HandleUpdate => "handle_update", 60 + Self::HandleUpdateDaily => "handle_update_daily", 61 } 62 } 63 ··· 75 Self::AppPassword => (10, 60_000), 76 Self::EmailUpdate => (5, 3_600_000), 77 Self::TotpVerify => (5, 300_000), 78 + Self::HandleUpdate => (10, 300_000), 79 + Self::HandleUpdateDaily => (50, 86_400_000), 80 } 81 } 82 } ··· 197 RateLimitKind::AppPassword => &self.rate_limiters.app_password, 198 RateLimitKind::EmailUpdate => &self.rate_limiters.email_update, 199 RateLimitKind::TotpVerify => &self.rate_limiters.totp_verify, 200 + RateLimitKind::HandleUpdate => &self.rate_limiters.handle_update, 201 + RateLimitKind::HandleUpdateDaily => &self.rate_limiters.handle_update_daily, 202 }; 203 204 let ok = limiter.check_key(&client_ip.to_string()).is_ok();
+1 -1
tests/common/mod.rs
··· 430 if attempt > 0 { 431 tokio::time::sleep(Duration::from_millis(100 * (attempt as u64 + 1))).await; 432 } 433 - let handle = format!("user_{}", uuid::Uuid::new_v4()); 434 let payload = json!({ 435 "handle": handle, 436 "email": format!("{}@example.com", handle),
··· 430 if attempt > 0 { 431 tokio::time::sleep(Duration::from_millis(100 * (attempt as u64 + 1))).await; 432 } 433 + let handle = format!("user-{}", uuid::Uuid::new_v4()); 434 let payload = json!({ 435 "handle": handle, 436 "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!("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
+13 -13
tests/email_update.rs
··· 67 let client = common::client(); 68 let base_url = common::base_url().await; 69 let pool = get_pool().await; 70 - let handle = format!("emailup_{}", uuid::Uuid::new_v4()); 71 let email = format!("{}@example.com", handle); 72 let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await; 73 let new_email = format!("new_{}@example.com", handle); ··· 108 async fn test_request_email_update_taken_email() { 109 let client = common::client(); 110 let base_url = common::base_url().await; 111 - let handle1 = format!("emailup_taken1_{}", uuid::Uuid::new_v4()); 112 let email1 = format!("{}@example.com", handle1); 113 let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await; 114 - let handle2 = format!("emailup_taken2_{}", uuid::Uuid::new_v4()); 115 let email2 = format!("{}@example.com", handle2); 116 let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await; 117 let res = client ··· 133 async fn test_confirm_email_invalid_token() { 134 let client = common::client(); 135 let base_url = common::base_url().await; 136 - let handle = format!("emailup_inv_{}", uuid::Uuid::new_v4()); 137 let email = format!("{}@example.com", handle); 138 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 139 let new_email = format!("new_{}@example.com", handle); ··· 168 let client = common::client(); 169 let base_url = common::base_url().await; 170 let pool = get_pool().await; 171 - let handle = format!("emailup_wrong_{}", uuid::Uuid::new_v4()); 172 let email = format!("{}@example.com", handle); 173 let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await; 174 let new_email = format!("new_{}@example.com", handle); ··· 205 async fn test_update_email_requires_token() { 206 let client = common::client(); 207 let base_url = common::base_url().await; 208 - let handle = format!("emailup_direct_{}", uuid::Uuid::new_v4()); 209 let email = format!("{}@example.com", handle); 210 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 211 let new_email = format!("direct_{}@example.com", handle); ··· 225 async fn test_update_email_same_email_noop() { 226 let client = common::client(); 227 let base_url = common::base_url().await; 228 - let handle = format!("emailup_same_{}", uuid::Uuid::new_v4()); 229 let email = format!("{}@example.com", handle); 230 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 231 let res = client ··· 246 async fn test_update_email_requires_token_after_pending() { 247 let client = common::client(); 248 let base_url = common::base_url().await; 249 - let handle = format!("emailup_token_{}", uuid::Uuid::new_v4()); 250 let email = format!("{}@example.com", handle); 251 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 252 let new_email = format!("pending_{}@example.com", handle); ··· 278 let client = common::client(); 279 let base_url = common::base_url().await; 280 let pool = get_pool().await; 281 - let handle = format!("emailup_valid_{}", uuid::Uuid::new_v4()); 282 let email = format!("{}@example.com", handle); 283 let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await; 284 let new_email = format!("valid_{}@example.com", handle); ··· 316 async fn test_update_email_invalid_token() { 317 let client = common::client(); 318 let base_url = common::base_url().await; 319 - let handle = format!("emailup_badtok_{}", uuid::Uuid::new_v4()); 320 let email = format!("{}@example.com", handle); 321 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 322 let new_email = format!("badtok_{}@example.com", handle); ··· 350 async fn test_update_email_already_taken() { 351 let client = common::client(); 352 let base_url = common::base_url().await; 353 - let handle1 = format!("emailup_dup1_{}", uuid::Uuid::new_v4()); 354 let email1 = format!("{}@example.com", handle1); 355 let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await; 356 - let handle2 = format!("emailup_dup2_{}", uuid::Uuid::new_v4()); 357 let email2 = format!("{}@example.com", handle2); 358 let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await; 359 let res = client ··· 394 async fn test_update_email_invalid_format() { 395 let client = common::client(); 396 let base_url = common::base_url().await; 397 - let handle = format!("emailup_fmt_{}", uuid::Uuid::new_v4()); 398 let email = format!("{}@example.com", handle); 399 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 400 let res = client
··· 67 let client = common::client(); 68 let base_url = common::base_url().await; 69 let pool = get_pool().await; 70 + let handle = format!("emailup-{}", uuid::Uuid::new_v4()); 71 let email = format!("{}@example.com", handle); 72 let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await; 73 let new_email = format!("new_{}@example.com", handle); ··· 108 async fn test_request_email_update_taken_email() { 109 let client = common::client(); 110 let base_url = common::base_url().await; 111 + let handle1 = format!("emailup-taken1-{}", uuid::Uuid::new_v4()); 112 let email1 = format!("{}@example.com", handle1); 113 let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await; 114 + let handle2 = format!("emailup-taken2-{}", uuid::Uuid::new_v4()); 115 let email2 = format!("{}@example.com", handle2); 116 let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await; 117 let res = client ··· 133 async fn test_confirm_email_invalid_token() { 134 let client = common::client(); 135 let base_url = common::base_url().await; 136 + let handle = format!("emailup-inv-{}", uuid::Uuid::new_v4()); 137 let email = format!("{}@example.com", handle); 138 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 139 let new_email = format!("new_{}@example.com", handle); ··· 168 let client = common::client(); 169 let base_url = common::base_url().await; 170 let pool = get_pool().await; 171 + let handle = format!("emailup-wrong-{}", uuid::Uuid::new_v4()); 172 let email = format!("{}@example.com", handle); 173 let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await; 174 let new_email = format!("new_{}@example.com", handle); ··· 205 async fn test_update_email_requires_token() { 206 let client = common::client(); 207 let base_url = common::base_url().await; 208 + let handle = format!("emailup-direct-{}", uuid::Uuid::new_v4()); 209 let email = format!("{}@example.com", handle); 210 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 211 let new_email = format!("direct_{}@example.com", handle); ··· 225 async fn test_update_email_same_email_noop() { 226 let client = common::client(); 227 let base_url = common::base_url().await; 228 + let handle = format!("emailup-same-{}", uuid::Uuid::new_v4()); 229 let email = format!("{}@example.com", handle); 230 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 231 let res = client ··· 246 async fn test_update_email_requires_token_after_pending() { 247 let client = common::client(); 248 let base_url = common::base_url().await; 249 + let handle = format!("emailup-token-{}", uuid::Uuid::new_v4()); 250 let email = format!("{}@example.com", handle); 251 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 252 let new_email = format!("pending_{}@example.com", handle); ··· 278 let client = common::client(); 279 let base_url = common::base_url().await; 280 let pool = get_pool().await; 281 + let handle = format!("emailup-valid-{}", uuid::Uuid::new_v4()); 282 let email = format!("{}@example.com", handle); 283 let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await; 284 let new_email = format!("valid_{}@example.com", handle); ··· 316 async fn test_update_email_invalid_token() { 317 let client = common::client(); 318 let base_url = common::base_url().await; 319 + let handle = format!("emailup-badtok-{}", uuid::Uuid::new_v4()); 320 let email = format!("{}@example.com", handle); 321 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 322 let new_email = format!("badtok_{}@example.com", handle); ··· 350 async fn test_update_email_already_taken() { 351 let client = common::client(); 352 let base_url = common::base_url().await; 353 + let handle1 = format!("emailup-dup1-{}", uuid::Uuid::new_v4()); 354 let email1 = format!("{}@example.com", handle1); 355 let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await; 356 + let handle2 = format!("emailup-dup2-{}", uuid::Uuid::new_v4()); 357 let email2 = format!("{}@example.com", handle2); 358 let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await; 359 let res = client ··· 394 async fn test_update_email_invalid_format() { 395 let client = common::client(); 396 let base_url = common::base_url().await; 397 + let handle = format!("emailup-fmt-{}", uuid::Uuid::new_v4()); 398 let email = format!("{}@example.com", handle); 399 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 400 let res = client
+160 -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://"); ··· 378 let body: Value = res.json().await.expect("Response was not valid JSON"); 379 assert_eq!(body["error"], "AuthenticationRequired"); 380 }
··· 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://"); ··· 378 let body: Value = res.json().await.expect("Response was not valid JSON"); 379 assert_eq!(body["error"], "AuthenticationRequired"); 380 } 381 + 382 + #[tokio::test] 383 + async fn test_update_handle_to_same() { 384 + let client = client(); 385 + let (access_jwt, _did) = create_account_and_login(&client).await; 386 + let session = client 387 + .get(format!( 388 + "{}/xrpc/com.atproto.server.getSession", 389 + base_url().await 390 + )) 391 + .bearer_auth(&access_jwt) 392 + .send() 393 + .await 394 + .expect("Failed to get session"); 395 + let session_body: Value = session.json().await.expect("Invalid JSON"); 396 + let current_handle = session_body["handle"].as_str().expect("No handle").to_string(); 397 + let short_handle = current_handle.split('.').next().unwrap_or(&current_handle); 398 + let res = client 399 + .post(format!( 400 + "{}/xrpc/com.atproto.identity.updateHandle", 401 + base_url().await 402 + )) 403 + .bearer_auth(&access_jwt) 404 + .json(&json!({ "handle": short_handle })) 405 + .send() 406 + .await 407 + .expect("Failed to send request"); 408 + assert_eq!(res.status(), StatusCode::OK); 409 + } 410 + 411 + #[tokio::test] 412 + async fn test_update_handle_no_auth() { 413 + let client = client(); 414 + let res = client 415 + .post(format!( 416 + "{}/xrpc/com.atproto.identity.updateHandle", 417 + base_url().await 418 + )) 419 + .json(&json!({ "handle": "newhandle" })) 420 + .send() 421 + .await 422 + .expect("Failed to send request"); 423 + assert_eq!(res.status(), StatusCode::UNAUTHORIZED); 424 + let body: Value = res.json().await.expect("Response was not valid JSON"); 425 + assert_eq!(body["error"], "AuthenticationRequired"); 426 + } 427 + 428 + #[tokio::test] 429 + async fn test_update_handle_invalid_characters() { 430 + let client = client(); 431 + let (access_jwt, _did) = create_account_and_login(&client).await; 432 + let res = client 433 + .post(format!( 434 + "{}/xrpc/com.atproto.identity.updateHandle", 435 + base_url().await 436 + )) 437 + .bearer_auth(&access_jwt) 438 + .json(&json!({ "handle": "invalid@handle!" })) 439 + .send() 440 + .await 441 + .expect("Failed to send request"); 442 + assert_eq!(res.status(), StatusCode::BAD_REQUEST); 443 + let body: Value = res.json().await.expect("Response was not valid JSON"); 444 + assert_eq!(body["error"], "InvalidHandle"); 445 + } 446 + 447 + #[tokio::test] 448 + async fn test_update_handle_empty() { 449 + let client = client(); 450 + let (access_jwt, _did) = create_account_and_login(&client).await; 451 + let res = client 452 + .post(format!( 453 + "{}/xrpc/com.atproto.identity.updateHandle", 454 + base_url().await 455 + )) 456 + .bearer_auth(&access_jwt) 457 + .json(&json!({ "handle": "" })) 458 + .send() 459 + .await 460 + .expect("Failed to send request"); 461 + assert_eq!(res.status(), StatusCode::BAD_REQUEST); 462 + let body: Value = res.json().await.expect("Response was not valid JSON"); 463 + assert_eq!(body["error"], "InvalidRequest"); 464 + } 465 + 466 + #[tokio::test] 467 + async fn test_update_handle_taken() { 468 + let client = client(); 469 + let (access_jwt1, _did1) = create_account_and_login(&client).await; 470 + let (access_jwt2, _did2) = create_account_and_login(&client).await; 471 + let short_handle = format!("taken{}", &uuid::Uuid::new_v4().to_string()[..8]); 472 + let update1 = client 473 + .post(format!( 474 + "{}/xrpc/com.atproto.identity.updateHandle", 475 + base_url().await 476 + )) 477 + .bearer_auth(&access_jwt1) 478 + .json(&json!({ "handle": short_handle })) 479 + .send() 480 + .await 481 + .expect("Failed to update handle"); 482 + assert_eq!(update1.status(), StatusCode::OK); 483 + let res = client 484 + .post(format!( 485 + "{}/xrpc/com.atproto.identity.updateHandle", 486 + base_url().await 487 + )) 488 + .bearer_auth(&access_jwt2) 489 + .json(&json!({ "handle": short_handle })) 490 + .send() 491 + .await 492 + .expect("Failed to send request"); 493 + assert_eq!(res.status(), StatusCode::BAD_REQUEST); 494 + let body: Value = res.json().await.expect("Response was not valid JSON"); 495 + assert_eq!(body["error"], "HandleTaken"); 496 + } 497 + 498 + #[tokio::test] 499 + async fn test_update_handle_too_short() { 500 + let client = client(); 501 + let (access_jwt, _did) = create_account_and_login(&client).await; 502 + let res = client 503 + .post(format!( 504 + "{}/xrpc/com.atproto.identity.updateHandle", 505 + base_url().await 506 + )) 507 + .bearer_auth(&access_jwt) 508 + .json(&json!({ "handle": "ab" })) 509 + .send() 510 + .await 511 + .expect("Failed to send request"); 512 + assert_eq!(res.status(), StatusCode::BAD_REQUEST); 513 + let body: Value = res.json().await.expect("Response was not valid JSON"); 514 + assert_eq!(body["error"], "InvalidHandle"); 515 + assert!(body["message"].as_str().unwrap().contains("short")); 516 + } 517 + 518 + #[tokio::test] 519 + async fn test_update_handle_too_long() { 520 + let client = client(); 521 + let (access_jwt, _did) = create_account_and_login(&client).await; 522 + let res = client 523 + .post(format!( 524 + "{}/xrpc/com.atproto.identity.updateHandle", 525 + base_url().await 526 + )) 527 + .bearer_auth(&access_jwt) 528 + .json(&json!({ "handle": "thishandleiswaytoolongforservicedomain" })) 529 + .send() 530 + .await 531 + .expect("Failed to send request"); 532 + assert_eq!(res.status(), StatusCode::BAD_REQUEST); 533 + let body: Value = res.json().await.expect("Response was not valid JSON"); 534 + assert_eq!(body["error"], "InvalidHandle"); 535 + assert!(body["message"].as_str().unwrap().contains("long")); 536 + }
+5 -5
tests/password_reset.rs
··· 19 let client = common::client(); 20 let base_url = common::base_url().await; 21 let pool = get_pool().await; 22 - let handle = format!("pwreset_{}", uuid::Uuid::new_v4()); 23 let email = format!("{}@example.com", handle); 24 let payload = json!({ 25 "handle": handle, ··· 81 let client = common::client(); 82 let base_url = common::base_url().await; 83 let pool = get_pool().await; 84 - let handle = format!("pwreset2_{}", uuid::Uuid::new_v4()); 85 let email = format!("{}@example.com", handle); 86 let old_password = "Oldpass123!"; 87 let new_password = "Newpass456!"; ··· 197 let client = common::client(); 198 let base_url = common::base_url().await; 199 let pool = get_pool().await; 200 - let handle = format!("pwreset3_{}", uuid::Uuid::new_v4()); 201 let email = format!("{}@example.com", handle); 202 let payload = json!({ 203 "handle": handle, ··· 261 let client = common::client(); 262 let base_url = common::base_url().await; 263 let pool = get_pool().await; 264 - let handle = format!("pwreset4_{}", uuid::Uuid::new_v4()); 265 let email = format!("{}@example.com", handle); 266 let payload = json!({ 267 "handle": handle, ··· 351 let pool = get_pool().await; 352 let client = common::client(); 353 let base_url = common::base_url().await; 354 - let handle = format!("pwreset5_{}", uuid::Uuid::new_v4()); 355 let email = format!("{}@example.com", handle); 356 let payload = json!({ 357 "handle": handle,
··· 19 let client = common::client(); 20 let base_url = common::base_url().await; 21 let pool = get_pool().await; 22 + let handle = format!("pwreset-{}", uuid::Uuid::new_v4()); 23 let email = format!("{}@example.com", handle); 24 let payload = json!({ 25 "handle": handle, ··· 81 let client = common::client(); 82 let base_url = common::base_url().await; 83 let pool = get_pool().await; 84 + let handle = format!("pwreset2-{}", uuid::Uuid::new_v4()); 85 let email = format!("{}@example.com", handle); 86 let old_password = "Oldpass123!"; 87 let new_password = "Newpass456!"; ··· 197 let client = common::client(); 198 let base_url = common::base_url().await; 199 let pool = get_pool().await; 200 + let handle = format!("pwreset3-{}", uuid::Uuid::new_v4()); 201 let email = format!("{}@example.com", handle); 202 let payload = json!({ 203 "handle": handle, ··· 261 let client = common::client(); 262 let base_url = common::base_url().await; 263 let pool = get_pool().await; 264 + let handle = format!("pwreset4-{}", uuid::Uuid::new_v4()); 265 let email = format!("{}@example.com", handle); 266 let payload = json!({ 267 "handle": handle, ··· 351 let pool = get_pool().await; 352 let client = common::client(); 353 let base_url = common::base_url().await; 354 + let handle = format!("pwreset5-{}", uuid::Uuid::new_v4()); 355 let email = format!("{}@example.com", handle); 356 let payload = json!({ 357 "handle": handle,
+4 -4
tests/rate_limit.rs
··· 9 let client = client(); 10 let url = format!("{}/xrpc/com.atproto.server.createSession", base_url().await); 11 let payload = json!({ 12 - "identifier": "nonexistent_user_for_rate_limit_test", 13 "password": "wrongpassword" 14 }); 15 let mut rate_limited_count = 0; ··· 53 let mut success_count = 0; 54 for i in 0..8 { 55 let payload = json!({ 56 - "email": format!("ratelimit_test_{}@example.com", i) 57 }); 58 let res = client 59 .post(&url) ··· 91 for i in 0..15 { 92 let unique_id = uuid::Uuid::new_v4(); 93 let payload = json!({ 94 - "handle": format!("ratelimit_{}_{}", i, unique_id), 95 - "email": format!("ratelimit_{}_{}@example.com", i, unique_id), 96 "password": "Testpass123!" 97 }); 98 let res = client
··· 9 let client = client(); 10 let url = format!("{}/xrpc/com.atproto.server.createSession", base_url().await); 11 let payload = json!({ 12 + "identifier": "nonexistent-user-for-rate-limit-test", 13 "password": "wrongpassword" 14 }); 15 let mut rate_limited_count = 0; ··· 53 let mut success_count = 0; 54 for i in 0..8 { 55 let payload = json!({ 56 + "email": format!("ratelimit-test_{}@example.com", i) 57 }); 58 let res = client 59 .post(&url) ··· 91 for i in 0..15 { 92 let unique_id = uuid::Uuid::new_v4(); 93 let payload = json!({ 94 + "handle": format!("ratelimit-{}_{}", i, unique_id), 95 + "email": format!("ratelimit-{}_{}@example.com", i, unique_id), 96 "password": "Testpass123!" 97 }); 98 let res = client
+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!("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))
+5 -5
tests/signing_key.rs
··· 174 assert_eq!(res.status(), StatusCode::OK); 175 let body: Value = res.json().await.unwrap(); 176 let signing_key = body["signingKey"].as_str().unwrap(); 177 - let handle = format!("reserved_key_user_{}", uuid::Uuid::new_v4()); 178 let res = client 179 .post(format!( 180 "{}/xrpc/com.atproto.server.createAccount", ··· 212 async fn test_create_account_with_invalid_signing_key() { 213 let client = common::client(); 214 let base_url = common::base_url().await; 215 - let handle = format!("bad_key_user_{}", uuid::Uuid::new_v4()); 216 let res = client 217 .post(format!( 218 "{}/xrpc/com.atproto.server.createAccount", ··· 248 assert_eq!(res.status(), StatusCode::OK); 249 let body: Value = res.json().await.unwrap(); 250 let signing_key = body["signingKey"].as_str().unwrap(); 251 - let handle1 = format!("reuse_key_user1_{}", uuid::Uuid::new_v4()); 252 let res = client 253 .post(format!( 254 "{}/xrpc/com.atproto.server.createAccount", ··· 264 .await 265 .expect("Failed to create first account"); 266 assert_eq!(res.status(), StatusCode::OK); 267 - let handle2 = format!("reuse_key_user2_{}", uuid::Uuid::new_v4()); 268 let res = client 269 .post(format!( 270 "{}/xrpc/com.atproto.server.createAccount", ··· 301 assert_eq!(res.status(), StatusCode::OK); 302 let body: Value = res.json().await.unwrap(); 303 let signing_key = body["signingKey"].as_str().unwrap(); 304 - let handle = format!("token_test_user_{}", uuid::Uuid::new_v4()); 305 let res = client 306 .post(format!( 307 "{}/xrpc/com.atproto.server.createAccount",
··· 174 assert_eq!(res.status(), StatusCode::OK); 175 let body: Value = res.json().await.unwrap(); 176 let signing_key = body["signingKey"].as_str().unwrap(); 177 + let handle = format!("reserved-key-user-{}", uuid::Uuid::new_v4()); 178 let res = client 179 .post(format!( 180 "{}/xrpc/com.atproto.server.createAccount", ··· 212 async fn test_create_account_with_invalid_signing_key() { 213 let client = common::client(); 214 let base_url = common::base_url().await; 215 + let handle = format!("bad-key-user-{}", uuid::Uuid::new_v4()); 216 let res = client 217 .post(format!( 218 "{}/xrpc/com.atproto.server.createAccount", ··· 248 assert_eq!(res.status(), StatusCode::OK); 249 let body: Value = res.json().await.unwrap(); 250 let signing_key = body["signingKey"].as_str().unwrap(); 251 + let handle1 = format!("reuse-key-user1-{}", uuid::Uuid::new_v4()); 252 let res = client 253 .post(format!( 254 "{}/xrpc/com.atproto.server.createAccount", ··· 264 .await 265 .expect("Failed to create first account"); 266 assert_eq!(res.status(), StatusCode::OK); 267 + let handle2 = format!("reuse-key-user2-{}", uuid::Uuid::new_v4()); 268 let res = client 269 .post(format!( 270 "{}/xrpc/com.atproto.server.createAccount", ··· 301 assert_eq!(res.status(), StatusCode::OK); 302 let body: Value = res.json().await.unwrap(); 303 let signing_key = body["signingKey"].as_str().unwrap(); 304 + let handle = format!("token-test-user-{}", uuid::Uuid::new_v4()); 305 let res = client 306 .post(format!( 307 "{}/xrpc/com.atproto.server.createAccount",