this repo has no description

Identity endpoint conformance vs ref

lewis 562287ef 4e58c5b0

+99 -23
src/api/identity/did.rs
··· 511 511 let rotation_keys = if auth_user.did.starts_with("did:web:") { 512 512 vec![] 513 513 } else { 514 - vec![did_key.clone()] 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] 515 522 }; 516 523 ( 517 524 StatusCode::OK, ··· 559 566 return e; 560 567 } 561 568 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) 569 + if !state 570 + .check_rate_limit(crate::state::RateLimitKind::HandleUpdate, &did) 564 571 .await 565 572 { 566 - Ok(Some(id)) => id, 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, 567 597 _ => return ApiError::InternalError.into_response(), 568 598 }; 569 - let new_handle = input.handle.trim(); 599 + let user_id = user_row.id; 600 + let current_handle = user_row.handle; 601 + let new_handle = input.handle.trim().to_ascii_lowercase(); 570 602 if new_handle.is_empty() { 571 603 return ApiError::InvalidRequest("handle is required".into()).into_response(); 572 604 } 573 605 if !new_handle 574 606 .chars() 575 - .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-' || c == '_') 607 + .all(|c| c.is_ascii_alphanumeric() || c == '.' || c == '-') 576 608 { 577 609 return ( 578 610 StatusCode::BAD_REQUEST, ··· 582 614 ) 583 615 .into_response(); 584 616 } 585 - if crate::moderation::has_explicit_slur(new_handle) { 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) { 586 634 return ( 587 635 StatusCode::BAD_REQUEST, 588 636 Json(json!({"error": "InvalidHandle", "message": "Inappropriate language in handle"})), ··· 591 639 } 592 640 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 593 641 let suffix = format!(".{}", hostname); 594 - let is_service_domain = crate::handle::is_service_domain_handle(new_handle, &hostname); 642 + let is_service_domain = crate::handle::is_service_domain_handle(&new_handle, &hostname); 595 643 let handle = if is_service_domain { 596 644 let short_part = if new_handle.ends_with(&suffix) { 597 - new_handle.strip_suffix(&suffix).unwrap_or(new_handle) 645 + new_handle.strip_suffix(&suffix).unwrap_or(&new_handle) 598 646 } else { 599 - new_handle 647 + &new_handle 600 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 + } 601 663 if short_part.contains('.') { 602 664 return ( 603 665 StatusCode::BAD_REQUEST, ··· 608 670 ) 609 671 .into_response(); 610 672 } 611 - if new_handle.ends_with(&suffix) { 612 - new_handle.to_string() 613 - } else { 614 - format!("{}.{}", new_handle, hostname) 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(); 615 686 } 687 + full_handle 616 688 } else { 617 - match crate::handle::verify_handle_ownership(new_handle, &did).await { 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 { 618 699 Ok(()) => {} 619 700 Err(crate::handle::HandleResolutionError::NotFound) => { 620 701 return ( ··· 649 730 .into_response(); 650 731 } 651 732 } 652 - new_handle.to_string() 733 + new_handle.clone() 653 734 }; 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 735 let existing = sqlx::query!( 660 736 "SELECT id FROM users WHERE handle = $1 AND id != $2", 661 737 handle, ··· 679 755 .await; 680 756 match result { 681 757 Ok(_) => { 682 - if let Some(old) = old_handle { 683 - let _ = state.cache.delete(&format!("handle:{}", old)).await; 758 + if !current_handle.is_empty() { 759 + let _ = state.cache.delete(&format!("handle:{}", current_handle)).await; 684 760 } 685 761 let _ = state.cache.delete(&format!("handle:{}", handle)).await; 686 762 if let Err(e) =
+28 -65
src/api/identity/plc/submit.rs
··· 23 23 headers: axum::http::HeaderMap, 24 24 Json(input): Json<SubmitPlcOperationInput>, 25 25 ) -> Response { 26 - info!("[MIGRATION] submitPlcOperation called"); 27 26 let bearer = match crate::auth::extract_bearer_token_from_header( 28 27 headers.get("Authorization").and_then(|h| h.to_str().ok()), 29 28 ) { 30 29 Some(t) => t, 31 30 None => { 32 - info!("[MIGRATION] submitPlcOperation: No bearer token"); 33 31 return ApiError::AuthenticationRequired.into_response(); 34 32 } 35 33 }; ··· 37 35 match crate::auth::validate_bearer_token_allow_deactivated(&state.db, &bearer).await { 38 36 Ok(user) => user, 39 37 Err(e) => { 40 - info!("[MIGRATION] submitPlcOperation: Auth failed: {:?}", e); 41 38 return ApiError::from(e).into_response(); 42 39 } 43 40 }; 44 - info!( 45 - "[MIGRATION] submitPlcOperation: Authenticated user did={}", 46 - auth_user.did 47 - ); 48 41 if let Err(e) = crate::auth::scope_check::check_identity_scope( 49 42 auth_user.is_oauth, 50 43 auth_user.scope.as_deref(), 51 44 crate::oauth::scopes::IdentityAttr::Wildcard, 52 45 ) { 53 - info!("[MIGRATION] submitPlcOperation: Scope check failed"); 54 46 return e; 55 47 } 56 48 let did = &auth_user.did; ··· 67 59 let hostname = std::env::var("PDS_HOSTNAME").unwrap_or_else(|_| "localhost".to_string()); 68 60 let public_url = format!("https://{}", hostname); 69 61 let user = match sqlx::query!( 70 - "SELECT id, handle, deactivated_at FROM users WHERE did = $1", 62 + "SELECT id, handle FROM users WHERE did = $1", 71 63 did 72 64 ) 73 65 .fetch_optional(&state.db) ··· 82 74 .into_response(); 83 75 } 84 76 }; 85 - let is_migration = user.deactivated_at.is_some(); 86 77 let key_row = match sqlx::query!( 87 78 "SELECT key_bytes, encryption_version FROM user_keys WHERE user_id = $1", 88 79 user.id ··· 123 114 } 124 115 }; 125 116 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()); 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()) { 130 120 let has_server_key = rotation_keys 131 121 .iter() 132 122 .any(|k| k.as_str() == Some(&server_rotation_key)); ··· 167 157 .into_response(); 168 158 } 169 159 } 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 - } 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() { 185 174 if let Some(also_known_as) = op.get("alsoKnownAs").and_then(|v| v.as_array()) { 186 175 let expected_handle = format!("at://{}", user.handle); 187 176 let first_aka = also_known_as.first().and_then(|v| v.as_str()); ··· 200 189 let plc_client = PlcClient::new(None); 201 190 let operation_clone = input.operation.clone(); 202 191 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 192 let result: Result<(), CircuitBreakerError<PlcError>> = 209 193 with_circuit_breaker(&state.circuit_breakers.plc_directory, || async { 210 194 plc_client ··· 213 197 }) 214 198 .await; 215 199 match result { 216 - Ok(()) => { 217 - info!( 218 - "[MIGRATION] submitPlcOperation: PLC directory accepted operation in {:?}", 219 - plc_start.elapsed() 220 - ); 221 - } 200 + Ok(()) => {} 222 201 Err(CircuitBreakerError::CircuitOpen(e)) => { 223 - warn!( 224 - "[MIGRATION] submitPlcOperation: PLC directory circuit breaker open: {}", 225 - e 226 - ); 202 + warn!("PLC directory circuit breaker open: {}", e); 227 203 return ( 228 204 StatusCode::SERVICE_UNAVAILABLE, 229 205 Json(json!({ ··· 234 210 .into_response(); 235 211 } 236 212 Err(CircuitBreakerError::OperationFailed(e)) => { 237 - error!( 238 - "[MIGRATION] submitPlcOperation: PLC operation failed: {:?}", 239 - e 240 - ); 213 + error!("PLC operation failed: {:?}", e); 241 214 return ( 242 215 StatusCode::BAD_GATEWAY, 243 216 Json(json!({ ··· 248 221 .into_response(); 249 222 } 250 223 } 251 - info!( 252 - "[MIGRATION] submitPlcOperation: Sequencing identity event for did={}", 253 - did 254 - ); 255 224 match sqlx::query!( 256 225 "INSERT INTO repo_seq (did, event_type) VALUES ($1, 'identity') RETURNING seq", 257 226 did ··· 260 229 .await 261 230 { 262 231 Ok(row) => { 263 - info!( 264 - "[MIGRATION] submitPlcOperation: Identity event sequenced with seq={}", 265 - row.seq 266 - ); 267 232 if let Err(e) = sqlx::query(&format!("NOTIFY repo_updates, '{}'", row.seq)) 268 233 .execute(&state.db) 269 234 .await 270 235 { 271 - warn!( 272 - "[MIGRATION] submitPlcOperation: Failed to notify identity event: {:?}", 273 - e 274 - ); 236 + warn!("Failed to notify identity event: {:?}", e); 275 237 } 276 238 } 277 239 Err(e) => { 278 - warn!( 279 - "[MIGRATION] submitPlcOperation: Failed to sequence identity event: {:?}", 280 - e 281 - ); 240 + warn!("Failed to sequence identity event: {:?}", e); 282 241 } 283 242 } 284 - info!("[MIGRATION] submitPlcOperation: SUCCESS for did={}", did); 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"); 285 248 (StatusCode::OK, Json(json!({}))).into_response() 286 249 }
+12 -12
src/api/validation.rs
··· 35 35 ), 36 36 Self::InvalidCharacters => write!( 37 37 f, 38 - "Handle contains invalid characters. Only alphanumeric, hyphens, and underscores are allowed" 38 + "Handle contains invalid characters. Only alphanumeric characters and hyphens are allowed" 39 39 ), 40 40 Self::StartsWithInvalidChar => { 41 - write!(f, "Handle cannot start with a hyphen or underscore") 41 + write!(f, "Handle cannot start with a hyphen") 42 42 } 43 - Self::EndsWithInvalidChar => write!(f, "Handle cannot end with a hyphen or underscore"), 43 + Self::EndsWithInvalidChar => write!(f, "Handle cannot end with a hyphen"), 44 44 Self::ContainsSpaces => write!(f, "Handle cannot contain spaces"), 45 45 Self::BannedWord => write!(f, "Inappropriate language in handle"), 46 46 } ··· 67 67 } 68 68 69 69 if let Some(first_char) = handle.chars().next() 70 - && (first_char == '-' || first_char == '_') 70 + && first_char == '-' 71 71 { 72 72 return Err(HandleValidationError::StartsWithInvalidChar); 73 73 } 74 74 75 75 if let Some(last_char) = handle.chars().last() 76 - && (last_char == '-' || last_char == '_') 76 + && last_char == '-' 77 77 { 78 78 return Err(HandleValidationError::EndsWithInvalidChar); 79 79 } 80 80 81 81 for c in handle.chars() { 82 - if !c.is_ascii_alphanumeric() && c != '-' && c != '_' { 82 + if !c.is_ascii_alphanumeric() && c != '-' { 83 83 return Err(HandleValidationError::InvalidCharacters); 84 84 } 85 85 } ··· 151 151 Ok("user-name".to_string()) 152 152 ); 153 153 assert_eq!( 154 - validate_short_handle("user_name"), 155 - Ok("user_name".to_string()) 156 - ); 157 - assert_eq!( 158 154 validate_short_handle("UPPERCASE"), 159 155 Ok("uppercase".to_string()) 160 156 ); ··· 194 190 ); 195 191 assert_eq!( 196 192 validate_short_handle("_starts"), 197 - Err(HandleValidationError::StartsWithInvalidChar) 193 + Err(HandleValidationError::InvalidCharacters) 198 194 ); 199 195 assert_eq!( 200 196 validate_short_handle("ends-"), ··· 202 198 ); 203 199 assert_eq!( 204 200 validate_short_handle("ends_"), 205 - Err(HandleValidationError::EndsWithInvalidChar) 201 + Err(HandleValidationError::InvalidCharacters) 202 + ); 203 + assert_eq!( 204 + validate_short_handle("user_name"), 205 + Err(HandleValidationError::InvalidCharacters) 206 206 ); 207 207 assert_eq!( 208 208 validate_short_handle("test@user"),
+8
src/appview/mod.rs
··· 110 110 Some(resolved) 111 111 } 112 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 + 113 121 async fn resolve_did_internal(&self, did: &str) -> Option<ResolvedService> { 114 122 let did_doc = if did.starts_with("did:web:") { 115 123 self.resolve_did_web(did).await
+4
src/handle/mod.rs
··· 93 93 } 94 94 95 95 pub fn is_service_domain_handle(handle: &str, hostname: &str) -> bool { 96 + if !handle.contains('.') { 97 + return true; 98 + } 96 99 let service_domains: Vec<String> = std::env::var("PDS_SERVICE_HANDLE_DOMAINS") 97 100 .map(|s| s.split(',').map(|d| d.trim().to_string()).collect()) 98 101 .unwrap_or_else(|_| vec![hostname.to_string()]); ··· 115 118 fn test_is_service_domain_handle() { 116 119 assert!(is_service_domain_handle("user.example.com", "example.com")); 117 120 assert!(is_service_domain_handle("example.com", "example.com")); 121 + assert!(is_service_domain_handle("myhandle", "example.com")); 118 122 assert!(!is_service_domain_handle("user.other.com", "example.com")); 119 123 assert!(!is_service_domain_handle("myhandle.xyz", "example.com")); 120 124 }
+12
src/rate_limit.rs
··· 30 30 pub app_password: Arc<KeyedRateLimiter>, 31 31 pub email_update: Arc<KeyedRateLimiter>, 32 32 pub totp_verify: Arc<KeyedRateLimiter>, 33 + pub handle_update: Arc<KeyedRateLimiter>, 34 + pub handle_update_daily: Arc<KeyedRateLimiter>, 33 35 } 34 36 35 37 impl Default for RateLimiters { ··· 78 80 Quota::with_period(std::time::Duration::from_secs(60)) 79 81 .unwrap() 80 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()), 81 93 )), 82 94 } 83 95 }
+8
src/state.rs
··· 37 37 AppPassword, 38 38 EmailUpdate, 39 39 TotpVerify, 40 + HandleUpdate, 41 + HandleUpdateDaily, 40 42 } 41 43 42 44 impl RateLimitKind { ··· 54 56 Self::AppPassword => "app_password", 55 57 Self::EmailUpdate => "email_update", 56 58 Self::TotpVerify => "totp_verify", 59 + Self::HandleUpdate => "handle_update", 60 + Self::HandleUpdateDaily => "handle_update_daily", 57 61 } 58 62 } 59 63 ··· 71 75 Self::AppPassword => (10, 60_000), 72 76 Self::EmailUpdate => (5, 3_600_000), 73 77 Self::TotpVerify => (5, 300_000), 78 + Self::HandleUpdate => (10, 300_000), 79 + Self::HandleUpdateDaily => (50, 86_400_000), 74 80 } 75 81 } 76 82 } ··· 191 197 RateLimitKind::AppPassword => &self.rate_limiters.app_password, 192 198 RateLimitKind::EmailUpdate => &self.rate_limiters.email_update, 193 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, 194 202 }; 195 203 196 204 let ok = limiter.check_key(&client_ip.to_string()).is_ok();
+1 -1
tests/common/mod.rs
··· 430 430 if attempt > 0 { 431 431 tokio::time::sleep(Duration::from_millis(100 * (attempt as u64 + 1))).await; 432 432 } 433 - let handle = format!("user_{}", uuid::Uuid::new_v4()); 433 + let handle = format!("user-{}", uuid::Uuid::new_v4()); 434 434 let payload = json!({ 435 435 "handle": handle, 436 436 "email": format!("{}@example.com", handle),
+7 -7
tests/did_web.rs
··· 11 11 #[tokio::test] 12 12 async fn test_create_self_hosted_did_web() { 13 13 let client = client(); 14 - let handle = format!("selfweb_{}", uuid::Uuid::new_v4()); 14 + let handle = format!("selfweb-{}", uuid::Uuid::new_v4()); 15 15 let payload = json!({ 16 16 "handle": handle, 17 17 "email": format!("{}@example.com", handle), ··· 98 98 let mock_uri = mock_server.uri(); 99 99 let mock_addr = mock_uri.trim_start_matches("http://"); 100 100 let did = format!("did:web:{}", mock_addr.replace(":", "%3A")); 101 - let handle = format!("extweb_{}", uuid::Uuid::new_v4()); 101 + let handle = format!("extweb-{}", uuid::Uuid::new_v4()); 102 102 let pds_endpoint = base_url().await.replace("http://", "https://"); 103 103 104 104 let reserve_res = client ··· 180 180 #[tokio::test] 181 181 async fn test_plc_operations_blocked_for_did_web() { 182 182 let client = client(); 183 - let handle = format!("plcblock_{}", uuid::Uuid::new_v4()); 183 + let handle = format!("plcblock-{}", uuid::Uuid::new_v4()); 184 184 let payload = json!({ 185 185 "handle": handle, 186 186 "email": format!("{}@example.com", handle), ··· 245 245 #[tokio::test] 246 246 async fn test_get_recommended_did_credentials_no_rotation_keys_for_did_web() { 247 247 let client = client(); 248 - let handle = format!("creds_{}", uuid::Uuid::new_v4()); 248 + let handle = format!("creds-{}", uuid::Uuid::new_v4()); 249 249 let payload = json!({ 250 250 "handle": handle, 251 251 "email": format!("{}@example.com", handle), ··· 294 294 #[tokio::test] 295 295 async fn test_did_plc_still_works_with_did_type_param() { 296 296 let client = client(); 297 - let handle = format!("plctype_{}", uuid::Uuid::new_v4()); 297 + let handle = format!("plctype-{}", uuid::Uuid::new_v4()); 298 298 let payload = json!({ 299 299 "handle": handle, 300 300 "email": format!("{}@example.com", handle), ··· 323 323 #[tokio::test] 324 324 async fn test_external_did_web_requires_did_field() { 325 325 let client = client(); 326 - let handle = format!("nodid_{}", uuid::Uuid::new_v4()); 326 + let handle = format!("nodid-{}", uuid::Uuid::new_v4()); 327 327 let payload = json!({ 328 328 "handle": handle, 329 329 "email": format!("{}@example.com", handle), ··· 392 392 mock_addr.replace(":", "%3A"), 393 393 unique_id 394 394 ); 395 - let handle = format!("byod_{}", uuid::Uuid::new_v4()); 395 + let handle = format!("byod-{}", uuid::Uuid::new_v4()); 396 396 let pds_endpoint = base_url().await.replace("http://", "https://"); 397 397 let pds_did = format!("did:web:{}", pds_endpoint.trim_start_matches("https://")); 398 398
+13 -13
tests/email_update.rs
··· 67 67 let client = common::client(); 68 68 let base_url = common::base_url().await; 69 69 let pool = get_pool().await; 70 - let handle = format!("emailup_{}", uuid::Uuid::new_v4()); 70 + let handle = format!("emailup-{}", uuid::Uuid::new_v4()); 71 71 let email = format!("{}@example.com", handle); 72 72 let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await; 73 73 let new_email = format!("new_{}@example.com", handle); ··· 108 108 async fn test_request_email_update_taken_email() { 109 109 let client = common::client(); 110 110 let base_url = common::base_url().await; 111 - let handle1 = format!("emailup_taken1_{}", uuid::Uuid::new_v4()); 111 + let handle1 = format!("emailup-taken1-{}", uuid::Uuid::new_v4()); 112 112 let email1 = format!("{}@example.com", handle1); 113 113 let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await; 114 - let handle2 = format!("emailup_taken2_{}", uuid::Uuid::new_v4()); 114 + let handle2 = format!("emailup-taken2-{}", uuid::Uuid::new_v4()); 115 115 let email2 = format!("{}@example.com", handle2); 116 116 let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await; 117 117 let res = client ··· 133 133 async fn test_confirm_email_invalid_token() { 134 134 let client = common::client(); 135 135 let base_url = common::base_url().await; 136 - let handle = format!("emailup_inv_{}", uuid::Uuid::new_v4()); 136 + let handle = format!("emailup-inv-{}", uuid::Uuid::new_v4()); 137 137 let email = format!("{}@example.com", handle); 138 138 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 139 139 let new_email = format!("new_{}@example.com", handle); ··· 168 168 let client = common::client(); 169 169 let base_url = common::base_url().await; 170 170 let pool = get_pool().await; 171 - let handle = format!("emailup_wrong_{}", uuid::Uuid::new_v4()); 171 + let handle = format!("emailup-wrong-{}", uuid::Uuid::new_v4()); 172 172 let email = format!("{}@example.com", handle); 173 173 let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await; 174 174 let new_email = format!("new_{}@example.com", handle); ··· 205 205 async fn test_update_email_requires_token() { 206 206 let client = common::client(); 207 207 let base_url = common::base_url().await; 208 - let handle = format!("emailup_direct_{}", uuid::Uuid::new_v4()); 208 + let handle = format!("emailup-direct-{}", uuid::Uuid::new_v4()); 209 209 let email = format!("{}@example.com", handle); 210 210 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 211 211 let new_email = format!("direct_{}@example.com", handle); ··· 225 225 async fn test_update_email_same_email_noop() { 226 226 let client = common::client(); 227 227 let base_url = common::base_url().await; 228 - let handle = format!("emailup_same_{}", uuid::Uuid::new_v4()); 228 + let handle = format!("emailup-same-{}", uuid::Uuid::new_v4()); 229 229 let email = format!("{}@example.com", handle); 230 230 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 231 231 let res = client ··· 246 246 async fn test_update_email_requires_token_after_pending() { 247 247 let client = common::client(); 248 248 let base_url = common::base_url().await; 249 - let handle = format!("emailup_token_{}", uuid::Uuid::new_v4()); 249 + let handle = format!("emailup-token-{}", uuid::Uuid::new_v4()); 250 250 let email = format!("{}@example.com", handle); 251 251 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 252 252 let new_email = format!("pending_{}@example.com", handle); ··· 278 278 let client = common::client(); 279 279 let base_url = common::base_url().await; 280 280 let pool = get_pool().await; 281 - let handle = format!("emailup_valid_{}", uuid::Uuid::new_v4()); 281 + let handle = format!("emailup-valid-{}", uuid::Uuid::new_v4()); 282 282 let email = format!("{}@example.com", handle); 283 283 let (access_jwt, did) = create_verified_account(&client, &base_url, &handle, &email).await; 284 284 let new_email = format!("valid_{}@example.com", handle); ··· 316 316 async fn test_update_email_invalid_token() { 317 317 let client = common::client(); 318 318 let base_url = common::base_url().await; 319 - let handle = format!("emailup_badtok_{}", uuid::Uuid::new_v4()); 319 + let handle = format!("emailup-badtok-{}", uuid::Uuid::new_v4()); 320 320 let email = format!("{}@example.com", handle); 321 321 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 322 322 let new_email = format!("badtok_{}@example.com", handle); ··· 350 350 async fn test_update_email_already_taken() { 351 351 let client = common::client(); 352 352 let base_url = common::base_url().await; 353 - let handle1 = format!("emailup_dup1_{}", uuid::Uuid::new_v4()); 353 + let handle1 = format!("emailup-dup1-{}", uuid::Uuid::new_v4()); 354 354 let email1 = format!("{}@example.com", handle1); 355 355 let (_, _) = create_verified_account(&client, &base_url, &handle1, &email1).await; 356 - let handle2 = format!("emailup_dup2_{}", uuid::Uuid::new_v4()); 356 + let handle2 = format!("emailup-dup2-{}", uuid::Uuid::new_v4()); 357 357 let email2 = format!("{}@example.com", handle2); 358 358 let (access_jwt2, _) = create_verified_account(&client, &base_url, &handle2, &email2).await; 359 359 let res = client ··· 394 394 async fn test_update_email_invalid_format() { 395 395 let client = common::client(); 396 396 let base_url = common::base_url().await; 397 - let handle = format!("emailup_fmt_{}", uuid::Uuid::new_v4()); 397 + let handle = format!("emailup-fmt-{}", uuid::Uuid::new_v4()); 398 398 let email = format!("{}@example.com", handle); 399 399 let (access_jwt, _) = create_verified_account(&client, &base_url, &handle, &email).await; 400 400 let res = client
+160 -4
tests/identity.rs
··· 8 8 #[tokio::test] 9 9 async fn test_resolve_handle_success() { 10 10 let client = client(); 11 - let short_handle = format!("resolvetest_{}", uuid::Uuid::new_v4()); 11 + let short_handle = format!("resolvetest-{}", uuid::Uuid::new_v4()); 12 12 let payload = json!({ 13 13 "handle": short_handle, 14 14 "email": format!("{}@example.com", short_handle), ··· 98 98 let mock_uri = mock_server.uri(); 99 99 let mock_addr = mock_uri.trim_start_matches("http://"); 100 100 let did = format!("did:web:{}", mock_addr.replace(":", "%3A")); 101 - let handle = format!("webuser_{}", uuid::Uuid::new_v4()); 101 + let handle = format!("webuser-{}", uuid::Uuid::new_v4()); 102 102 let pds_endpoint = base_url().await.replace("http://", "https://"); 103 103 104 104 let reserve_res = client ··· 183 183 #[tokio::test] 184 184 async fn test_create_account_duplicate_handle() { 185 185 let client = client(); 186 - let handle = format!("dupe_{}", uuid::Uuid::new_v4()); 186 + let handle = format!("dupe-{}", uuid::Uuid::new_v4()); 187 187 let email = format!("{}@example.com", handle); 188 188 let payload = json!({ 189 189 "handle": handle, ··· 220 220 let mock_server = MockServer::start().await; 221 221 let mock_uri = mock_server.uri(); 222 222 let mock_addr = mock_uri.trim_start_matches("http://"); 223 - let handle = format!("lifecycle_{}", uuid::Uuid::new_v4()); 223 + let handle = format!("lifecycle-{}", uuid::Uuid::new_v4()); 224 224 let did = format!("did:web:{}:u:{}", mock_addr.replace(":", "%3A"), handle); 225 225 let email = format!("{}@test.com", handle); 226 226 let pds_endpoint = base_url().await.replace("http://", "https://"); ··· 378 378 let body: Value = res.json().await.expect("Response was not valid JSON"); 379 379 assert_eq!(body["error"], "AuthenticationRequired"); 380 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 19 let client = common::client(); 20 20 let base_url = common::base_url().await; 21 21 let pool = get_pool().await; 22 - let handle = format!("pwreset_{}", uuid::Uuid::new_v4()); 22 + let handle = format!("pwreset-{}", uuid::Uuid::new_v4()); 23 23 let email = format!("{}@example.com", handle); 24 24 let payload = json!({ 25 25 "handle": handle, ··· 81 81 let client = common::client(); 82 82 let base_url = common::base_url().await; 83 83 let pool = get_pool().await; 84 - let handle = format!("pwreset2_{}", uuid::Uuid::new_v4()); 84 + let handle = format!("pwreset2-{}", uuid::Uuid::new_v4()); 85 85 let email = format!("{}@example.com", handle); 86 86 let old_password = "Oldpass123!"; 87 87 let new_password = "Newpass456!"; ··· 197 197 let client = common::client(); 198 198 let base_url = common::base_url().await; 199 199 let pool = get_pool().await; 200 - let handle = format!("pwreset3_{}", uuid::Uuid::new_v4()); 200 + let handle = format!("pwreset3-{}", uuid::Uuid::new_v4()); 201 201 let email = format!("{}@example.com", handle); 202 202 let payload = json!({ 203 203 "handle": handle, ··· 261 261 let client = common::client(); 262 262 let base_url = common::base_url().await; 263 263 let pool = get_pool().await; 264 - let handle = format!("pwreset4_{}", uuid::Uuid::new_v4()); 264 + let handle = format!("pwreset4-{}", uuid::Uuid::new_v4()); 265 265 let email = format!("{}@example.com", handle); 266 266 let payload = json!({ 267 267 "handle": handle, ··· 351 351 let pool = get_pool().await; 352 352 let client = common::client(); 353 353 let base_url = common::base_url().await; 354 - let handle = format!("pwreset5_{}", uuid::Uuid::new_v4()); 354 + let handle = format!("pwreset5-{}", uuid::Uuid::new_v4()); 355 355 let email = format!("{}@example.com", handle); 356 356 let payload = json!({ 357 357 "handle": handle,
+4 -4
tests/rate_limit.rs
··· 9 9 let client = client(); 10 10 let url = format!("{}/xrpc/com.atproto.server.createSession", base_url().await); 11 11 let payload = json!({ 12 - "identifier": "nonexistent_user_for_rate_limit_test", 12 + "identifier": "nonexistent-user-for-rate-limit-test", 13 13 "password": "wrongpassword" 14 14 }); 15 15 let mut rate_limited_count = 0; ··· 53 53 let mut success_count = 0; 54 54 for i in 0..8 { 55 55 let payload = json!({ 56 - "email": format!("ratelimit_test_{}@example.com", i) 56 + "email": format!("ratelimit-test_{}@example.com", i) 57 57 }); 58 58 let res = client 59 59 .post(&url) ··· 91 91 for i in 0..15 { 92 92 let unique_id = uuid::Uuid::new_v4(); 93 93 let payload = json!({ 94 - "handle": format!("ratelimit_{}_{}", i, unique_id), 95 - "email": format!("ratelimit_{}_{}@example.com", i, unique_id), 94 + "handle": format!("ratelimit-{}_{}", i, unique_id), 95 + "email": format!("ratelimit-{}_{}@example.com", i, unique_id), 96 96 "password": "Testpass123!" 97 97 }); 98 98 let res = client
+1 -1
tests/server.rs
··· 26 26 async fn test_account_and_session_lifecycle() { 27 27 let client = client(); 28 28 let base = base_url().await; 29 - let handle = format!("user_{}", uuid::Uuid::new_v4()); 29 + let handle = format!("user-{}", uuid::Uuid::new_v4()); 30 30 let payload = json!({ "handle": handle, "email": format!("{}@example.com", handle), "password": "Testpass123!" }); 31 31 let create_res = client 32 32 .post(format!("{}/xrpc/com.atproto.server.createAccount", base))
+5 -5
tests/signing_key.rs
··· 174 174 assert_eq!(res.status(), StatusCode::OK); 175 175 let body: Value = res.json().await.unwrap(); 176 176 let signing_key = body["signingKey"].as_str().unwrap(); 177 - let handle = format!("reserved_key_user_{}", uuid::Uuid::new_v4()); 177 + let handle = format!("reserved-key-user-{}", uuid::Uuid::new_v4()); 178 178 let res = client 179 179 .post(format!( 180 180 "{}/xrpc/com.atproto.server.createAccount", ··· 212 212 async fn test_create_account_with_invalid_signing_key() { 213 213 let client = common::client(); 214 214 let base_url = common::base_url().await; 215 - let handle = format!("bad_key_user_{}", uuid::Uuid::new_v4()); 215 + let handle = format!("bad-key-user-{}", uuid::Uuid::new_v4()); 216 216 let res = client 217 217 .post(format!( 218 218 "{}/xrpc/com.atproto.server.createAccount", ··· 248 248 assert_eq!(res.status(), StatusCode::OK); 249 249 let body: Value = res.json().await.unwrap(); 250 250 let signing_key = body["signingKey"].as_str().unwrap(); 251 - let handle1 = format!("reuse_key_user1_{}", uuid::Uuid::new_v4()); 251 + let handle1 = format!("reuse-key-user1-{}", uuid::Uuid::new_v4()); 252 252 let res = client 253 253 .post(format!( 254 254 "{}/xrpc/com.atproto.server.createAccount", ··· 264 264 .await 265 265 .expect("Failed to create first account"); 266 266 assert_eq!(res.status(), StatusCode::OK); 267 - let handle2 = format!("reuse_key_user2_{}", uuid::Uuid::new_v4()); 267 + let handle2 = format!("reuse-key-user2-{}", uuid::Uuid::new_v4()); 268 268 let res = client 269 269 .post(format!( 270 270 "{}/xrpc/com.atproto.server.createAccount", ··· 301 301 assert_eq!(res.status(), StatusCode::OK); 302 302 let body: Value = res.json().await.unwrap(); 303 303 let signing_key = body["signingKey"].as_str().unwrap(); 304 - let handle = format!("token_test_user_{}", uuid::Uuid::new_v4()); 304 + let handle = format!("token-test-user-{}", uuid::Uuid::new_v4()); 305 305 let res = client 306 306 .post(format!( 307 307 "{}/xrpc/com.atproto.server.createAccount",