Skip to content

Commit 49dab10

Browse files
committed
Add testcase
1 parent 87e08a1 commit 49dab10

2 files changed

Lines changed: 257 additions & 0 deletions

File tree

crates/vespera_macro/src/multipart_impl.rs

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -984,4 +984,203 @@ mod tests {
984984
"non-strict should not check for unknown fields"
985985
);
986986
}
987+
988+
// ─── process_fields direct tests ────────────────────────────────────
989+
//
990+
// Exercise process_fields directly to ensure quote! token construction
991+
// for each branch (parse_value, strict assignment, field matching) is
992+
// fully traced by the coverage tool.
993+
994+
fn parse_fields_from(code: &str) -> syn::DeriveInput {
995+
syn::parse_str(code).unwrap()
996+
}
997+
998+
fn get_named_fields(
999+
input: &syn::DeriveInput,
1000+
) -> &syn::punctuated::Punctuated<syn::Field, syn::token::Comma> {
1001+
match &input.data {
1002+
syn::Data::Struct(s) => match &s.fields {
1003+
Fields::Named(n) => &n.named,
1004+
_ => panic!("expected named fields"),
1005+
},
1006+
_ => panic!("expected struct"),
1007+
}
1008+
}
1009+
1010+
#[test]
1011+
fn test_process_fields_required_field_generates_parse_value() {
1012+
let input = parse_fields_from("struct T { pub name: String }");
1013+
let fields = get_named_fields(&input);
1014+
let cg = process_fields(fields.iter(), None, false, false);
1015+
1016+
// parse_value is interpolated into each assignment
1017+
let assignment_code = cg
1018+
.assignments
1019+
.iter()
1020+
.map(ToString::to_string)
1021+
.collect::<Vec<_>>()
1022+
.join(" ");
1023+
assert!(
1024+
assignment_code.contains("TryFromFieldWithState"),
1025+
"parse_value should contain turbofish call"
1026+
);
1027+
assert!(
1028+
assignment_code.contains("try_from_field_with_state"),
1029+
"should call try_from_field_with_state"
1030+
);
1031+
assert!(
1032+
assignment_code.contains("\"name\""),
1033+
"should match on field name"
1034+
);
1035+
1036+
// post_loop should have MissingField check for required fields
1037+
let post_code = cg
1038+
.post_loop
1039+
.iter()
1040+
.map(ToString::to_string)
1041+
.collect::<Vec<_>>()
1042+
.join(" ");
1043+
assert!(
1044+
post_code.contains("MissingField"),
1045+
"required field should have MissingField check"
1046+
);
1047+
}
1048+
1049+
#[test]
1050+
fn test_process_fields_strict_required_field_generates_duplicate_check() {
1051+
let input = parse_fields_from("struct T { pub name: String, pub age: i32 }");
1052+
let fields = get_named_fields(&input);
1053+
let cg = process_fields(fields.iter(), None, true, false);
1054+
1055+
// strict mode: assignments should contain is_none + DuplicateField check
1056+
let assignment_code = cg
1057+
.assignments
1058+
.iter()
1059+
.map(ToString::to_string)
1060+
.collect::<Vec<_>>()
1061+
.join(" ");
1062+
assert!(
1063+
assignment_code.contains("is_none"),
1064+
"strict assignment should check is_none"
1065+
);
1066+
assert!(
1067+
assignment_code.contains("DuplicateField"),
1068+
"strict assignment should have DuplicateField"
1069+
);
1070+
assert!(
1071+
assignment_code.contains("\"name\""),
1072+
"should match name field"
1073+
);
1074+
assert!(
1075+
assignment_code.contains("\"age\""),
1076+
"should match age field"
1077+
);
1078+
1079+
// Both fields should have parse_value with turbofish
1080+
assert!(
1081+
assignment_code.contains("TryFromFieldWithState"),
1082+
"should contain turbofish"
1083+
);
1084+
}
1085+
1086+
#[test]
1087+
fn test_process_fields_vec_field_generates_push() {
1088+
let input = parse_fields_from("struct T { pub tags: Vec<String> }");
1089+
let fields = get_named_fields(&input);
1090+
let cg = process_fields(fields.iter(), None, false, false);
1091+
1092+
let decl_code = cg
1093+
.declarations
1094+
.iter()
1095+
.map(ToString::to_string)
1096+
.collect::<Vec<_>>()
1097+
.join(" ");
1098+
assert!(
1099+
decl_code.contains("Vec :: new"),
1100+
"Vec field should initialize with Vec::new()"
1101+
);
1102+
1103+
let assignment_code = cg
1104+
.assignments
1105+
.iter()
1106+
.map(ToString::to_string)
1107+
.collect::<Vec<_>>()
1108+
.join(" ");
1109+
assert!(
1110+
assignment_code.contains("push"),
1111+
"Vec field assignment should use push"
1112+
);
1113+
1114+
// Vec fields should NOT have post_loop (no MissingField check)
1115+
assert!(
1116+
cg.post_loop.is_empty(),
1117+
"Vec fields should not have post-loop checks"
1118+
);
1119+
}
1120+
1121+
#[test]
1122+
fn test_process_fields_option_field_no_missing_check() {
1123+
let input = parse_fields_from("struct T { pub bio: Option<String> }");
1124+
let fields = get_named_fields(&input);
1125+
let cg = process_fields(fields.iter(), None, false, false);
1126+
1127+
let decl_code = cg
1128+
.declarations
1129+
.iter()
1130+
.map(ToString::to_string)
1131+
.collect::<Vec<_>>()
1132+
.join(" ");
1133+
assert!(
1134+
decl_code.contains("Option :: None"),
1135+
"Option field should initialize to None"
1136+
);
1137+
1138+
// Option fields should NOT have post_loop
1139+
assert!(
1140+
cg.post_loop.is_empty(),
1141+
"Option fields should not have post-loop checks"
1142+
);
1143+
}
1144+
1145+
#[test]
1146+
fn test_process_fields_strict_vec_field_uses_push_not_duplicate() {
1147+
let input = parse_fields_from("struct T { pub tags: Vec<String> }");
1148+
let fields = get_named_fields(&input);
1149+
let cg = process_fields(fields.iter(), None, true, false);
1150+
1151+
// Even in strict mode, Vec fields use push (not duplicate check)
1152+
let assignment_code = cg
1153+
.assignments
1154+
.iter()
1155+
.map(ToString::to_string)
1156+
.collect::<Vec<_>>()
1157+
.join(" ");
1158+
assert!(
1159+
assignment_code.contains("push"),
1160+
"Vec in strict mode should still use push"
1161+
);
1162+
assert!(
1163+
!assignment_code.contains("DuplicateField"),
1164+
"Vec should not have duplicate check"
1165+
);
1166+
}
1167+
1168+
#[test]
1169+
fn test_process_fields_mixed_types() {
1170+
let input = parse_fields_from(
1171+
"struct T { pub name: String, pub tags: Vec<String>, pub bio: Option<String> }",
1172+
);
1173+
let fields = get_named_fields(&input);
1174+
let cg = process_fields(fields.iter(), None, false, false);
1175+
1176+
assert_eq!(cg.idents.len(), 3, "should have 3 fields");
1177+
assert_eq!(cg.declarations.len(), 3, "should have 3 declarations");
1178+
assert_eq!(cg.assignments.len(), 3, "should have 3 assignments");
1179+
// Only 'name' is required (not Option, not Vec), so 1 post_loop
1180+
assert_eq!(
1181+
cg.post_loop.len(),
1182+
1,
1183+
"only required field should have post-loop"
1184+
);
1185+
}
9871186
}

examples/axum-example/tests/integration_test.rs

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1743,3 +1743,61 @@ async fn test_struct_level_serde_default_all_provided() {
17431743
assert_eq!(result["count"], 99);
17441744
assert_eq!(result["active"], true);
17451745
}
1746+
1747+
// ============== Multipart error path coverage tests ==========================
1748+
//
1749+
// These tests trigger real axum MultipartRejection / MultipartError paths
1750+
// to cover From impls and Display arms for InvalidRequest/InvalidRequestBody.
1751+
1752+
#[tokio::test]
1753+
async fn test_multipart_rejection_non_multipart_content_type() {
1754+
// Sending JSON to a multipart handler triggers MultipartRejection → InvalidRequest
1755+
let server = TestServer::new(create_coverage_test_app());
1756+
1757+
let response = server
1758+
.post("/strict-test")
1759+
.content_type("application/json")
1760+
.bytes(b"{\"name\":\"x\",\"age\":1}".to_vec().into())
1761+
.await;
1762+
1763+
response.assert_status(axum::http::StatusCode::BAD_REQUEST);
1764+
let body = response.text();
1765+
assert!(
1766+
body.contains("Invalid multipart request"),
1767+
"Should use InvalidRequest Display: {body}"
1768+
);
1769+
}
1770+
1771+
#[tokio::test]
1772+
async fn test_multipart_rejection_missing_content_type() {
1773+
// Sending raw bytes with no content type triggers MultipartRejection
1774+
let server = TestServer::new(create_coverage_test_app());
1775+
1776+
let response = server
1777+
.post("/vec-test")
1778+
.bytes(b"not multipart".to_vec().into())
1779+
.await;
1780+
1781+
// axum rejects with 4xx because there's no multipart content-type
1782+
assert!(
1783+
response.status_code().is_client_error(),
1784+
"Should be a client error, got {}",
1785+
response.status_code()
1786+
);
1787+
}
1788+
1789+
#[tokio::test]
1790+
async fn test_numeric_field_non_utf8_bytes() {
1791+
// Send non-UTF-8 bytes for a numeric (i32) field → WrongFieldType from from_utf8 error
1792+
let server = TestServer::new(create_coverage_test_app());
1793+
1794+
let invalid_utf8 = Part::bytes(vec![0xFF, 0xFE, 0xFD]).file_name("bad.bin");
1795+
let form = MultipartForm::new()
1796+
.add_text("name", "Alice")
1797+
.add_part("count", invalid_utf8)
1798+
.add_text("score", "1.0")
1799+
.add_text("initial", "A");
1800+
1801+
let response = server.post("/numeric-char-test").multipart(form).await;
1802+
response.assert_status(axum::http::StatusCode::UNSUPPORTED_MEDIA_TYPE);
1803+
}

0 commit comments

Comments
 (0)