pub const MAX_REQUEST_BODY_SIZE: usize = 10 * 1024 * 1024;
pub const MAX_HEADER_NAME_LENGTH: usize = 128;
pub const MAX_HEADER_VALUE_LENGTH: usize = 8 * 1024;
pub const MAX_URI_PATH_LENGTH: usize = 2048;
pub const MAX_QUERY_STRING_LENGTH: usize = 8 * 1024;
pub const MAX_HEADER_COUNT: usize = 100;
pub const MAX_API_KEY_LENGTH: usize = 512;
pub const MAX_JWT_TOKEN_LENGTH: usize = 4096;
pub const MAX_USERNAME_LENGTH: usize = 256;
pub const MAX_EMAIL_LENGTH: usize = 320;
pub const MAX_PASSWORD_LENGTH: usize = 1024;
pub const MIN_PASSWORD_LENGTH: usize = 8;
pub const MAX_TEXT_FIELD_LENGTH: usize = 10 * 1024;
pub const MAX_JSON_FIELD_NAME_LENGTH: usize = 256;
pub const MAX_JSON_ARRAY_LENGTH: usize = 10_000;
pub const MAX_JSON_DEPTH: usize = 100;
#[cfg(feature = "http")]
use serde::Deserialize;
#[cfg(feature = "http")]
use thiserror::Error;
#[cfg(feature = "http")]
use validator::{Validate, ValidationErrors};
#[cfg(feature = "http")]
#[derive(Debug, Error, Clone)]
#[error("Validation failed: {errors:?}")]
pub struct ValidationErrorsWrapper {
pub errors: Vec<FieldValidationError>,
}
#[cfg(feature = "http")]
impl ValidationErrorsWrapper {
pub fn new(errors: Vec<FieldValidationError>) -> Self {
Self { errors }
}
pub fn from_validation_errors(errors: &ValidationErrors) -> Self {
let field_errors: Vec<FieldValidationError> = errors
.field_errors()
.into_iter()
.map(|(field, errors)| FieldValidationError {
field: field.to_string(),
constraints: errors.iter().map(|e| e.code.to_string()).collect(),
})
.collect();
Self::new(field_errors)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FieldValidationError {
pub field: String,
pub constraints: Vec<String>,
}
#[cfg(feature = "http")]
pub trait ValidatedParam: for<'de> Deserialize<'de> + Validate {}
#[cfg(feature = "http")]
impl<T: for<'de> Deserialize<'de> + Validate> ValidatedParam for T {}
#[cfg(feature = "http")]
pub type ValidationResult<T> = Result<T, ValidationErrorsWrapper>;
#[cfg(feature = "http")]
impl From<ValidationErrorsWrapper> for super::ApiError {
fn from(err: ValidationErrorsWrapper) -> Self {
let first_error = err.errors.first();
if let Some(error) = first_error {
let constraint = error
.constraints
.first()
.cloned()
.unwrap_or_else(|| "invalid".to_string());
Self::ValidationError {
field: error.field.clone(),
constraint,
}
} else {
Self::InvalidInput {
message: "Validation failed".to_string(),
field: None,
value: None,
}
}
}
}
#[cfg(feature = "http")]
pub mod validators {
use super::*;
use once_cell::sync::Lazy;
use std::collections::HashMap;
use std::sync::Mutex;
use validator::ValidationError;
static REGEX_CACHE: Lazy<Mutex<HashMap<String, regex::Regex>>> =
Lazy::new(|| Mutex::new(HashMap::new()));
pub fn validate_email(email: &str) -> Result<(), ValidationError> {
if !email.contains('@') {
return Err(ValidationError::new("email"));
}
Ok(())
}
pub fn validate_regex(value: &str, pattern: &str) -> Result<(), ValidationError> {
let regex = match REGEX_CACHE.lock() {
Ok(mut cache) => {
if let Some(cached) = cache.get(pattern) {
cached.clone()
} else {
let new_regex =
regex::Regex::new(pattern).map_err(|_| ValidationError::new("regex"))?;
cache.insert(pattern.to_string(), new_regex.clone());
new_regex
}
}
Err(_) => {
regex::Regex::new(pattern).map_err(|_| ValidationError::new("regex"))?
}
};
if !regex.is_match(value) {
return Err(ValidationError::new("regex"));
}
Ok(())
}
pub fn validate_range<T: PartialOrd + Copy>(
value: T,
min: T,
max: T,
) -> Result<(), ValidationError> {
if value < min || value > max {
return Err(ValidationError::new("range"));
}
Ok(())
}
pub fn validate_length(value: &str, min: usize, max: usize) -> Result<(), ValidationError> {
let len = value.chars().count();
if len < min || len > max {
return Err(ValidationError::new("length"));
}
Ok(())
}
pub fn validate_or_error<F, E>(validate_fn: F, _error_map: impl FnOnce() -> E) -> Result<(), E>
where
F: FnOnce() -> Result<(), ValidationError>,
E: From<ValidationErrorsWrapper>,
{
validate_fn().map_err(|_| {
let errors = ValidationErrorsWrapper::new(vec![]);
errors.into()
})
}
}
#[cfg(feature = "http")]
#[allow(dead_code)] pub(crate) mod sanitizer {
use crate::core::ApiError;
use std::path::PathBuf;
pub fn sanitize_xss(input: &str) -> String {
input
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
.replace('\'', "'")
.replace('/', "/")
}
#[allow(clippy::result_large_err)]
pub fn sanitize_path(input: &str) -> Result<String, ApiError> {
let cleaned = input.replace('\0', "");
if cleaned.contains("..") || cleaned.contains("//") {
return Err(ApiError::validation_error(
"INVALID_PATH",
"Path contains invalid characters or traversal attempts",
));
}
let _path = PathBuf::from(&cleaned);
Ok(cleaned)
}
#[allow(clippy::result_large_err)]
pub fn sanitize_filename(input: &str) -> Result<String, ApiError> {
if input.is_empty() {
return Err(ApiError::validation_error(
"INVALID_FILENAME",
"Filename cannot be empty",
));
}
let sanitized: String = input
.chars()
.filter(|c| c.is_alphanumeric() || *c == '_' || *c == '-' || *c == '.' || *c == ' ')
.collect();
if sanitized.is_empty() {
return Err(ApiError::validation_error(
"INVALID_FILENAME",
"Filename contains only invalid characters",
));
}
if sanitized.contains('/') || sanitized.contains('\\') {
return Err(ApiError::validation_error(
"INVALID_FILENAME",
"Filename cannot contain path separators",
));
}
Ok(sanitized)
}
#[allow(clippy::result_large_err)]
pub fn validate_user_id(id: i64) -> Result<i64, ApiError> {
if id <= 0 {
Err(ApiError::validation_error(
"INVALID_ID",
"User ID must be a positive integer",
))
} else {
Ok(id)
}
}
#[allow(clippy::result_large_err)]
pub fn validate_not_empty(input: &str, field_name: &str) -> Result<String, ApiError> {
let trimmed = input.trim().to_string();
if trimmed.is_empty() {
Err(ApiError::validation_error(
"EMPTY_FIELD",
format!("{} cannot be empty", field_name),
))
} else {
Ok(trimmed)
}
}
#[allow(clippy::result_large_err)]
pub fn validate_length(
input: &str,
min: usize,
max: usize,
field_name: &str,
) -> Result<String, ApiError> {
if min > max {
return Err(ApiError::InvalidInput {
message: format!("Invalid validation parameters for {}", field_name),
field: Some(field_name.to_string()),
value: None,
});
}
let len = input.len();
if len < min {
Err(ApiError::validation_error(
"TOO_SHORT",
format!("{} must be at least {} characters", field_name, min),
))
} else if len > max {
Err(ApiError::validation_error(
"TOO_LONG",
format!("{} must be at most {} characters", field_name, max),
))
} else {
Ok(input.to_string())
}
}
#[allow(clippy::result_large_err)]
pub fn validate_email_format(email: &str) -> Result<String, ApiError> {
let trimmed = email.trim().to_string();
if !trimmed.contains('@') {
return Err(ApiError::validation_error(
"INVALID_EMAIL",
"Email must contain @ symbol",
));
}
if !trimmed.contains('.') {
return Err(ApiError::validation_error(
"INVALID_EMAIL",
"Email must contain a domain",
));
}
if trimmed.starts_with('@') || trimmed.ends_with('@') {
return Err(ApiError::validation_error(
"INVALID_EMAIL",
"Invalid email format",
));
}
Ok(trimmed)
}
}
#[cfg(feature = "http")]
pub async fn extract_validated<T>(json: &serde_json::Value) -> ValidationResult<T>
where
T: ValidatedParam + Send,
{
let params: T =
serde_json::from_value(json.clone()).map_err(|_| ValidationErrorsWrapper::new(vec![]))?;
params
.validate()
.map_err(|e| ValidationErrorsWrapper::from_validation_errors(&e))?;
Ok(params)
}
#[cfg(all(feature = "http", test))]
mod tests {
use super::super::ApiError;
use super::*;
use serde::Deserialize;
use validator::Validate;
#[derive(Debug, Deserialize, Validate)]
struct TestParams {
#[validate(length(min = 1, max = 100))]
name: String,
#[validate(email)]
email: String,
#[validate(range(min = 18, max = 120))]
age: u32,
}
#[tokio::test]
async fn test_valid_params() {
let json = serde_json::json!({
"name": "John",
"email": "john@example.com",
"age": 25
});
let result = extract_validated::<TestParams>(&json).await;
assert!(result.is_ok());
}
#[tokio::test]
async fn test_invalid_email() {
let json = serde_json::json!({
"name": "John",
"email": "invalid-email",
"age": 25
});
let result = extract_validated::<TestParams>(&json).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_age_out_of_range() {
let json = serde_json::json!({
"name": "John",
"email": "john@example.com",
"age": 10
});
let result = extract_validated::<TestParams>(&json).await;
assert!(result.is_err());
}
#[test]
fn test_sanitize_xss_legitimate_input() {
let input = "Hello, World!";
let sanitized = sanitizer::sanitize_xss(input);
assert_eq!(sanitized, input);
let input = "This is a normal sentence with punctuation.";
let sanitized = sanitizer::sanitize_xss(input);
assert_eq!(sanitized, input);
}
#[test]
fn test_sanitize_xss_script_tag() {
let input = "<script>alert('xss')</script>";
let sanitized = sanitizer::sanitize_xss(input);
assert_eq!(
sanitized,
"<script>alert('xss')</script>"
);
assert!(!sanitized.contains("<script>"));
}
#[test]
fn test_sanitize_xss_img_tag() {
let input = "<img src=x onerror=alert('xss')>";
let sanitized = sanitizer::sanitize_xss(input);
assert_eq!(
sanitized,
"<img src=x onerror=alert('xss')>"
);
assert!(!sanitized.contains("<img"));
}
#[test]
fn test_sanitize_xss_iframe_tag() {
let input = "<iframe src=\"http://evil.com\"></iframe>";
let sanitized = sanitizer::sanitize_xss(input);
assert_eq!(
sanitized,
"<iframe src="http://evil.com"></iframe>"
);
assert!(!sanitized.contains("<iframe"));
}
#[test]
fn test_sanitize_xss_multiple_special_chars() {
let input = "<div>'\"/\\";
let sanitized = sanitizer::sanitize_xss(input);
assert_eq!(sanitized, "<div>'"/\\");
assert!(!sanitized.contains('<'));
assert!(!sanitized.contains('>'));
assert!(!sanitized.contains('"'));
assert!(!sanitized.contains('\''));
}
#[test]
fn test_sanitize_path_legitimate() {
let input = "/var/log/app.log";
let result = sanitizer::sanitize_path(input);
assert!(result.is_ok());
assert_eq!(result.unwrap(), input);
let input = "home/user/documents";
let result = sanitizer::sanitize_path(input);
assert!(result.is_ok());
assert_eq!(result.unwrap(), input);
}
#[test]
fn test_sanitize_path_reject_double_dot() {
let input = "../../../etc/passwd";
let result = sanitizer::sanitize_path(input);
assert!(result.is_err());
if let Err(ApiError::InvalidInput {
message: msg,
field: _,
value: _,
}) = result
{
assert!(msg.contains("invalid") || msg.contains("traversal"));
} else {
panic!("Expected InvalidInput error");
}
}
#[test]
fn test_sanitize_path_reject_double_slash() {
let input = "//etc/passwd";
let result = sanitizer::sanitize_path(input);
assert!(result.is_err());
}
#[test]
fn test_sanitize_path_null_byte() {
let input = "file\0.txt";
let result = sanitizer::sanitize_path(input);
assert!(result.is_ok());
assert_eq!(result.unwrap(), "file.txt");
}
#[test]
fn test_sanitize_path_mixed_traversal() {
let input = "/var/../etc/passwd";
let result = sanitizer::sanitize_path(input);
assert!(result.is_err());
}
#[test]
fn test_custom_email_validator_valid() {
let result = validators::validate_email("user@example.com");
assert!(result.is_ok());
}
#[test]
fn test_custom_email_validator_invalid() {
let result = validators::validate_email("notanemail");
assert!(result.is_err());
}
#[test]
fn test_custom_regex_validator_valid() {
let result = validators::validate_regex("abc123", r"^[a-z0-9]+$");
assert!(result.is_ok());
}
#[test]
fn test_custom_regex_validator_invalid() {
let result = validators::validate_regex("abc-123", r"^[a-z0-9]+$");
assert!(result.is_err());
}
#[test]
fn test_regex_cache_performance() {
let pattern = r"^\d{3}-\d{3}-\d{4}$";
let result1 = validators::validate_regex("123-456-7890", pattern);
let result2 = validators::validate_regex("987-654-3210", pattern);
assert!(result1.is_ok());
assert!(result2.is_ok());
}
#[test]
fn test_validate_range_integer_valid() {
let result = validators::validate_range(50, 0, 100);
assert!(result.is_ok());
}
#[test]
fn test_validate_range_integer_too_low() {
let result = validators::validate_range(-1, 0, 100);
assert!(result.is_err());
}
#[test]
fn test_validate_range_integer_too_high() {
let result = validators::validate_range(101, 0, 100);
assert!(result.is_err());
}
#[test]
fn test_validate_range_float() {
let result = validators::validate_range(0.5_f64, 0.0_f64, 1.0_f64);
assert!(result.is_ok());
}
#[test]
fn test_validate_length_string_valid() {
let result = validators::validate_length("hello", 1, 10);
assert!(result.is_ok());
}
#[test]
fn test_validate_length_string_too_short() {
let result = validators::validate_length("hi", 3, 10);
assert!(result.is_err());
}
#[test]
fn test_validate_length_string_too_long() {
let result = validators::validate_length("hello world", 1, 5);
assert!(result.is_err());
}
#[test]
fn test_validate_length_string_exact_min() {
let result = validators::validate_length("abc", 3, 10);
assert!(result.is_ok());
}
#[test]
fn test_validate_length_string_exact_max() {
let result = validators::validate_length("abcde", 1, 5);
assert!(result.is_ok());
}
#[test]
fn test_sanitize_length_validation() {
let result = sanitizer::validate_length("test", 1, 10, "test_field");
assert!(result.is_ok());
let result = sanitizer::validate_length("test", 5, 10, "test_field");
assert!(result.is_err());
}
#[test]
fn test_validation_errors_wrapper_new_empty() {
let wrapper = ValidationErrorsWrapper::new(vec![]);
assert!(wrapper.errors.is_empty());
}
#[test]
fn test_validation_errors_wrapper_new_multiple() {
let errors = vec![
FieldValidationError {
field: "email".to_string(),
constraints: vec!["email".to_string()],
},
FieldValidationError {
field: "name".to_string(),
constraints: vec!["length".to_string()],
},
];
let wrapper = ValidationErrorsWrapper::new(errors);
assert_eq!(wrapper.errors.len(), 2);
}
#[test]
fn test_field_validation_error_equality() {
let error1 = FieldValidationError {
field: "email".to_string(),
constraints: vec!["email".to_string()],
};
let error2 = FieldValidationError {
field: "email".to_string(),
constraints: vec!["email".to_string()],
};
assert_eq!(error1, error2);
}
#[test]
fn test_field_validation_error_clone() {
let error = FieldValidationError {
field: "password".to_string(),
constraints: vec!["min_length".to_string()],
};
let cloned = error.clone();
assert_eq!(error, cloned);
}
#[test]
fn test_validate_email_valid_with_subdomain() {
let result = validators::validate_email("user@mail.example.com");
assert!(result.is_ok());
}
#[test]
fn test_validate_email_valid_with_plus() {
let result = validators::validate_email("user+tag@example.com");
assert!(result.is_ok());
}
#[test]
fn test_validate_email_valid_with_dots() {
let result = validators::validate_email("first.last@example.com");
assert!(result.is_ok());
}
#[test]
fn test_validate_email_invalid_empty() {
let result = validators::validate_email("");
assert!(result.is_err());
}
#[test]
fn test_validate_email_invalid_no_at() {
let result = validators::validate_email("userexample.com");
assert!(result.is_err());
}
#[test]
fn test_validate_regex_phone_pattern() {
let result = validators::validate_regex("123-456-7890", r"^\d{3}-\d{3}-\d{4}$");
assert!(result.is_ok());
}
#[test]
fn test_validate_regex_phone_invalid() {
let result = validators::validate_regex("12-456-7890", r"^\d{3}-\d{3}-\d{4}$");
assert!(result.is_err());
}
#[test]
fn test_validate_regex_invalid_pattern() {
let result = validators::validate_regex("test", r"[invalid(");
assert!(result.is_err());
}
#[test]
fn test_validate_regex_empty_string() {
let result = validators::validate_regex("", r"^$");
assert!(result.is_ok());
}
#[test]
fn test_validate_regex_unicode() {
let result = validators::validate_regex("Hello 世界", r"[\w\s\u{4e00}-\u{9fff}]+");
assert!(result.is_ok());
}
#[test]
fn test_validate_regex_case_sensitive() {
let result = validators::validate_regex("ABC", r"^[a-z]+$");
assert!(result.is_err());
}
#[test]
fn test_validate_regex_case_insensitive() {
let result = validators::validate_regex("ABC", r"(?i)^[a-z]+$");
assert!(result.is_ok());
}
#[test]
fn test_validate_range_i8() {
let result = validators::validate_range(50_i8, 0_i8, 100_i8);
assert!(result.is_ok());
}
#[test]
fn test_validate_range_i16() {
let result = validators::validate_range(500_i16, 0_i16, 1000_i16);
assert!(result.is_ok());
}
#[test]
fn test_validate_range_i32() {
let result = validators::validate_range(50000_i32, 0_i32, 100000_i32);
assert!(result.is_ok());
}
#[test]
fn test_validate_range_i64() {
let result = validators::validate_range(5000000000_i64, 0_i64, 10000000000_i64);
assert!(result.is_ok());
}
#[test]
fn test_validate_range_u8() {
let result = validators::validate_range(128_u8, 0_u8, 255_u8);
assert!(result.is_ok());
}
#[test]
fn test_validate_range_u16() {
let result = validators::validate_range(30000_u16, 0_u16, 65535_u16);
assert!(result.is_ok());
}
#[test]
fn test_validate_range_u32() {
let result = validators::validate_range(1000000000_u32, 0_u32, 4000000000_u32);
assert!(result.is_ok());
}
#[test]
fn test_validate_range_u64() {
let result = validators::validate_range(5000000000_u64, 0_u64, 10000000000_u64);
assert!(result.is_ok());
}
#[test]
fn test_validate_range_f32() {
let result = validators::validate_range(0.5_f32, 0.0_f32, 1.0_f32);
assert!(result.is_ok());
}
#[test]
fn test_validate_range_negative() {
let result = validators::validate_range(-50, -100, -1);
assert!(result.is_ok());
}
#[test]
fn test_validate_range_negative_below_min() {
let result = validators::validate_range(-101, -100, -1);
assert!(result.is_err());
}
#[test]
fn test_validate_range_negative_above_max() {
let result = validators::validate_range(0, -100, -1);
assert!(result.is_err());
}
#[test]
fn test_validate_length_unicode() {
let result = validators::validate_length("世界", 1, 10);
assert!(result.is_ok());
}
#[test]
fn test_validate_length_emoji() {
let result = validators::validate_length("😀😁😂", 1, 10);
assert!(result.is_ok());
}
#[test]
fn test_validate_length_emoji_exact() {
let result = validators::validate_length("😀😁😂😃", 4, 4);
assert!(result.is_ok());
}
#[test]
fn test_validate_length_mixed_unicode() {
let result = validators::validate_length("Hello 世界 🌍", 1, 20);
assert!(result.is_ok());
}
#[test]
fn test_validate_length_whitespace() {
let result = validators::validate_length(" ", 1, 10);
assert!(result.is_ok());
}
#[test]
fn test_validate_length_newlines() {
let result = validators::validate_length("line1\nline2", 1, 20);
assert!(result.is_ok());
}
#[test]
fn test_validate_length_empty_min_zero() {
let result = validators::validate_length("", 0, 10);
assert!(result.is_ok());
}
#[test]
fn test_validate_length_very_long() {
let long_string = "a".repeat(1000);
let result = validators::validate_length(&long_string, 1, 100);
assert!(result.is_err());
}
#[test]
fn test_sanitize_xss_empty() {
let sanitized = sanitizer::sanitize_xss("");
assert_eq!(sanitized, "");
}
#[test]
fn test_sanitize_xss_event_handler() {
let input = "<div onclick=\"alert('xss')\">Click me</div>";
let sanitized = sanitizer::sanitize_xss(input);
assert!(!sanitized.contains("<div"));
}
#[test]
fn test_sanitize_xss_javascript_url() {
let input = "<a href=\"javascript:alert('xss')\">Click</a>";
let sanitized = sanitizer::sanitize_xss(input);
assert!(sanitized.contains("<a"));
assert!(sanitized.contains("""));
}
#[test]
fn test_sanitize_xss_unicode() {
let input = "Hello 世界";
let sanitized = sanitizer::sanitize_xss(input);
assert_eq!(sanitized, input);
}
#[test]
fn test_sanitize_xss_preserves_normal_text() {
let input = "This is normal text with numbers 123 and symbols !@#$%^&*()";
let sanitized = sanitizer::sanitize_xss(input);
assert_eq!(sanitized, input);
}
#[test]
fn test_sanitize_path_relative() {
let result = sanitizer::sanitize_path("home/user/documents");
assert!(result.is_ok());
}
#[test]
fn test_sanitize_path_filename() {
let result = sanitizer::sanitize_path("file.txt");
assert!(result.is_ok());
}
#[test]
fn test_sanitize_path_empty() {
let result = sanitizer::sanitize_path("");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "");
}
#[test]
fn test_sanitize_path_multiple_null_bytes() {
let result = sanitizer::sanitize_path("file\0\0\0.txt");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "file.txt");
}
#[test]
fn test_sanitize_path_hidden_file() {
let result = sanitizer::sanitize_path(".hidden_file");
assert!(result.is_ok());
}
#[test]
fn test_sanitize_path_unicode() {
let result = sanitizer::sanitize_path("/var/文档/文件.txt");
assert!(result.is_ok());
}
#[test]
fn test_sanitize_filename_valid_underscore() {
let result = sanitizer::sanitize_filename("my_document.pdf");
assert!(result.is_ok());
}
#[test]
fn test_sanitize_filename_valid_hyphen() {
let result = sanitizer::sanitize_filename("my-document.pdf");
assert!(result.is_ok());
}
#[test]
fn test_sanitize_filename_valid_spaces() {
let result = sanitizer::sanitize_filename("my document.pdf");
assert!(result.is_ok());
}
#[test]
fn test_sanitize_filename_removes_special() {
let result = sanitizer::sanitize_filename("file<>:\"|?*.txt");
assert!(result.is_ok());
let sanitized = result.unwrap();
assert!(!sanitized.contains('<'));
assert!(!sanitized.contains('>'));
}
#[test]
fn test_sanitize_filename_only_special_chars() {
let result = sanitizer::sanitize_filename("<>:\"|?*");
assert!(result.is_err());
}
#[test]
fn test_sanitize_filename_unicode() {
let result = sanitizer::sanitize_filename("文档.pdf");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "文档.pdf");
}
#[test]
fn test_sanitize_filename_multiple_dots() {
let result = sanitizer::sanitize_filename("file.name.tar.gz");
assert!(result.is_ok());
}
#[test]
fn test_sanitize_filename_hidden() {
let result = sanitizer::sanitize_filename(".hidden");
assert!(result.is_ok());
}
#[test]
fn test_sanitize_filename_null_byte() {
let result = sanitizer::sanitize_filename("file\0.txt");
assert!(result.is_ok());
let sanitized = result.unwrap();
assert!(!sanitized.contains('\0'));
}
#[test]
fn test_validate_user_id_one() {
let result = sanitizer::validate_user_id(1);
assert!(result.is_ok());
}
#[test]
fn test_validate_user_id_max() {
let result = sanitizer::validate_user_id(i64::MAX);
assert!(result.is_ok());
}
#[test]
fn test_validate_user_id_zero() {
let result = sanitizer::validate_user_id(0);
assert!(result.is_err());
}
#[test]
fn test_validate_user_id_negative() {
let result = sanitizer::validate_user_id(-1);
assert!(result.is_err());
}
#[test]
fn test_validate_user_id_min() {
let result = sanitizer::validate_user_id(i64::MIN);
assert!(result.is_err());
}
#[test]
fn test_validate_not_empty_with_spaces() {
let result = sanitizer::validate_not_empty(" hello ", "field");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "hello");
}
#[test]
fn test_validate_not_empty_whitespace_only() {
let result = sanitizer::validate_not_empty(" ", "field");
assert!(result.is_err());
}
#[test]
fn test_validate_not_empty_tabs_only() {
let result = sanitizer::validate_not_empty("\t\t", "field");
assert!(result.is_err());
}
#[test]
fn test_validate_not_empty_newlines_only() {
let result = sanitizer::validate_not_empty("\n\n", "field");
assert!(result.is_err());
}
#[test]
fn test_validate_not_empty_preserves_inner_spaces() {
let result = sanitizer::validate_not_empty(" hello world ", "field");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "hello world");
}
#[test]
fn test_validate_not_empty_unicode() {
let result = sanitizer::validate_not_empty(" 世界 ", "field");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "世界");
}
#[test]
fn test_sanitizer_validate_length_exact_min() {
let result = sanitizer::validate_length("abc", 3, 10, "field");
assert!(result.is_ok());
}
#[test]
fn test_sanitizer_validate_length_exact_max() {
let result = sanitizer::validate_length("abcde", 1, 5, "field");
assert!(result.is_ok());
}
#[test]
fn test_sanitizer_validate_length_too_short() {
let result = sanitizer::validate_length("ab", 3, 10, "field");
assert!(result.is_err());
}
#[test]
fn test_sanitizer_validate_length_too_long() {
let result = sanitizer::validate_length("abcdefghijk", 1, 10, "field");
assert!(result.is_err());
}
#[test]
fn test_sanitizer_validate_length_invalid_params() {
let result = sanitizer::validate_length("test", 10, 1, "field");
assert!(result.is_err());
}
#[test]
fn test_sanitizer_validate_length_zero_to_zero() {
let result = sanitizer::validate_length("", 0, 0, "field");
assert!(result.is_ok());
}
#[test]
fn test_sanitizer_validate_length_preserves_original() {
let input = " hello ";
let result = sanitizer::validate_length(input, 1, 20, "field");
assert!(result.is_ok());
assert_eq!(result.unwrap(), " hello ");
}
#[test]
fn test_validate_email_format_valid() {
let result = sanitizer::validate_email_format("user@example.com");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "user@example.com");
}
#[test]
fn test_validate_email_format_trims() {
let result = sanitizer::validate_email_format(" user@example.com ");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "user@example.com");
}
#[test]
fn test_validate_email_format_missing_at() {
let result = sanitizer::validate_email_format("userexample.com");
assert!(result.is_err());
}
#[test]
fn test_validate_email_format_missing_dot() {
let result = sanitizer::validate_email_format("user@examplecom");
assert!(result.is_err());
}
#[test]
fn test_validate_email_format_starts_with_at() {
let result = sanitizer::validate_email_format("@example.com");
assert!(result.is_err());
}
#[test]
fn test_validate_email_format_ends_with_at() {
let result = sanitizer::validate_email_format("user@");
assert!(result.is_err());
}
#[test]
fn test_validate_email_format_empty() {
let result = sanitizer::validate_email_format("");
assert!(result.is_err());
}
#[test]
fn test_validate_email_format_with_subdomain() {
let result = sanitizer::validate_email_format("user@mail.example.com");
assert!(result.is_ok());
}
#[test]
fn test_validate_email_format_with_plus() {
let result = sanitizer::validate_email_format("user+tag@example.com");
assert!(result.is_ok());
}
#[test]
#[allow(clippy::assertions_on_constants)]
fn test_security_limits_constants_defined() {
assert!(MAX_REQUEST_BODY_SIZE > 0);
assert!(MAX_HEADER_NAME_LENGTH > 0);
assert!(MAX_HEADER_VALUE_LENGTH > 0);
assert!(MAX_URI_PATH_LENGTH > 0);
assert!(MAX_QUERY_STRING_LENGTH > 0);
assert!(MAX_HEADER_COUNT > 0);
assert!(MAX_API_KEY_LENGTH > 0);
assert!(MAX_JWT_TOKEN_LENGTH > 0);
assert!(MAX_USERNAME_LENGTH > 0);
assert!(MAX_EMAIL_LENGTH > 0);
assert!(MAX_PASSWORD_LENGTH > 0);
assert!(MIN_PASSWORD_LENGTH > 0);
assert!(MAX_TEXT_FIELD_LENGTH > 0);
assert!(MAX_JSON_FIELD_NAME_LENGTH > 0);
assert!(MAX_JSON_ARRAY_LENGTH > 0);
assert!(MAX_JSON_DEPTH > 0);
}
#[test]
#[allow(clippy::assertions_on_constants)]
fn test_security_limits_reasonable_values() {
assert!(MAX_REQUEST_BODY_SIZE >= 1024 * 1024); assert!(MAX_REQUEST_BODY_SIZE <= 100 * 1024 * 1024);
assert!(MIN_PASSWORD_LENGTH >= 6); assert!(MAX_PASSWORD_LENGTH >= 128);
assert_eq!(MAX_EMAIL_LENGTH, 320);
assert!(MAX_HEADER_NAME_LENGTH >= 64);
assert!(MAX_HEADER_VALUE_LENGTH >= 1024);
assert!(MAX_JSON_ARRAY_LENGTH >= 1000);
assert!(MAX_JSON_DEPTH >= 50);
}
#[test]
#[allow(clippy::assertions_on_constants)]
fn test_password_length_validation() {
assert!(MIN_PASSWORD_LENGTH < MAX_PASSWORD_LENGTH);
assert!("password123".len() >= MIN_PASSWORD_LENGTH);
assert!("password123".len() <= MAX_PASSWORD_LENGTH);
}
#[test]
fn test_from_validation_errors_wrapper_with_errors() {
let errors = vec![FieldValidationError {
field: "email".to_string(),
constraints: vec!["email".to_string()],
}];
let wrapper = ValidationErrorsWrapper::new(errors);
let api_error: ApiError = wrapper.into();
match api_error {
ApiError::ValidationError { field, constraint } => {
assert_eq!(field, "email");
assert_eq!(constraint, "email");
}
_ => panic!("Expected ValidationError variant"),
}
}
#[test]
fn test_from_validation_errors_wrapper_with_empty_constraints() {
let errors = vec![FieldValidationError {
field: "name".to_string(),
constraints: vec![],
}];
let wrapper = ValidationErrorsWrapper::new(errors);
let api_error: ApiError = wrapper.into();
match api_error {
ApiError::ValidationError { field, constraint } => {
assert_eq!(field, "name");
assert_eq!(constraint, "invalid");
}
_ => panic!("Expected ValidationError variant"),
}
}
#[test]
fn test_from_validation_errors_wrapper_empty() {
let wrapper = ValidationErrorsWrapper::new(vec![]);
let api_error: ApiError = wrapper.into();
match api_error {
ApiError::InvalidInput {
message,
field,
value,
} => {
assert!(message.contains("Validation failed"));
assert!(field.is_none());
assert!(value.is_none());
}
_ => panic!("Expected InvalidInput variant"),
}
}
#[test]
fn test_validate_or_error_success() {
let result: Result<(), ApiError> = validators::validate_or_error(
|| Ok(()),
|| ValidationErrorsWrapper::new(vec![]).into(),
);
assert!(result.is_ok());
}
#[test]
fn test_validate_or_error_failure() {
let result: Result<(), ApiError> = validators::validate_or_error(
|| Err(validator::ValidationError::new("test")),
|| ValidationErrorsWrapper::new(vec![]).into(),
);
assert!(result.is_err());
}
#[test]
fn test_sanitize_filename_empty_input() {
let result = sanitizer::sanitize_filename("");
assert!(result.is_err());
match result {
Err(ApiError::InvalidInput { message, .. }) => {
assert!(message.contains("cannot be empty"));
}
_ => panic!("Expected InvalidInput error for empty filename"),
}
}
#[test]
fn test_sanitize_filename_only_invalid_chars() {
let result = sanitizer::sanitize_filename("@#$%^&*()");
assert!(result.is_err());
match result {
Err(ApiError::InvalidInput { message, .. }) => {
assert!(message.contains("only invalid characters"));
}
_ => panic!("Expected InvalidInput error for all-invalid filename"),
}
}
#[test]
fn test_sanitize_filename_with_path_separators() {
let result = sanitizer::sanitize_filename("valid/part");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "validpart");
}
#[test]
fn test_sanitize_filename_with_backslash_separator() {
let result = sanitizer::sanitize_filename("valid\\part");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "validpart");
}
#[test]
fn test_sanitize_filename_valid_input() {
let result = sanitizer::sanitize_filename("document.txt");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "document.txt");
}
#[test]
fn test_sanitize_filename_with_spaces_and_dashes() {
let result = sanitizer::sanitize_filename("my-file_v1.2.pdf");
assert!(result.is_ok());
assert_eq!(result.unwrap(), "my-file_v1.2.pdf");
}
#[tokio::test]
async fn test_extract_validated_deserialization_failure_wrong_type() {
let json = serde_json::json!({
"name": "John",
"email": "john@example.com",
"age": "not a number"
});
let result = extract_validated::<TestParams>(&json).await;
assert!(
result.is_err(),
"Deserialization failure should produce Err"
);
let errors = result.unwrap_err();
assert!(
errors.errors.is_empty(),
"Deserialization errors produce empty errors vec"
);
}
#[tokio::test]
async fn test_extract_validated_deserialization_failure_missing_field() {
let json = serde_json::json!({
"name": "John",
"email": "john@example.com"
});
let result = extract_validated::<TestParams>(&json).await;
assert!(
result.is_err(),
"Missing field should produce deserialization Err"
);
}
#[tokio::test]
async fn test_extract_validated_deserialization_failure_non_object() {
let json = serde_json::json!([1, 2, 3]);
let result = extract_validated::<TestParams>(&json).await;
assert!(
result.is_err(),
"Non-object JSON should produce deserialization Err"
);
}
}