test(axum): close package mutation gate

Exercise every state, typed-form, async, and result registration family; replace fallible numeric headers with typed HeaderValue construction; and classify only validated equivalent multipart/header mutants.

req: axum_integration/003

req: derive_handler/005

req: multipart/003

req: test/020

req: test/021

req: test/022
This commit is contained in:
slhx agent
2026-07-17 01:50:16 +02:00
parent 8aeab5fca1
commit 89747c8c48
4 changed files with 168 additions and 14 deletions
+9 -11
View File
@@ -1347,10 +1347,10 @@ impl IntoResponse for PageResponse {
.headers_mut()
.insert(HEMX_PARTIAL_HEADER, HeaderValue::from_static("true"));
}
if let Some(fingerprint) = self.fingerprint.and_then(fingerprint_header) {
if let Some(fingerprint) = self.fingerprint {
response
.headers_mut()
.insert(HEMX_FINGERPRINT_HEADER, fingerprint);
.insert(HEMX_FINGERPRINT_HEADER, fingerprint_header(fingerprint));
}
if let Some(title) = self
.title
@@ -1380,8 +1380,8 @@ fn html_with_root_fingerprint(mut html: String, fingerprint: BuildFingerprint) -
html
}
fn fingerprint_header(fingerprint: BuildFingerprint) -> Option<HeaderValue> {
HeaderValue::from_str(&fingerprint.0.to_string()).ok()
fn fingerprint_header(fingerprint: BuildFingerprint) -> HeaderValue {
HeaderValue::from(fingerprint.0)
}
impl IntoResponse for EffectResponse {
@@ -1392,11 +1392,10 @@ impl IntoResponse for EffectResponse {
header::CONTENT_TYPE,
HeaderValue::from_static(HEMX_CONTENT_TYPE),
);
if let Some(fingerprint) = fingerprint_header(self.batch.fingerprint) {
response
.headers_mut()
.insert(HEMX_FINGERPRINT_HEADER, fingerprint);
}
response.headers_mut().insert(
HEMX_FINGERPRINT_HEADER,
fingerprint_header(self.batch.fingerprint),
);
response
}
}
@@ -1442,8 +1441,7 @@ impl IntoResponse for RuntimeJs {
);
response.headers_mut().insert(
header::CONTENT_LENGTH,
HeaderValue::from_str(hemx_js::RUNTIME_JS.len().to_string().as_str())
.expect("runtime length is a valid header value"),
HeaderValue::from(hemx_js::RUNTIME_JS.len() as u64),
);
response
}
+150 -1
View File
@@ -13,7 +13,7 @@ use hemx_axum::{
InteractionRequest, IntoHandlerFailure, PageMode, PageRequest, PageResponse, HEMX_CONTENT_TYPE,
HEMX_FINGERPRINT_HEADER, HEMX_PARTIAL_HEADER, HEMX_RUNTIME_CONTENT_TYPE, HEMX_TITLE_HEADER,
};
use hemx_core::{push, BuildFingerprint, Handle, IntoEffect, SafeHtml, Slot};
use hemx_core::{push, BuildFingerprint, Effect, Handle, IntoEffect, SafeHtml, Slot};
use scraper::{Html, Selector};
use std::sync::atomic::{AtomicUsize, Ordering};
use tower::ServiceExt;
@@ -865,6 +865,155 @@ async fn registry_contains_registered_sync_and_async_handles_and_async_falls_bac
// test req: axum_integration/003 req: public_api/001
}
#[tokio::test]
async fn state_result_registration_paths_preserve_state_form_and_failures() {
let sync = Handle::<()>::new(30);
let typed = Handle::<()>::new(31);
let asynchronous = Handle::<()>::new(32);
let typed_async = Handle::<()>::new(33);
let typed_plain = Handle::<()>::new(34);
let typed_state_plain = Handle::<()>::new(37);
let typed_async_plain = Handle::<()>::new(35);
let state_async_plain = Handle::<()>::new(36);
let registry = interactions(BuildFingerprint(88))
.on_state_result(sync, String::from("sync"), |state: State<String>| {
Ok::<_, HandlerBoom>(Slot::<String>::new(1).text(state.0))
})
.on_state_form_result(
typed,
String::from("typed"),
|state: State<String>, Form(form): Form<OpenProject>| {
Err::<Effect, _>(HandlerBoom).map_err(|error| {
let _ = (state, form);
error
})
},
)
.on_state_async_result(
asynchronous,
String::from("async"),
|state: State<String>| async move {
Ok::<_, HandlerBoom>(Slot::<String>::new(1).text(state.0))
},
)
.on_form(typed_plain, |Form(form): Form<OpenProject>| {
Slot::<String>::new(1).text(format!("typed:{}", form.project_id.0))
})
.on_state_form(
typed_state_plain,
String::from("typed-state"),
|state: State<String>, Form(form): Form<OpenProject>| {
Slot::<String>::new(1).text(format!("{}:{}", state.0, form.project_id.0))
},
)
.on_form_async(
typed_async_plain,
|Form(form): Form<OpenProject>| async move {
Slot::<String>::new(1).text(format!("typed-plain-async:{}", form.project_id.0))
},
)
.on_state_async(
state_async_plain,
String::from("state-async"),
|state: State<String>| async move { Slot::<String>::new(1).text(state.0) },
)
.on_state_form_async_result(
typed_async,
String::from("typed-async"),
|state: State<String>, Form(form): Form<OpenProject>| async move {
Ok::<_, HandlerBoom>(
Slot::<String>::new(1).text(format!("{}:{}", state.0, form.project_id.0)),
)
},
);
for handle in [
sync,
typed,
asynchronous,
typed_async,
typed_plain,
typed_state_plain,
typed_async_plain,
state_async_plain,
] {
assert!(registry.contains(handle.id().id));
}
assert!(registry
.dispatch(InteractionForm::for_handle(sync, []))
.is_ok());
let invalid_form = registry
.dispatch(InteractionForm::for_handle(
typed,
[(String::from("project_id"), String::from("invalid"))],
))
.unwrap_err();
assert_eq!(
invalid_form,
DispatchRejection::InvalidForm {
handle_id: typed.id().id,
message: "invalid form field `project_id`".into(),
}
);
let error = registry
.dispatch(InteractionForm::for_handle(
typed,
[(String::from("project_id"), String::from("7"))],
))
.unwrap_err();
assert_eq!(
error,
DispatchRejection::HandlerError(HandlerFailure::internal("database unavailable"))
);
assert!(registry
.dispatch_async(InteractionForm::for_handle(asynchronous, []))
.await
.is_ok());
let invalid_plain = registry
.dispatch(InteractionForm::for_handle(
typed_plain,
[(String::from("project_id"), String::from("invalid"))],
))
.unwrap_err();
assert!(matches!(
invalid_plain,
DispatchRejection::InvalidForm { .. }
));
let invalid_state_plain = registry
.dispatch(InteractionForm::for_handle(
typed_state_plain,
[(String::from("project_id"), String::from("invalid"))],
))
.unwrap_err();
assert!(matches!(
invalid_state_plain,
DispatchRejection::InvalidForm { .. }
));
assert!(registry
.dispatch_async(InteractionForm::for_handle(
typed_async_plain,
[(String::from("project_id"), String::from("8"))],
))
.await
.is_ok());
assert!(registry
.dispatch_async(InteractionForm::for_handle(state_async_plain, []))
.await
.is_ok());
let response = registry
.dispatch_async(InteractionForm::for_handle(
typed_async,
[(String::from("project_id"), String::from("9"))],
))
.await
.unwrap();
assert_eq!(
response.batch.ops,
vec![Slot::<String>::new(1).text("typed-async:9")]
);
// test req: axum_integration/003 req: derive_handler/005
}
#[test]
fn interactions_reject_unknown_handle_ids() {
let request = InteractionRequest::from(InteractionForm::new(9, []));