#[cfg(feature = "cache")]
mod cache_advanced {
use sdforge::cache::{DashMapCache, SyncCache};
#[test]
fn test_cache_invalidate_star_matches_all_keys() {
let cache = DashMapCache::new();
cache.set("user:1", b"v1".to_vec());
cache.set("user:2", b"v2".to_vec());
cache.set("session:abc", b"v3".to_vec());
let deleted = cache.invalidate("*");
assert_eq!(deleted, 3);
assert_eq!(cache.len(), 0);
}
#[test]
fn test_cache_invalidate_prefix_pattern() {
let cache = DashMapCache::new();
cache.set("user:1", b"v1".to_vec());
cache.set("user:2", b"v2".to_vec());
cache.set("session:abc", b"v3".to_vec());
let deleted = cache.invalidate("user:*");
assert_eq!(deleted, 2);
assert!(cache.contains("session:abc"));
assert!(!cache.contains("user:1"));
}
#[test]
fn test_cache_invalidate_question_matches_single_char() {
let cache = DashMapCache::new();
cache.set("a", b"1".to_vec());
cache.set("ab", b"2".to_vec());
cache.set("ac", b"3".to_vec());
cache.set("abcd", b"4".to_vec());
let deleted = cache.invalidate("a?");
assert_eq!(deleted, 2);
assert!(cache.contains("a"));
assert!(cache.contains("abcd"));
}
#[test]
fn test_cache_invalidate_empty_pattern_matches_nothing() {
let cache = DashMapCache::new();
cache.set("key", b"v".to_vec());
let deleted = cache.invalidate("");
assert_eq!(deleted, 0);
assert!(cache.contains("key"));
}
#[test]
fn test_cache_find_keys_by_pattern_returns_matching() {
let cache = DashMapCache::new();
cache.set("user:1", b"v1".to_vec());
cache.set("user:2", b"v2".to_vec());
cache.set("admin:1", b"v3".to_vec());
let keys = cache.find_keys_by_pattern("user:*");
assert_eq!(keys.len(), 2);
assert!(keys.contains(&"user:1".to_string()));
assert!(keys.contains(&"user:2".to_string()));
}
#[test]
fn test_cache_find_keys_by_pattern_star_only_matches_all() {
let cache = DashMapCache::new();
cache.set("k1", b"v1".to_vec());
cache.set("k2", b"v2".to_vec());
let keys = cache.find_keys_by_pattern("*");
assert_eq!(keys.len(), 2);
}
#[test]
fn test_cache_get_many_returns_only_found_keys() {
let cache = DashMapCache::new();
cache.set("present", b"v1".to_vec());
let result = cache.get_many(&["present", "absent"]);
assert_eq!(result.len(), 1);
assert!(result.contains_key("present"));
assert!(!result.contains_key("absent"));
}
#[test]
fn test_cache_set_many_bulk_insert() {
let cache = DashMapCache::new();
let items = vec![
("k1".to_string(), b"v1".to_vec()),
("k2".to_string(), b"v2".to_vec()),
("k3".to_string(), b"v3".to_vec()),
];
cache.set_many(&items);
assert_eq!(cache.len(), 3);
assert!(cache.contains("k1"));
assert!(cache.contains("k2"));
assert!(cache.contains("k3"));
}
#[test]
fn test_cache_delete_many_returns_count() {
let cache = DashMapCache::new();
cache.set("k1", b"v1".to_vec());
cache.set("k2", b"v2".to_vec());
cache.set("k3", b"v3".to_vec());
let deleted = cache.delete_many(&["k1", "k2", "nonexistent"]);
assert_eq!(deleted, 2);
assert!(!cache.contains("k1"));
assert!(!cache.contains("k2"));
assert!(cache.contains("k3"));
}
#[test]
fn test_cache_stats_default_reports_keys() {
let cache = DashMapCache::new();
let stats = cache.get_stats();
assert!(stats.contains_key("total_keys"));
assert_eq!(stats.get("total_keys"), Some(&0));
}
#[test]
fn test_cache_poisoned_set_falls_back_to_backend() {
let cache = DashMapCache::new();
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let _guard = cache.find_keys_by_pattern("*");
unreachable!("this branch is never reached");
}));
let _ = result;
cache.set("post_poison", b"val".to_vec());
assert!(cache.contains("post_poison"));
assert_eq!(cache.get("post_poison"), Some(b"val".to_vec()));
}
#[test]
fn test_cache_clear_empties_all_keys() {
let cache = DashMapCache::new();
cache.set("k1", b"v1".to_vec());
cache.set("k2", b"v2".to_vec());
assert_eq!(cache.len(), 2);
cache.clear();
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
}
#[test]
fn test_cache_with_capacity_construction() {
let cache = DashMapCache::with_capacity(100);
assert_eq!(cache.len(), 0);
assert!(cache.is_empty());
}
}
#[cfg(feature = "http")]
mod config_advanced {
use sdforge::config::{
AppConfig, AuthConfig, ConfigError, CorsConfig, ServerConfig, TimeoutConfig, ValidateConfig,
};
#[test]
fn test_config_port_zero_rejected() {
let config = ServerConfig {
host: "localhost".to_string(),
port: 0,
request_timeout_secs: 30,
cors: None,
};
let result = config.validate();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("port cannot be 0"));
}
#[test]
fn test_config_timeout_zero_rejected() {
let config = ServerConfig {
host: "localhost".to_string(),
port: 8080,
request_timeout_secs: 0,
cors: None,
};
let result = config.validate();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("timeout_secs cannot be 0"));
}
#[test]
fn test_config_timeout_above_86400_rejected() {
let config = ServerConfig {
host: "localhost".to_string(),
port: 8080,
request_timeout_secs: 100_000,
cors: None,
};
let result = config.validate();
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.to_string().contains("86400"));
}
#[test]
fn test_config_timeout_boundary_86400_accepted() {
let config = ServerConfig {
host: "localhost".to_string(),
port: 8080,
request_timeout_secs: 86_400,
cors: None,
};
assert!(config.validate().is_ok());
}
#[test]
fn test_config_jwt_empty_secret_rejected() {
let config = AuthConfig::Jwt {
secret: "".to_string(),
};
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty"));
}
#[test]
fn test_config_jwt_short_secret_rejected() {
let config = AuthConfig::Jwt {
secret: "short".to_string(),
};
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("short"));
}
#[test]
fn test_config_jwt_weak_secret_rejected() {
let weak = "secretsecretsecretsecretsecretse".to_string(); assert!(weak.len() >= 32);
let config = AuthConfig::Jwt {
secret: weak.clone(),
};
assert!(config.validate().is_ok());
}
#[test]
fn test_config_jwt_exact_weak_word_rejected() {
let weak = "PASSWORD".to_string();
let config = AuthConfig::Jwt { secret: weak };
let result = config.validate();
assert!(result.is_err());
}
#[test]
fn test_config_apikey_empty_prefix_rejected() {
let config = AuthConfig::ApiKey {
header_name: "X-API-Key".to_string(),
prefix: "".to_string(),
};
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty"));
}
#[test]
fn test_config_apikey_valid_prefix_accepted() {
let config = AuthConfig::ApiKey {
header_name: "X-API-Key".to_string(),
prefix: "sk-".to_string(),
};
assert!(config.validate().is_ok());
}
#[test]
fn test_config_none_variant_accepted() {
let config = AuthConfig::None;
assert!(config.validate().is_ok());
}
#[test]
fn test_config_validate_trait_for_server_config() {
let config = ServerConfig::default();
assert!(ValidateConfig::validate(&config).is_ok());
}
#[test]
fn test_config_validate_trait_for_auth_config() {
let config = AuthConfig::None;
assert!(ValidateConfig::validate(&config).is_ok());
}
#[test]
fn test_config_validate_trait_propagates_error() {
let config = ServerConfig {
host: "localhost".to_string(),
port: 0,
request_timeout_secs: 30,
cors: None,
};
assert!(ValidateConfig::validate(&config).is_err());
}
#[test]
fn test_config_cors_empty_origins_rejected() {
let config = ServerConfig {
host: "localhost".to_string(),
port: 8080,
request_timeout_secs: 30,
cors: Some(CorsConfig {
allowed_origins: vec![],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
}),
};
let result = config.validate();
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("empty"));
}
#[test]
fn test_config_cors_invalid_origin_rejected() {
let config = ServerConfig {
host: "localhost".to_string(),
port: 8080,
request_timeout_secs: 30,
cors: Some(CorsConfig {
allowed_origins: vec!["localhost:3000".to_string()],
allowed_methods: vec!["GET".to_string()],
allowed_headers: vec![],
}),
};
let result = config.validate();
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("Invalid CORS origin")
);
}
#[test]
fn test_config_app_config_validate_invalid_server() {
let config = AppConfig {
server: ServerConfig {
host: "localhost".to_string(),
port: 0,
request_timeout_secs: 30,
cors: None,
},
authentication: AuthConfig::None,
timeout: None,
};
assert!(config.validate().is_err());
}
#[test]
fn test_config_app_config_builder_valid() {
let config = AppConfig::builder()
.server(ServerConfig {
host: "0.0.0.0".to_string(),
port: 8080,
request_timeout_secs: 30,
cors: None,
})
.authentication(AuthConfig::None)
.timeout(TimeoutConfig::default())
.build();
assert!(config.is_ok());
}
#[test]
fn test_config_error_variants_display() {
assert!(
ConfigError::FileNotFound {
path: "/x".to_string()
}
.to_string()
.contains("File not found")
);
assert!(
ConfigError::ParseError {
message: "bad".to_string()
}
.to_string()
.contains("Parse error")
);
assert!(
ConfigError::IoError {
reason: "perm".to_string()
}
.to_string()
.contains("IO error")
);
assert!(
ConfigError::ValidationError("v".to_string())
.to_string()
.contains("Validation error")
);
assert!(
ConfigError::LoadError("l".to_string())
.to_string()
.contains("load error")
);
assert!(
ConfigError::WatchError("w".to_string())
.to_string()
.contains("Watch error")
);
assert!(
ConfigError::Unknown("u".to_string())
.to_string()
.contains("Unknown")
);
}
}
mod error_advanced {
use sdforge::core::ServiceError;
use sdforge::error::{ApiError, ErrorCategory, ErrorContext, SdForgeError};
#[derive(Debug, thiserror::Error)]
#[error("downstream failure: {0}")]
struct DownstreamError(String);
#[test]
fn test_error_from_std_error_generates_id_and_sanitizes() {
let source = DownstreamError("sensitive db connection string".to_string());
let err = ApiError::from_std_error(source);
match err {
ApiError::Internal {
message,
error_id,
source,
context,
} => {
assert!(!message.contains("sensitive"));
assert!(message.contains("internal error"));
assert_eq!(error_id.len(), 16);
assert!(source.is_some());
assert!(context.is_none());
}
_ => panic!("Expected Internal variant"),
}
}
#[test]
fn test_error_from_std_error_preserves_source_chain() {
let err = ApiError::from_std_error(DownstreamError("chained failure".to_string()));
let src = err.source().expect("source should be present");
assert!(src.to_string().contains("chained failure"));
}
#[test]
fn test_error_internal_with_source_chain_accessible() {
let err = ApiError::internal_with_source(
"sanitized",
"err-456",
DownstreamError("db down".to_string()),
);
let src = err.source().expect("source should be present");
assert!(src.to_string().contains("db down"));
}
#[test]
fn test_error_internal_with_context_stores_context() {
let ctx = ErrorContext::new()
.with_extra("request_id".to_string(), "req-789".to_string())
.with_extra("user_id".to_string(), "u-42".to_string());
let err = ApiError::internal_with_context("msg", "err-ctx", ctx);
match err {
ApiError::Internal { context, .. } => {
let ctx = context.expect("context should be present");
assert_eq!(ctx.extra.get("request_id"), Some(&"req-789".to_string()));
assert_eq!(ctx.extra.get("user_id"), Some(&"u-42".to_string()));
}
_ => panic!("Expected Internal variant"),
}
}
#[test]
fn test_error_internal_with_source_and_context() {
let ctx = ErrorContext::current();
let err = ApiError::internal_with_source_and_context(
"sanitized",
"err-000",
DownstreamError("combined".to_string()),
ctx,
);
assert!(err.source().is_some());
match err {
ApiError::Internal { context, .. } => {
assert!(context.is_some());
}
_ => panic!("Expected Internal variant"),
}
}
#[test]
fn test_error_sanitized_message_internal_strips_details() {
let err = ApiError::internal_error("secret stack trace with paths /etc/passwd", "id");
let msg = err.sanitized_message();
assert!(!msg.contains("secret"));
assert!(!msg.contains("/etc/passwd"));
assert!(msg.contains("internal error"));
}
#[test]
fn test_error_sanitized_message_service_unavailable_strips_service() {
let err = ApiError::service_unavailable("internal-db-host:5432", Some(30));
let msg = err.sanitized_message();
assert!(!msg.contains("internal-db-host:5432"));
assert!(msg.contains("unavailable") || msg.contains("try again"));
}
#[test]
fn test_error_sanitized_message_client_errors_preserved() {
assert!(
ApiError::not_found("User", None)
.sanitized_message()
.contains("User")
);
assert!(
ApiError::invalid_input("bad email", None, None)
.sanitized_message()
.contains("bad email")
);
assert!(
ApiError::authentication_failed("expired token")
.sanitized_message()
.contains("expired token")
);
assert!(
ApiError::access_denied("read", None)
.sanitized_message()
.contains("read")
);
assert!(
ApiError::validation("email", "bad format")
.sanitized_message()
.contains("email")
);
}
#[test]
fn test_error_to_service_error_status_codes() {
let cases: Vec<(&str, u16, ApiError)> = vec![
("NOT_FOUND", 404, ApiError::not_found("X", None)),
(
"INVALID_INPUT",
400,
ApiError::invalid_input("m", None, None),
),
(
"AUTHENTICATION_FAILED",
401,
ApiError::authentication_failed("r"),
),
("ACCESS_DENIED", 403, ApiError::access_denied("p", None)),
(
"RATE_LIMIT_EXCEEDED",
429,
ApiError::rate_limit_exceeded(1, 1),
),
("INTERNAL_ERROR", 500, ApiError::internal_error("m", "id")),
(
"SERVICE_UNAVAILABLE",
503,
ApiError::service_unavailable("svc", None),
),
("VALIDATION_ERROR", 422, ApiError::validation("f", "c")),
];
for (code, status, err) in cases {
let svc = err.to_service_error();
assert_eq!(svc.code(), code);
assert_eq!(svc.http_status(), status);
}
}
#[test]
fn test_error_to_service_error_internal_includes_error_id() {
let err = ApiError::internal_error("msg", "err-abc-123");
let svc = err.to_service_error();
let details = svc.details().expect("details should be present");
assert_eq!(details["error_id"], "err-abc-123");
}
#[test]
fn test_error_to_service_error_service_unavailable_includes_retry() {
let err = ApiError::service_unavailable("redis", Some(30));
let svc = err.to_service_error();
let details = svc.details().expect("details should be present");
assert_eq!(details["service"], "redis");
assert_eq!(details["retry_after"], 30);
}
#[test]
fn test_error_from_api_error_for_service_error_delegates() {
let api_err = ApiError::not_found("User", None);
let svc: ServiceError = api_err.into();
assert_eq!(svc.code(), "NOT_FOUND");
assert_eq!(svc.http_status(), 404);
}
#[test]
fn test_error_source_none_for_client_errors() {
assert!(ApiError::not_found("X", None).source().is_none());
assert!(ApiError::invalid_input("m", None, None).source().is_none());
assert!(ApiError::authentication_failed("r").source().is_none());
assert!(ApiError::access_denied("p", None).source().is_none());
assert!(ApiError::rate_limit_exceeded(1, 1).source().is_none());
assert!(ApiError::validation("f", "c").source().is_none());
}
#[test]
fn test_error_source_none_when_no_source_attached() {
assert!(ApiError::internal_error("m", "id").source().is_none());
assert!(
ApiError::service_unavailable("svc", None)
.source()
.is_none()
);
}
#[test]
fn test_error_category_all_variants() {
assert_eq!(
ApiError::not_found("X", None).category(),
ErrorCategory::ClientError
);
assert_eq!(
ApiError::invalid_input("m", None, None).category(),
ErrorCategory::ClientError
);
assert_eq!(
ApiError::authentication_failed("r").category(),
ErrorCategory::AuthError
);
assert_eq!(
ApiError::access_denied("p", None).category(),
ErrorCategory::AuthError
);
assert_eq!(
ApiError::rate_limit_exceeded(1, 1).category(),
ErrorCategory::RateLimitError
);
assert_eq!(
ApiError::internal_error("m", "id").category(),
ErrorCategory::ServerError
);
assert_eq!(
ApiError::service_unavailable("svc", None).category(),
ErrorCategory::ServerError
);
assert_eq!(
ApiError::validation("f", "c").category(),
ErrorCategory::ValidationError
);
}
#[test]
fn test_error_to_mcp_json_all_variants_valid_json() {
let errors: Vec<(&str, ApiError)> = vec![
("NOT_FOUND", ApiError::not_found("User", None)),
("INVALID_INPUT", ApiError::invalid_input("msg", None, None)),
(
"AUTHENTICATION_FAILED",
ApiError::authentication_failed("r"),
),
("ACCESS_DENIED", ApiError::access_denied("p", None)),
("RATE_LIMIT_EXCEEDED", ApiError::rate_limit_exceeded(1, 1)),
("INTERNAL_ERROR", ApiError::internal_error("m", "id")),
(
"SERVICE_UNAVAILABLE",
ApiError::service_unavailable("svc", None),
),
("VALIDATION_ERROR", ApiError::validation("f", "c")),
];
for (code, err) in errors {
let json = err.to_mcp_json();
let parsed: serde_json::Value = serde_json::from_str(&json).unwrap_or_else(|e| {
panic!("to_mcp_json for {} should produce valid JSON: {}", code, e)
});
assert_eq!(parsed["success"], false);
assert_eq!(parsed["error"]["code"], code);
assert!(parsed["error"]["message"].is_string());
}
}
#[test]
fn test_error_serde_roundtrip_not_found() {
let err = ApiError::not_found("Document", Some("42".to_string()));
let json = serde_json::to_string(&err).expect("serialization should succeed");
let restored: ApiError =
serde_json::from_str(&json).expect("deserialization should succeed");
match restored {
ApiError::NotFound {
resource,
resource_id,
} => {
assert_eq!(resource, "Document");
assert_eq!(resource_id, Some("42".to_string()));
}
_ => panic!("Expected NotFound variant after round-trip"),
}
}
#[test]
fn test_error_serde_includes_type_tag() {
let err = ApiError::validation("email", "invalid");
let json = serde_json::to_string(&err).expect("serialization should succeed");
assert!(
json.contains(r#""type":"ValidationError""#),
"json should include type tag: {}",
json
);
}
#[test]
fn test_error_serde_skips_source_field() {
let err =
ApiError::internal_with_source("msg", "id", DownstreamError("secret".to_string()));
let json = serde_json::to_string(&err).expect("serialization should succeed");
assert!(
!json.contains("secret"),
"source should be skipped in serialization"
);
}
#[test]
fn test_error_sdforge_error_from_api_error() {
let api_err = ApiError::not_found("User", None);
let sd_err: SdForgeError = api_err.into();
match sd_err {
SdForgeError::Api(ApiError::NotFound { resource, .. }) => {
assert_eq!(resource, "User");
}
_ => panic!("Expected Api(NotFound) variant"),
}
}
#[test]
fn test_error_sdforge_error_internal() {
let err = SdForgeError::internal("internal failure");
match err {
SdForgeError::Internal(msg) => assert_eq!(msg, "internal failure"),
_ => panic!("Expected Internal variant"),
}
}
#[test]
fn test_error_sdforge_error_category_for_api() {
let err: SdForgeError = ApiError::not_found("X", None).into();
assert_eq!(err.category(), ErrorCategory::ClientError);
}
#[test]
fn test_error_sdforge_error_sanitized_message_for_api() {
let err: SdForgeError = ApiError::internal_error("secret", "id").into();
let msg = err.sanitized_message();
assert!(!msg.contains("secret"));
}
}
#[cfg(feature = "ratelimit")]
mod ratelimit_error_advanced {
use sdforge::error::ApiError;
use sdforge::security::RateLimitError;
#[test]
fn test_ratelimit_exceeded_maps_to_rate_limit_exceeded() {
let err = ApiError::from(RateLimitError::Exceeded {
limit: 100,
window_seconds: 60,
});
match err {
ApiError::RateLimitExceeded {
limit,
window_seconds,
} => {
assert_eq!(limit, 100);
assert_eq!(window_seconds, 60);
}
_ => panic!("Expected RateLimitExceeded, got {err:?}"),
}
}
#[test]
fn test_ratelimit_banned_maps_to_access_denied() {
let err = ApiError::from(RateLimitError::Banned {
reason: "abuse detected".to_string(),
});
match err {
ApiError::AccessDenied {
permission,
user_id,
} => {
assert_eq!(permission, "rate_limit");
assert!(user_id.is_none());
}
_ => panic!("Expected AccessDenied, got {err:?}"),
}
}
#[test]
fn test_ratelimit_circuit_open_maps_to_service_unavailable() {
let err = ApiError::from(RateLimitError::CircuitOpen);
match err {
ApiError::ServiceUnavailable {
service,
retry_after,
source,
} => {
assert_eq!(service, "circuit_breaker");
assert!(retry_after.is_none());
assert!(source.is_none());
}
_ => panic!("Expected ServiceUnavailable, got {err:?}"),
}
}
#[test]
fn test_ratelimit_quota_exhausted_maps_to_rate_limit_exceeded() {
let err = ApiError::from(RateLimitError::QuotaExhausted {
used: 50,
total: 100,
});
match err {
ApiError::RateLimitExceeded {
limit,
window_seconds,
} => {
assert_eq!(limit, 100);
assert_eq!(window_seconds, 50);
}
_ => panic!("Expected RateLimitExceeded, got {err:?}"),
}
}
#[test]
fn test_ratelimit_exceeded_u64_overflow_saturates_to_u32_max() {
let err = ApiError::from(RateLimitError::Exceeded {
limit: u64::MAX,
window_seconds: u64::MAX,
});
match err {
ApiError::RateLimitExceeded {
limit,
window_seconds,
} => {
assert_eq!(limit, u32::MAX);
assert_eq!(window_seconds, u32::MAX);
}
_ => panic!("Expected RateLimitExceeded, got {err:?}"),
}
}
#[test]
fn test_ratelimit_quota_exhausted_u64_overflow_saturates() {
let err = ApiError::from(RateLimitError::QuotaExhausted {
used: u64::MAX,
total: u64::MAX,
});
match err {
ApiError::RateLimitExceeded {
limit,
window_seconds,
} => {
assert_eq!(limit, u32::MAX);
assert_eq!(window_seconds, u32::MAX);
}
_ => panic!("Expected RateLimitExceeded, got {err:?}"),
}
}
#[test]
fn test_ratelimit_limiteron_variant_maps_to_internal_with_source() {
use limiteron::LimiteronError;
let err =
RateLimitError::Limiteron(LimiteronError::ConfigError("test config error".to_string()));
let api_error: ApiError = err.into();
match api_error {
ApiError::Internal { source, .. } => {
assert!(source.is_some());
}
_ => panic!("Expected Internal variant for Limiteron error"),
}
}
#[test]
fn test_ratelimit_error_display_all_variants() {
assert!(
RateLimitError::Exceeded {
limit: 100,
window_seconds: 60
}
.to_string()
.contains("100")
);
assert!(
RateLimitError::Banned {
reason: "abuse".to_string()
}
.to_string()
.contains("abuse")
);
assert!(
RateLimitError::CircuitOpen
.to_string()
.contains("Circuit breaker open")
);
assert!(
RateLimitError::QuotaExhausted {
used: 50,
total: 100
}
.to_string()
.contains("50/100")
);
}
}
#[cfg(feature = "logging")]
mod logging_advanced {
use sdforge::logging::{
LogEntry, LogFormat, LogLevel, LoggerConfig, LoggerError, StructuredLogger,
get_global_logger, init_global_logger,
};
#[test]
fn test_log_entry_with_field_various_value_types() {
let entry = LogEntry::new(LogLevel::Debug, "test", "msg")
.with_field("null_val", serde_json::Value::Null)
.with_field("bool_val", serde_json::json!(true))
.with_field("num_val", serde_json::json!(3.15))
.with_field("arr_val", serde_json::json!([1, 2, 3]))
.with_field("obj_val", serde_json::json!({"nested": "value"}));
assert_eq!(entry.fields.len(), 5);
assert_eq!(entry.fields.get("null_val"), Some(&serde_json::Value::Null));
assert_eq!(
entry.fields.get("arr_val"),
Some(&serde_json::json!([1, 2, 3]))
);
}
#[test]
fn test_log_entry_with_fields_overwrites_same_key() {
let entry = LogEntry::new(LogLevel::Info, "test", "msg")
.with_field("key", "first")
.with_field("key", "second");
assert_eq!(entry.fields.len(), 1);
assert_eq!(entry.fields.get("key"), Some(&serde_json::json!("second")));
}
#[test]
fn test_log_entry_with_fields_empty_vec() {
let entry = LogEntry::new(LogLevel::Info, "test", "msg").with_fields(vec![]);
assert_eq!(entry.fields.len(), 0);
}
#[test]
fn test_log_level_ordering_chained() {
assert!(LogLevel::Trace < LogLevel::Info);
assert!(LogLevel::Debug < LogLevel::Error);
assert!(LogLevel::Info < LogLevel::Warn);
}
#[test]
fn test_log_level_equality() {
assert_eq!(LogLevel::Info, LogLevel::Info);
assert!(LogLevel::Info >= LogLevel::Info);
assert!(LogLevel::Info <= LogLevel::Info);
}
#[test]
fn test_log_level_serde_roundtrip() {
let levels = [
LogLevel::Trace,
LogLevel::Debug,
LogLevel::Info,
LogLevel::Warn,
LogLevel::Error,
];
for level in levels {
let json = serde_json::to_string(&level).unwrap();
let restored: LogLevel = serde_json::from_str(&json).unwrap();
assert_eq!(level, restored);
}
}
#[test]
fn test_log_level_serde_lowercase() {
let json = serde_json::to_string(&LogLevel::Info).unwrap();
assert!(json.contains("info"));
}
#[test]
fn test_log_level_display_all() {
assert_eq!(format!("{}", LogLevel::Trace), "TRACE");
assert_eq!(format!("{}", LogLevel::Debug), "DEBUG");
assert_eq!(format!("{}", LogLevel::Info), "INFO");
assert_eq!(format!("{}", LogLevel::Warn), "WARN");
assert_eq!(format!("{}", LogLevel::Error), "ERROR");
}
#[test]
fn test_logger_config_default_values() {
let config = LoggerConfig::default();
assert_eq!(config.min_level, LogLevel::Info);
assert!(matches!(config.format, LogFormat::Json));
assert!(config.colored);
assert_eq!(config.max_file_size, 10 * 1024 * 1024);
assert_eq!(config.max_files, 5);
}
#[test]
fn test_logger_config_clone_independent() {
let config = LoggerConfig {
min_level: LogLevel::Debug,
format: LogFormat::Text,
colored: false,
max_file_size: 5 * 1024 * 1024,
max_files: 3,
};
let cloned = config.clone();
assert_eq!(cloned.min_level, config.min_level);
assert_eq!(cloned.format, config.format);
assert_eq!(cloned.colored, config.colored);
assert_eq!(cloned.max_file_size, config.max_file_size);
assert_eq!(cloned.max_files, config.max_files);
}
#[test]
fn test_log_format_equality() {
assert_eq!(LogFormat::Json, LogFormat::Json);
assert_eq!(LogFormat::Text, LogFormat::Text);
assert_ne!(LogFormat::Json, LogFormat::Text);
}
#[test]
fn test_logger_error_display() {
let err = LoggerError::AlreadyInitialized;
assert!(err.to_string().contains("already initialized"));
}
#[test]
fn test_logger_error_debug() {
let err = LoggerError::AlreadyInitialized;
let debug_str = format!("{:?}", err);
assert!(debug_str.contains("AlreadyInitialized"));
}
#[tokio::test]
#[serial_test::serial]
async fn test_global_logger_double_init_fails() {
let config = LoggerConfig::default();
let _first = init_global_logger(config.clone());
let second = init_global_logger(config);
assert!(
matches!(second, Err(LoggerError::AlreadyInitialized)),
"Second init_global_logger call should fail with AlreadyInitialized"
);
}
#[tokio::test]
#[serial_test::serial]
async fn test_global_logger_get_returns_some_after_init() {
let _ = init_global_logger(LoggerConfig::default());
let logger = get_global_logger();
assert!(
logger.is_some(),
"get_global_logger should return Some after initialization"
);
}
#[tokio::test]
#[serial_test::serial]
async fn test_global_logger_init_then_double_init() {
let config = LoggerConfig::default();
let _ = init_global_logger(config.clone());
let second = init_global_logger(config);
assert!(matches!(second, Err(LoggerError::AlreadyInitialized)));
}
#[tokio::test]
async fn test_structured_logger_emits_all_levels() {
let config = LoggerConfig {
min_level: LogLevel::Trace,
format: LogFormat::Json,
colored: false,
..Default::default()
};
let logger = StructuredLogger::new(config);
logger.trace("test", "trace msg", vec![]);
logger.debug("test", "debug msg", vec![]);
logger.info("test", "info msg", vec![]);
logger.warn("test", "warn msg", vec![]);
logger.error("test", "error msg", vec![]);
logger.flush().await;
logger.shutdown().await;
}
#[tokio::test]
async fn test_structured_logger_filters_below_min_level() {
let config = LoggerConfig {
min_level: LogLevel::Warn,
format: LogFormat::Json,
colored: false,
..Default::default()
};
let logger = StructuredLogger::new(config);
logger.trace("test", "should be filtered", vec![]);
logger.debug("test", "should be filtered", vec![]);
logger.info("test", "should be filtered", vec![]);
logger.warn("test", "warn msg", vec![]);
logger.error("test", "error msg", vec![]);
logger.flush().await;
logger.shutdown().await;
}
#[test]
fn test_log_info_macro_without_init_does_not_panic() {
sdforge::log_info!(
"e2e_test",
"macro call without init",
"key".to_string() => "val"
);
}
#[test]
fn test_log_error_macro_without_init_does_not_panic() {
sdforge::log_error!(
"e2e_test",
"error macro call",
"code".to_string() => 500
);
}
#[test]
fn test_log_debug_macro_without_init_does_not_panic() {
sdforge::log_debug!("e2e_test", "debug macro call");
}
}
#[cfg(feature = "i18n")]
mod i18n_advanced {
use sdforge::i18n::{HttpI18nFormatter, I18nError, parse_accept_language};
#[test]
fn test_i18n_parse_accept_language_standard() {
let locales = parse_accept_language("en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7");
assert_eq!(locales, vec!["en-US", "en", "zh-CN", "zh"]);
}
#[test]
fn test_i18n_parse_accept_language_default_q() {
let locales = parse_accept_language("fr,en;q=0.9");
assert_eq!(locales, vec!["fr", "en"]);
}
#[test]
fn test_i18n_parse_accept_language_q_zero_excluded() {
let locales = parse_accept_language("en;q=0,fr");
assert_eq!(locales, vec!["fr"]);
}
#[test]
fn test_i18n_parse_accept_language_empty() {
let locales = parse_accept_language("");
assert!(locales.is_empty());
}
#[test]
fn test_i18n_parse_accept_language_whitespace_entries() {
let locales = parse_accept_language(" , en ; q=0.9 , ");
assert_eq!(locales, vec!["en"]);
}
#[test]
fn test_i18n_parse_accept_language_malformed_q() {
let locales = parse_accept_language("fr;q=abc,en;q=0.5");
assert_eq!(locales, vec!["fr", "en"]);
}
#[test]
fn test_i18n_parse_accept_language_q_zero_mixed() {
let locales = parse_accept_language("en;q=0,fr;q=0.5,de");
assert_eq!(locales, vec!["de", "fr"]);
}
#[test]
fn test_i18n_parse_accept_language_single_locale() {
let locales = parse_accept_language("en-US");
assert_eq!(locales, vec!["en-US"]);
}
#[test]
fn test_i18n_parse_accept_language_all_q_zero() {
let locales = parse_accept_language("en;q=0,fr;q=0");
assert!(locales.is_empty());
}
#[test]
fn test_i18n_from_accept_language_valid() {
let fmt = HttpI18nFormatter::from_accept_language("en-US,en;q=0.9,zh-CN;q=0.8");
assert!(fmt.is_ok());
}
#[test]
fn test_i18n_from_accept_language_fallback_to_second() {
let fmt = HttpI18nFormatter::from_accept_language("not-a-locale!!!,en-US");
assert!(fmt.is_ok());
}
#[test]
fn test_i18n_from_accept_language_all_invalid_returns_no_valid_locale() {
let result = HttpI18nFormatter::from_accept_language("not-a-locale!!!");
assert!(result.is_err());
match result.err().unwrap() {
I18nError::NoValidLocale { header, .. } => {
assert_eq!(header, "not-a-locale!!!");
}
other => panic!("expected NoValidLocale, got {other:?}"),
}
}
#[test]
fn test_i18n_new_invalid_locale_returns_invalid_locale() {
let result = HttpI18nFormatter::new("not-a-valid-locale!!!");
assert!(result.is_err());
match result.err().unwrap() {
I18nError::InvalidLocale { input, .. } => {
assert_eq!(input, "not-a-valid-locale!!!");
}
other => panic!("expected InvalidLocale, got {other:?}"),
}
}
#[test]
fn test_i18n_new_valid_locale_en_us() {
let fmt = HttpI18nFormatter::new("en-US");
assert!(fmt.is_ok());
}
#[test]
fn test_i18n_new_valid_locale_zh_cn() {
let fmt = HttpI18nFormatter::new("zh-CN");
assert!(fmt.is_ok());
}
#[test]
fn test_i18n_format_error_message_singular() {
let fmt = HttpI18nFormatter::new("en").expect("en locale");
let msg = fmt.format_error_message(404, 1).expect("error message");
assert!(msg.contains("One"));
assert!(msg.contains("error"));
assert!(msg.contains("404"));
}
#[test]
fn test_i18n_format_error_message_plural() {
let fmt = HttpI18nFormatter::new("en").expect("en locale");
let msg = fmt.format_error_message(500, 2).expect("error message");
assert!(msg.contains("Other"));
assert!(msg.contains("errors"));
assert!(msg.contains("500"));
}
#[test]
fn test_i18n_format_number_en_us() {
let fmt = HttpI18nFormatter::new("en-US").expect("en-US locale");
let result = fmt.format_number(1_234_567.89_f64).expect("format number");
assert!(result.contains(','));
assert!(result.contains('.'));
}
#[test]
fn test_i18n_format_number_nan_rejected() {
let fmt = HttpI18nFormatter::new("en-US").expect("en-US locale");
assert!(fmt.format_number(f64::NAN).is_err());
}
#[test]
fn test_i18n_format_number_infinity_rejected() {
let fmt = HttpI18nFormatter::new("en-US").expect("en-US locale");
assert!(fmt.format_number(f64::INFINITY).is_err());
}
#[test]
fn test_i18n_format_timestamp_valid() {
let fmt = HttpI18nFormatter::new("en-US").expect("en-US locale");
let result = fmt.format_timestamp(2026, 7, 11).expect("format timestamp");
assert!(result.contains("2026"));
assert!(!result.is_empty());
}
#[test]
fn test_i18n_format_timestamp_invalid_month() {
let fmt = HttpI18nFormatter::new("en-US").expect("en-US locale");
assert!(fmt.format_timestamp(2026, 13, 1).is_err());
}
#[test]
fn test_i18n_format_timestamp_feb_30() {
let fmt = HttpI18nFormatter::new("en-US").expect("en-US locale");
assert!(fmt.format_timestamp(2026, 2, 30).is_err());
}
#[test]
fn test_i18n_compare_headers_less() {
use std::cmp::Ordering;
let fmt = HttpI18nFormatter::new("en").expect("en locale");
assert_eq!(
fmt.compare_headers("apple", "banana").expect("compare"),
Ordering::Less
);
}
#[test]
fn test_i18n_compare_headers_greater() {
use std::cmp::Ordering;
let fmt = HttpI18nFormatter::new("en").expect("en locale");
assert_eq!(
fmt.compare_headers("banana", "apple").expect("compare"),
Ordering::Greater
);
}
#[test]
fn test_i18n_compare_headers_equal() {
use std::cmp::Ordering;
let fmt = HttpI18nFormatter::new("en").expect("en locale");
assert_eq!(
fmt.compare_headers("apple", "apple").expect("compare"),
Ordering::Equal
);
}
}
#[cfg(feature = "security")]
mod security_advanced {
use sdforge::security::{AuthConfigError, AuthError, JwtError};
#[test]
fn test_auth_error_missing_auth_display() {
let err = AuthError::MissingAuth;
assert!(
err.to_string()
.contains("Missing or invalid authorization header")
);
}
#[test]
fn test_auth_error_invalid_token_display() {
let err = AuthError::InvalidToken;
assert!(err.to_string().contains("Invalid or expired token"));
}
#[test]
fn test_auth_error_insufficient_permissions_display() {
let err = AuthError::InsufficientPermissions {
required: "admin".to_string(),
user_permissions: vec!["read".to_string()],
};
let msg = err.to_string();
assert!(msg.contains("Insufficient permissions"));
assert!(msg.contains("admin"));
}
#[test]
fn test_jwt_error_invalid_format() {
let err = JwtError::InvalidFormat;
let debug = format!("{:?}", err);
assert!(debug.contains("InvalidFormat"));
}
#[test]
fn test_jwt_error_base64_decode_error() {
let err = JwtError::Base64DecodeError;
let debug = format!("{:?}", err);
assert!(debug.contains("Base64DecodeError"));
}
#[test]
fn test_jwt_error_invalid_signature() {
let err = JwtError::InvalidSignature;
let debug = format!("{:?}", err);
assert!(debug.contains("InvalidSignature"));
}
#[test]
fn test_jwt_error_expired() {
let err = JwtError::Expired;
let debug = format!("{:?}", err);
assert!(debug.contains("Expired"));
}
#[test]
fn test_jwt_error_not_yet_valid() {
let err = JwtError::NotYetValid;
let debug = format!("{:?}", err);
assert!(debug.contains("NotYetValid"));
}
#[test]
fn test_jwt_error_invalid_payload() {
let err = JwtError::InvalidPayload;
let debug = format!("{:?}", err);
assert!(debug.contains("InvalidPayload"));
}
#[test]
fn test_jwt_error_clock_skew() {
let err = JwtError::ClockSkew;
let debug = format!("{:?}", err);
assert!(debug.contains("ClockSkew"));
}
#[test]
fn test_auth_config_error_invalid_secret_display() {
let err = AuthConfigError::InvalidSecret("bad secret".to_string());
let msg = err.to_string();
assert!(msg.contains("Invalid secret"));
assert!(msg.contains("bad secret"));
}
#[test]
fn test_auth_config_error_secret_too_short_display() {
let err = AuthConfigError::SecretTooShort { length: 10 };
let msg = err.to_string();
assert!(msg.contains("Secret too short"));
assert!(msg.contains("10"));
assert!(msg.contains("32"));
}
#[test]
fn test_auth_config_error_missing_character_class_display() {
let err = AuthConfigError::MissingCharacterClass {
required_type: "uppercase letter",
};
let msg = err.to_string();
assert!(msg.contains("uppercase letter"));
assert!(msg.contains("at least one"));
}
#[test]
fn test_auth_config_error_from_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
let err: AuthConfigError = io_err.into();
match err {
AuthConfigError::IoError { .. } => {}
other => panic!("Expected IoError, got {other:?}"),
}
}
}
#[cfg(feature = "ratelimit")]
mod ratelimit_advanced {
use sdforge::security::{RateLimitError, RateLimiter};
#[test]
fn test_ratelimit_error_exceeded_construction() {
let err = RateLimitError::Exceeded {
limit: 100,
window_seconds: 60,
};
let msg = err.to_string();
assert!(msg.contains("100"));
assert!(msg.contains("60"));
}
#[test]
fn test_ratelimit_error_banned_construction() {
let err = RateLimitError::Banned {
reason: "abuse".to_string(),
};
assert!(err.to_string().contains("abuse"));
}
#[test]
fn test_ratelimit_error_circuit_open_construction() {
let err = RateLimitError::CircuitOpen;
assert!(err.to_string().contains("Circuit breaker open"));
}
#[test]
fn test_ratelimit_error_quota_exhausted_construction() {
let err = RateLimitError::QuotaExhausted {
used: 50,
total: 100,
};
let msg = err.to_string();
assert!(msg.contains("50"));
assert!(msg.contains("100"));
}
#[test]
fn test_ratelimit_error_from_limiteron_error() {
use limiteron::LimiteronError;
let lim_err = LimiteronError::ConfigError("bad config".to_string());
let err: RateLimitError = lim_err.into();
match err {
RateLimitError::Limiteron(inner) => {
assert!(inner.to_string().contains("bad config"));
}
_ => panic!("Expected Limiteron variant"),
}
}
#[tokio::test]
async fn test_limiteron_adapter_new_constructs_successfully() {
use sdforge::security::LimiteronAdapter;
let adapter = LimiteronAdapter::new().await;
let result = adapter.check("127.0.0.1").await;
assert!(result.is_ok(), "default config should allow first request");
}
#[tokio::test]
async fn test_limiteron_adapter_default_constructs_successfully() {
use sdforge::security::LimiteronAdapter;
let adapter = LimiteronAdapter::default().await;
let _ = adapter.check("192.168.1.1").await;
}
#[tokio::test]
async fn test_limiteron_adapter_builder_default_config() {
use sdforge::security::LimiteronAdapter;
let adapter = LimiteronAdapter::builder().build().await;
assert!(
adapter.is_ok(),
"builder with default config should succeed"
);
}
#[tokio::test]
async fn test_limiteron_adapter_check_allows_first_request() {
use sdforge::security::{LimiteronAdapter, RateLimiter};
let adapter = LimiteronAdapter::new().await;
let result = adapter.check("10.0.0.1").await;
assert!(result.is_ok());
}
}
#[cfg(any(feature = "http", feature = "security"))]
mod regex_cache_advanced {
use sdforge::core::regex_cache::{RegexCache, common, get_regex};
#[test]
fn test_regex_cache_get_or_compile_returns_compiled_regex() {
let cache = RegexCache::new();
let regex = cache.get_or_compile(r"^\d+$").expect("valid pattern");
assert!(regex.is_match("123"));
assert!(!regex.is_match("abc"));
}
#[test]
fn test_regex_cache_hit_returns_same_arc() {
let cache = RegexCache::new();
let first = cache.get_or_compile(r"^\d+$").expect("first compile");
let second = cache
.get_or_compile(r"^\d+$")
.expect("second compile (cache hit)");
assert!(std::sync::Arc::ptr_eq(&first, &second));
}
#[test]
fn test_regex_cache_miss_returns_different_arc() {
let cache = RegexCache::new();
let first = cache.get_or_compile(r"^\d+$").expect("first pattern");
let second = cache.get_or_compile(r"^[a-z]+$").expect("second pattern");
assert!(!std::sync::Arc::ptr_eq(&first, &second));
}
#[test]
fn test_regex_cache_invalid_pattern_returns_error() {
let cache = RegexCache::new();
let result = cache.get_or_compile(r"[invalid(");
assert!(result.is_err());
}
#[test]
fn test_regex_cache_clear_empties_all() {
let cache = RegexCache::new();
cache.get_or_compile(r"^\d+$").expect("compile");
cache.get_or_compile(r"^[a-z]+$").expect("compile");
assert_eq!(cache.stats().total_patterns, 2);
cache.clear();
assert_eq!(cache.stats().total_patterns, 0);
}
#[test]
fn test_regex_cache_stats_reports_capacity() {
let cache = RegexCache::with_capacity(500);
let stats = cache.stats();
assert_eq!(stats.max_capacity, 500);
assert_eq!(stats.total_patterns, 0);
}
#[test]
fn test_regex_cache_eviction_removes_oldest() {
let cache = RegexCache::with_capacity(2);
cache.get_or_compile(r"^\d+$").expect("first");
cache.get_or_compile(r"^[a-z]+$").expect("second");
cache.get_or_compile(r"^[A-Z]+$").expect("third");
let stats = cache.stats();
assert!(
stats.total_patterns <= 2,
"cache should evict to stay within capacity: got {}",
stats.total_patterns
);
}
#[test]
fn test_regex_cache_global_get_regex_returns_compiled() {
let regex = get_regex(r"^\d{3}-\d{3}-\d{4}$").expect("valid pattern");
assert!(regex.is_match("123-456-7890"));
assert!(!regex.is_match("123-456-789"));
}
#[test]
fn test_regex_common_email_matches_valid() {
let regex = common::email();
assert!(regex.is_match("user@example.com"));
assert!(regex.is_match("test.user+tag@sub.example.co.uk"));
assert!(!regex.is_match("not-an-email"));
}
#[test]
fn test_regex_common_url_matches_valid() {
let regex = common::url();
assert!(regex.is_match("https://example.com"));
assert!(regex.is_match("http://sub.example.com/path?q=1"));
assert!(!regex.is_match("not-a-url"));
}
#[test]
fn test_regex_common_ipv4_matches_valid() {
let regex = common::ipv4();
assert!(regex.is_match("192.168.1.1"));
assert!(regex.is_match("10.0.0.1"));
assert!(regex.is_match("255.255.255.255"));
assert!(!regex.is_match("256.1.1.1"));
assert!(!regex.is_match("not-an-ip"));
}
#[test]
fn test_regex_common_uuid_matches_valid() {
let regex = common::uuid();
assert!(regex.is_match("550e8400-e29b-41d4-a716-446655440000"));
assert!(regex.is_match("12345678-1234-1234-1234-1234567890AB"));
assert!(!regex.is_match("not-a-uuid"));
assert!(!regex.is_match("550e8400-e29b-41d4-a716-4466554400"));
}
#[test]
fn test_regex_cache_with_capacity_construction() {
let cache = RegexCache::with_capacity(100);
let stats = cache.stats();
assert_eq!(stats.max_capacity, 100);
assert_eq!(stats.total_patterns, 0);
}
#[test]
fn test_regex_cache_default_construction() {
let cache = RegexCache::default();
let stats = cache.stats();
assert!(stats.max_capacity > 0);
assert_eq!(stats.total_patterns, 0);
}
#[test]
fn test_regex_cache_stats_clone_preserves_values() {
let cache = RegexCache::with_capacity(100);
cache.get_or_compile(r"^\d+$").expect("compile");
let stats = cache.stats();
let copied = stats;
assert_eq!(copied.total_patterns, stats.total_patterns);
assert_eq!(copied.max_capacity, stats.max_capacity);
}
}
#[cfg(feature = "http")]
mod plugin_init_advanced {
use sdforge::init_all_plugins;
#[test]
#[serial_test::serial]
fn test_plugin_init_returns_counts() {
let counts = init_all_plugins();
let _ = counts.routes;
}
#[test]
#[serial_test::serial]
fn test_plugin_init_is_idempotent() {
let first = init_all_plugins();
let second = init_all_plugins();
assert_eq!(first.routes, second.routes);
}
#[test]
#[serial_test::serial]
fn test_plugin_init_multiple_calls_consistent() {
let c1 = init_all_plugins();
let c2 = init_all_plugins();
let c3 = init_all_plugins();
assert_eq!(c1.routes, c2.routes);
assert_eq!(c2.routes, c3.routes);
}
#[test]
#[serial_test::serial]
fn test_plugin_counts_fields_accessible() {
let counts = init_all_plugins();
let _routes = counts.routes;
}
}
mod cross_module_advanced {
use sdforge::error::{ApiError, LocalizedError, SdForgeError, TranslationStore};
#[test]
fn test_cross_error_localized_zh_not_found() {
let err = ApiError::not_found("User", None);
let msg = err.localized_message(&"zh".to_string());
assert!(msg.contains("资源未找到"));
assert!(msg.contains("User"));
}
#[test]
fn test_cross_error_localized_zh_cn_variant() {
let err = ApiError::not_found("Document", None);
let msg = err.localized_message(&"zh-CN".to_string());
assert!(msg.contains("资源未找到"));
assert!(msg.contains("Document"));
}
#[test]
fn test_cross_error_localized_zh_hans_variant() {
let err = ApiError::not_found("Item", None);
let msg = err.localized_message(&"zh-Hans".to_string());
assert!(msg.contains("资源未找到"));
}
#[test]
fn test_cross_error_localized_fr_not_found() {
let err = ApiError::not_found("User", None);
let msg = err.localized_message(&"fr".to_string());
assert!(msg.contains("Ressource introuvable"));
assert!(msg.contains("User"));
}
#[test]
fn test_cross_error_localized_fr_fr_variant() {
let err = ApiError::not_found("Doc", None);
let msg = err.localized_message(&"fr-FR".to_string());
assert!(msg.contains("Ressource introuvable"));
}
#[test]
fn test_cross_error_localized_es_not_found() {
let err = ApiError::not_found("User", None);
let msg = err.localized_message(&"es".to_string());
assert!(msg.contains("Recurso no encontrado"));
assert!(msg.contains("User"));
}
#[test]
fn test_cross_error_localized_es_es_variant() {
let err = ApiError::not_found("Doc", None);
let msg = err.localized_message(&"es-ES".to_string());
assert!(msg.contains("Recurso no encontrado"));
}
#[test]
fn test_cross_error_localized_unknown_locale_falls_back_to_english() {
let err = ApiError::not_found("User", None);
let msg = err.localized_message(&"xx-YY".to_string());
assert!(msg.contains("User"));
}
#[test]
fn test_cross_error_localized_zh_invalid_input() {
let err = ApiError::invalid_input("bad value", None, None);
let msg = err.localized_message(&"zh".to_string());
assert!(msg.contains("无效输入"));
assert!(msg.contains("bad value"));
}
#[test]
fn test_cross_error_localized_zh_authentication_failed() {
let err = ApiError::authentication_failed("expired");
let msg = err.localized_message(&"zh".to_string());
assert!(msg.contains("认证失败"));
assert!(msg.contains("expired"));
}
#[test]
fn test_cross_error_localized_zh_access_denied() {
let err = ApiError::access_denied("read", None);
let msg = err.localized_message(&"zh".to_string());
assert!(msg.contains("访问被拒绝"));
assert!(msg.contains("read"));
}
#[test]
fn test_cross_error_localized_zh_rate_limit_exceeded() {
let err = ApiError::rate_limit_exceeded(100, 60);
let msg = err.localized_message(&"zh".to_string());
assert!(msg.contains("请求频率超限"));
assert!(msg.contains("100"));
assert!(msg.contains("60"));
}
#[test]
fn test_cross_error_localized_zh_internal() {
let err = ApiError::internal_error("secret", "id");
let msg = err.localized_message(&"zh".to_string());
assert!(msg.contains("内部错误"));
}
#[test]
fn test_cross_error_localized_zh_service_unavailable() {
let err = ApiError::service_unavailable("db", None);
let msg = err.localized_message(&"zh".to_string());
assert!(msg.contains("服务不可用"));
assert!(msg.contains("db"));
}
#[test]
fn test_cross_error_localized_zh_validation_error() {
let err = ApiError::validation("email", "bad format");
let msg = err.localized_message(&"zh".to_string());
assert!(msg.contains("验证失败"));
assert!(msg.contains("email"));
}
#[test]
fn test_cross_error_default_message_matches_display() {
let err = ApiError::not_found("User", None);
let default = err.default_message();
let display = err.to_string();
assert_eq!(default, display);
}
#[test]
fn test_cross_translation_store_add_and_get() {
let mut store = TranslationStore::new();
store.add_translation("zh-CN".to_string(), "Hello".to_string(), "你好".to_string());
store.add_translation(
"fr-FR".to_string(),
"Hello".to_string(),
"Bonjour".to_string(),
);
assert_eq!(
store.get(&"zh-CN".to_string(), "Hello"),
Some(&"你好".to_string())
);
assert_eq!(
store.get(&"fr-FR".to_string(), "Hello"),
Some(&"Bonjour".to_string())
);
}
#[test]
fn test_cross_translation_store_get_missing_locale_returns_none() {
let store = TranslationStore::new();
assert!(store.get(&"xx-YY".to_string(), "Hello").is_none());
}
#[test]
fn test_cross_translation_store_get_missing_key_returns_none() {
let mut store = TranslationStore::new();
store.add_translation("en".to_string(), "Hello".to_string(), "Hi".to_string());
assert!(store.get(&"en".to_string(), "Goodbye").is_none());
}
#[test]
fn test_cross_translation_store_default_is_empty() {
let store = TranslationStore::default();
assert!(store.get(&"en".to_string(), "Hello").is_none());
}
#[test]
fn test_cross_translation_store_overwrites_same_key() {
let mut store = TranslationStore::new();
store.add_translation("en".to_string(), "Hello".to_string(), "Hi".to_string());
store.add_translation("en".to_string(), "Hello".to_string(), "Hey".to_string());
assert_eq!(
store.get(&"en".to_string(), "Hello"),
Some(&"Hey".to_string())
);
}
#[test]
fn test_cross_error_api_to_sdforge_error_chain() {
let api_err = ApiError::not_found("User", Some("42".to_string()));
let sd_err: SdForgeError = api_err.into();
assert_eq!(
sd_err.category(),
sdforge::error::ErrorCategory::ClientError
);
let msg = sd_err.sanitized_message();
assert!(msg.contains("User"));
}
#[test]
fn test_cross_error_api_to_service_error_chain() {
use sdforge::core::ServiceError;
let api_err = ApiError::authentication_failed("bad token");
let svc: ServiceError = api_err.into();
assert_eq!(svc.code(), "AUTHENTICATION_FAILED");
assert_eq!(svc.http_status(), 401);
}
#[test]
fn test_cross_error_internal_with_context_propagates_to_service_error() {
use sdforge::error::ErrorContext;
let ctx = ErrorContext::new()
.with_extra("trace_id".to_string(), "trace-123".to_string())
.with_extra("span_id".to_string(), "span-456".to_string());
let err = ApiError::internal_with_context("msg", "err-ctx", ctx);
let svc = err.to_service_error();
let details = svc.details().expect("details should be present");
assert_eq!(details["error_id"], "err-ctx");
let context_value = details
.get("context")
.expect("context should be in details");
let context_obj = context_value
.as_object()
.expect("context should be a JSON object");
let extra = context_obj
.get("extra")
.and_then(|v| v.as_object())
.expect("extra should be a JSON object in context");
assert_eq!(extra.get("trace_id"), Some(&serde_json::json!("trace-123")));
assert_eq!(extra.get("span_id"), Some(&serde_json::json!("span-456")));
}
}
#[cfg(feature = "http")]
mod validation_constants_advanced {
use sdforge::core::validation::{
MAX_API_KEY_LENGTH, MAX_EMAIL_LENGTH, MAX_HEADER_COUNT, MAX_HEADER_NAME_LENGTH,
MAX_HEADER_VALUE_LENGTH, MAX_JSON_ARRAY_LENGTH, MAX_JSON_DEPTH, MAX_JSON_FIELD_NAME_LENGTH,
MAX_JWT_TOKEN_LENGTH, MAX_PASSWORD_LENGTH, MAX_QUERY_STRING_LENGTH, MAX_REQUEST_BODY_SIZE,
MAX_TEXT_FIELD_LENGTH, MAX_URI_PATH_LENGTH, MAX_USERNAME_LENGTH, MIN_PASSWORD_LENGTH,
};
#[test]
fn test_validation_constants_are_positive() {
const { assert!(MAX_REQUEST_BODY_SIZE > 0) };
const { assert!(MAX_HEADER_NAME_LENGTH > 0) };
const { assert!(MAX_HEADER_VALUE_LENGTH > 0) };
const { assert!(MAX_URI_PATH_LENGTH > 0) };
const { assert!(MAX_QUERY_STRING_LENGTH > 0) };
const { assert!(MAX_HEADER_COUNT > 0) };
const { assert!(MAX_API_KEY_LENGTH > 0) };
const { assert!(MAX_JWT_TOKEN_LENGTH > 0) };
const { assert!(MAX_USERNAME_LENGTH > 0) };
const { assert!(MAX_EMAIL_LENGTH > 0) };
const { assert!(MAX_PASSWORD_LENGTH > 0) };
const { assert!(MIN_PASSWORD_LENGTH > 0) };
const { assert!(MAX_TEXT_FIELD_LENGTH > 0) };
const { assert!(MAX_JSON_FIELD_NAME_LENGTH > 0) };
const { assert!(MAX_JSON_ARRAY_LENGTH > 0) };
const { assert!(MAX_JSON_DEPTH > 0) };
}
#[test]
fn test_validation_constants_specific_values() {
assert_eq!(MAX_REQUEST_BODY_SIZE, 10 * 1024 * 1024);
assert_eq!(MAX_HEADER_NAME_LENGTH, 128);
assert_eq!(MAX_HEADER_VALUE_LENGTH, 8 * 1024);
assert_eq!(MAX_URI_PATH_LENGTH, 2048);
assert_eq!(MAX_QUERY_STRING_LENGTH, 8 * 1024);
assert_eq!(MAX_HEADER_COUNT, 100);
assert_eq!(MAX_API_KEY_LENGTH, 512);
assert_eq!(MAX_JWT_TOKEN_LENGTH, 4096);
assert_eq!(MAX_USERNAME_LENGTH, 256);
assert_eq!(MAX_EMAIL_LENGTH, 320);
assert_eq!(MAX_PASSWORD_LENGTH, 1024);
assert_eq!(MIN_PASSWORD_LENGTH, 8);
assert_eq!(MAX_TEXT_FIELD_LENGTH, 10 * 1024);
assert_eq!(MAX_JSON_FIELD_NAME_LENGTH, 256);
assert_eq!(MAX_JSON_ARRAY_LENGTH, 10_000);
assert_eq!(MAX_JSON_DEPTH, 100);
}
}