From 52a32cfb1f2029f765cfaefcca1086622519f2f5 Mon Sep 17 00:00:00 2001 From: slhx agent Date: Fri, 17 Jul 2026 00:36:04 +0200 Subject: [PATCH] test(axum): harden page and media extraction Prove explicit partial header values, full-page body and fingerprint delivery, and case-insensitive urlencoded media types through public Axum extractors and responses. req: page_swap/001 req: abi/005 req: security/003 --- PLAN.md | 2 +- hemx-axum/tests/response.rs | 49 +++++++++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/PLAN.md b/PLAN.md index 111cb95..281b91a 100644 --- a/PLAN.md +++ b/PLAN.md @@ -21,7 +21,7 @@ - [ ] **State:** In progress — the package-native capped xtask entry point is reachable, rejects unknown packages, propagates mutest failure, and mutation-tests `hemx-core`, `hemx-js`, and the full `hemx-test` package cleanly; full package closure remains. - **User value:** maintainers can run one bounded repository command and trust that meaningful Rust logic across every mutation-applicable library is either killed or explicitly justified. - **Build:** add a capped `hemx-xtask` mutation command that invokes `/opt/repositories/mutest`/`mutest` through package-native test targets rather than the broken workspace-wide example path; enumerate only current mutation-applicable library/proc-macro packages; finish adversarial tests or simplify code until every survivor is classified; keep equivalent, invariant-only, and infrastructure-inapplicable classifications inspectable and minimal; document the exact local release command in the existing readiness surface. -- **Blocked by:** none; broad survivors currently remain in `hemx-axum`, `hemx-build`, `hemx-derive`, and `hemx-lsp` outside already-clean focused contracts; the current `hemx-axum` frontier now proves page-mode, response constructors, form accessors/rejections, media-type limits, multipart success/error semantics, sync/async registry dispatch, effect/rejection responses, and embedded runtime delivery mutation-clean; remaining integration paths remain. +- **Blocked by:** none; broad survivors currently remain in `hemx-axum`, `hemx-build`, `hemx-derive`, and `hemx-lsp` outside already-clean focused contracts; the current `hemx-axum` frontier now proves page-mode, response constructors, form accessors/rejections, media-type limits, multipart success/error semantics, sync/async registry dispatch, effect/rejection responses, and embedded runtime delivery mutation-clean; page extraction/response and media-type integration paths are now mutation-clean; remaining app/transport integration paths remain. - **Proof:** the new xtask mutation command exits zero within its documented bound, covers each applicable package, emits no unexplained missed mutant, and a deliberate adjacent mutation makes it fail. `cargo run -p hemx-xtask -- test` remains green. req: test/020 req: test/021 ## 3. Elect and enforce the release license policy diff --git a/hemx-axum/tests/response.rs b/hemx-axum/tests/response.rs index d0276ad..11c057e 100644 --- a/hemx-axum/tests/response.rs +++ b/hemx-axum/tests/response.rs @@ -91,7 +91,7 @@ async fn interaction_boundary_honors_media_type_and_host_body_limit() { Request::post("/mutate") .header( header::CONTENT_TYPE, - "application/x-www-form-urlencoded; charset=utf-8", + "Application/X-Www-Form-Urlencoded; charset=UTF-8", ) .body(Body::from("__h=1")) .unwrap(), @@ -235,12 +235,26 @@ impl hemx_core::FromForm for OpenProject { } #[test] -fn page_mode_detects_partial_header() { - let mut headers = HeaderMap::new(); - assert_eq!(PageMode::from_headers(&headers), PageMode::Full); - - headers.insert(HEMX_PARTIAL_HEADER, "true".parse().unwrap()); - assert_eq!(PageMode::from_headers(&headers), PageMode::Partial); +fn page_mode_detects_only_explicit_partial_header_values() { + for value in [ + None, + Some("false"), + Some("0"), + Some("TRUE"), + Some("invalid"), + ] { + let mut headers = HeaderMap::new(); + if let Some(value) = value { + headers.insert(HEMX_PARTIAL_HEADER, value.parse().unwrap()); + } + assert_eq!(PageMode::from_headers(&headers), PageMode::Full); + } + for value in ["true", "1"] { + let mut headers = HeaderMap::new(); + headers.insert(HEMX_PARTIAL_HEADER, value.parse().unwrap()); + assert_eq!(PageMode::from_headers(&headers), PageMode::Partial); + } + // test req: page_swap/001 } #[test] @@ -365,6 +379,27 @@ fn page_response_constructors_preserve_mode_and_optional_fingerprint() { // test req: page_swap/001 req: abi/005 } +#[tokio::test] +async fn full_page_response_preserves_html_and_fingerprint_without_partial_headers() { + let response = PageResponse::full("Full") + .fingerprint(BuildFingerprint(42)) + .into_response(); + + assert_eq!(response.status(), StatusCode::OK); + assert_eq!( + response.headers()[header::CONTENT_TYPE], + "text/html; charset=utf-8" + ); + assert_eq!(response.headers()[HEMX_FINGERPRINT_HEADER], "42"); + assert!(!response.headers().contains_key(HEMX_PARTIAL_HEADER)); + assert!(!response.headers().contains_key(HEMX_TITLE_HEADER)); + assert_eq!( + to_bytes(response.into_body(), 1024).await.unwrap().as_ref(), + b"Full" + ); + // test req: page_swap/001 req: abi/005 +} + #[test] fn partial_page_response_sets_partial_and_title_headers() { let response = PageResponse::partial("
Docs
")