use std::borrow::Cow;
use super::error::MAX_REFLECTED_VALUE_CHARS;
use super::*;
use axum::http::StatusCode;
use axum::response::IntoResponse;
#[test]
fn test_str_to_bool_truthy() {
for val in &[
"true", "True", "TRUE", "yes", "Yes", "y", "Y", "1", "on", "ON",
] {
assert_eq!(str_to_bool(val), Some(true), "expected true for `{val}`");
}
}
#[test]
fn test_str_to_bool_falsy() {
for val in &[
"false", "False", "FALSE", "no", "No", "n", "N", "0", "off", "OFF",
] {
assert_eq!(str_to_bool(val), Some(false), "expected false for `{val}`");
}
}
#[test]
fn test_str_to_bool_invalid() {
for val in &["maybe", "2", "", "yep", "nah"] {
assert_eq!(str_to_bool(val), None, "expected None for `{val}`");
}
}
#[test]
fn test_str_to_bool_trims_surrounding_whitespace() {
assert_eq!(str_to_bool(" true "), Some(true));
assert_eq!(str_to_bool("true\n"), Some(true));
assert_eq!(str_to_bool("\tyes\r\n"), Some(true));
assert_eq!(str_to_bool(" false"), Some(false));
assert_eq!(str_to_bool("off\n"), Some(false));
assert_eq!(str_to_bool("tr ue"), None);
assert_eq!(str_to_bool(" "), None);
}
#[test]
fn field_metadata_full_headers_are_optional_by_default() {
let metadata = FieldMetadata {
name: Some("file".to_owned()),
file_name: Some("data.bin".to_owned()),
content_type: Some("application/octet-stream".to_owned()),
headers: None,
};
assert!(metadata.headers().is_none());
}
#[test]
fn multipart_default_limits_can_be_reapplied_without_drift() {
let original = default_multipart_limits();
let previous = set_default_multipart_limits(original);
assert_eq!(previous, original);
assert_eq!(default_multipart_limits(), original);
}
#[test]
fn temp_file_default_limit_is_bounded_and_configurable() {
assert_eq!(
default_temp_file_field_limit_bytes(),
DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES
);
assert_eq!(DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES, 16 * 1024 * 1024);
let previous = set_default_temp_file_field_limit_bytes(2 * 1024 * 1024);
assert_eq!(previous, DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES);
assert_eq!(default_temp_file_field_limit_bytes(), 2 * 1024 * 1024);
let restored = set_default_temp_file_field_limit_bytes(previous);
assert_eq!(restored, 2 * 1024 * 1024);
assert_eq!(
default_temp_file_field_limit_bytes(),
DEFAULT_TEMP_FILE_FIELD_LIMIT_BYTES
);
}
#[test]
fn register_multipart_bytes_lets_custom_parsers_enforce_aggregate_cap() {
let rt = tokio::runtime::Builder::new_current_thread()
.build()
.expect("current-thread runtime");
let outcome = rt.block_on(async {
let limits = MultipartLimits::new(10, DEFAULT_MULTIPART_MAX_FIELDS);
MULTIPART_AGGREGATE
.scope(RefCell::new(MultipartAggregateState::new(limits)), async {
register_multipart_bytes("custom", 4)?;
register_multipart_bytes("custom", 4)?;
register_multipart_bytes("custom", 4)
})
.await
});
assert!(
matches!(
outcome,
Err(TypedMultipartError::RequestTooLarge {
limit_bytes: 10,
..
})
),
"custom-parser byte accounting must trip the aggregate cap, got {outcome:?}"
);
assert!(register_multipart_bytes("custom", usize::MAX).is_ok());
}
#[test]
fn test_error_display() {
let err = TypedMultipartError::MissingField {
field_name: "name".to_string(),
};
assert_eq!(err.to_string(), "Missing field: `name`");
let err = TypedMultipartError::FieldTooLarge {
field_name: "file".to_string(),
limit_bytes: 1024,
};
assert_eq!(
err.to_string(),
"Field `file` exceeds size limit of 1024 bytes"
);
let err = TypedMultipartError::WrongFieldType {
field_name: "age".to_string(),
wanted: Cow::Borrowed("i32"),
source: "invalid digit".to_string(),
};
assert_eq!(
err.to_string(),
"Wrong type for field `age` (expected i32): invalid digit"
);
}
#[test]
fn test_error_display_duplicate_field() {
let err = TypedMultipartError::DuplicateField {
field_name: "email".to_string(),
};
assert_eq!(err.to_string(), "Duplicate field: `email`");
}
#[test]
fn other_error_body_hides_internal_source() {
let err = TypedMultipartError::Other {
source: "/tmp/vespera-upload-7f3a.part: No such file or directory".to_string(),
};
let body = String::from_utf8(err.error_body()).expect("envelope is UTF-8");
assert_eq!(
body,
r#"{"errors":[{"message":"internal error while processing multipart request","path":""}]}"#
);
assert!(
!body.contains("/tmp/"),
"internal source path leaked into response body"
);
assert!(err.to_string().contains("/tmp/"));
let missing = TypedMultipartError::MissingField {
field_name: "avatar".to_string(),
};
let missing_body = String::from_utf8(missing.error_body()).expect("envelope is UTF-8");
assert_eq!(
missing_body,
r#"{"errors":[{"message":"Missing field: `avatar`","path":"avatar"}]}"#
);
}
#[test]
fn test_error_display_unknown_field() {
let err = TypedMultipartError::UnknownField {
field_name: "foo".to_string(),
};
assert_eq!(err.to_string(), "Unknown field: `foo`");
}
#[test]
fn test_error_display_invalid_enum_value() {
let err = TypedMultipartError::InvalidEnumValue {
field_name: "status".to_string(),
value: "maybe".to_string(),
};
assert_eq!(
err.to_string(),
"Invalid enum value `maybe` for field `status`"
);
}
#[test]
fn invalid_enum_value_constructor_stores_bounded_value() {
let oversized = "가".repeat(MAX_REFLECTED_VALUE_CHARS * 4);
let err = TypedMultipartError::invalid_enum_value("status".to_string(), &oversized);
match err {
TypedMultipartError::InvalidEnumValue { value, .. } => {
assert!(value.ends_with("... (truncated)"));
assert!(
value.chars().count()
<= MAX_REFLECTED_VALUE_CHARS + "... (truncated)".chars().count()
);
assert!(value.chars().count() < oversized.chars().count());
}
_ => panic!("expected InvalidEnumValue"),
}
}
#[test]
fn invalid_bool_message_reflects_bounded_value() {
let oversized = "x".repeat(MAX_REFLECTED_VALUE_CHARS + 10);
let message = format!(
"invalid boolean value: `{}`",
truncate_reflected_value(&oversized)
);
assert!(message.contains("... (truncated)"));
assert!(!message.contains(&oversized));
}
#[test]
fn test_error_display_nameless_field() {
let err = TypedMultipartError::NamelessField;
assert_eq!(err.to_string(), "Encountered a field without a name");
}
#[test]
fn test_error_display_other() {
let err = TypedMultipartError::Other {
source: "something went wrong".to_string(),
};
assert_eq!(err.to_string(), "something went wrong");
}
#[test]
fn test_into_response_duplicate_field() {
let err = TypedMultipartError::DuplicateField {
field_name: "x".to_string(),
};
let resp = err.into_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn test_into_response_unknown_field() {
let err = TypedMultipartError::UnknownField {
field_name: "x".to_string(),
};
let resp = err.into_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn test_into_response_invalid_enum_value() {
let err = TypedMultipartError::InvalidEnumValue {
field_name: "x".to_string(),
value: "bad".to_string(),
};
let resp = err.into_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn test_into_response_nameless_field() {
let err = TypedMultipartError::NamelessField;
let resp = err.into_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn test_into_response_wrong_field_type() {
let err = TypedMultipartError::WrongFieldType {
field_name: "age".to_string(),
wanted: Cow::Borrowed("i32"),
source: "err".to_string(),
};
let resp = err.into_response();
assert_eq!(resp.status(), StatusCode::UNPROCESSABLE_ENTITY);
}
#[test]
fn test_into_response_field_too_large() {
let err = TypedMultipartError::FieldTooLarge {
field_name: "file".to_string(),
limit_bytes: 100,
};
let resp = err.into_response();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
}
#[test]
fn test_into_response_other() {
let err = TypedMultipartError::Other {
source: "err".to_string(),
};
let resp = err.into_response();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
}
#[test]
fn test_into_response_missing_field() {
let err = TypedMultipartError::MissingField {
field_name: "x".to_string(),
};
let resp = err.into_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
}
#[test]
fn test_error_trait_is_implemented() {
let err: Box<dyn std::error::Error> = Box::new(TypedMultipartError::Other {
source: "test".to_string(),
});
assert_eq!(err.to_string(), "test");
assert!(std::error::Error::source(err.as_ref()).is_none());
}
#[test]
fn test_typed_multipart_deref() {
let tm = TypedMultipart("hello".to_string());
assert_eq!(&*tm, "hello");
assert_eq!(tm.len(), 5); }
#[test]
fn test_typed_multipart_deref_mut() {
let mut tm = TypedMultipart(vec![1, 2, 3]);
tm.push(4);
assert_eq!(&*tm, &[1, 2, 3, 4]);
}