use axum::body::Body;
use axum::http::{Method, Request, StatusCode};
use http_body_util::BodyExt as _;
use serde_json::Value;
use tempfile::TempDir;
use tower::ServiceExt;
fn make_app() -> (axum::Router, TempDir) {
let td = TempDir::new().expect("tmp dir");
let opts = mnem_http::AppOptions {
allow_labels: Some(true),
in_memory: false,
metrics_enabled: false,
};
let app = mnem_http::app_with_options(td.path(), opts).expect("build app");
(app, td)
}
async fn body_to_json(body: Body) -> Value {
let bytes = body.collect().await.expect("collect").to_bytes();
serde_json::from_slice(&bytes).unwrap_or_else(|e| {
panic!(
"expected JSON envelope but got non-JSON: {} (bytes = {:?})",
e,
String::from_utf8_lossy(&bytes)
)
})
}
#[tokio::test]
async fn malformed_json_returns_envelope() {
let (app, _td) = make_app();
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/v1/nodes")
.header("content-type", "application/json")
.body(Body::from("malformed-json"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let ct = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
.to_string();
assert!(
ct.starts_with("application/json"),
"expected JSON envelope, got content-type {ct:?}"
);
let j = body_to_json(resp.into_body()).await;
assert_eq!(j["schema"], "mnem.v1.err");
assert!(j["error"].is_string());
}
#[tokio::test]
async fn wrong_type_returns_envelope() {
let (app, _td) = make_app();
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/v1/nodes")
.header("content-type", "application/json")
.body(Body::from(r#"{"summary": 123}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let j = body_to_json(resp.into_body()).await;
assert_eq!(j["schema"], "mnem.v1.err");
}
#[tokio::test]
async fn missing_content_type_returns_envelope() {
let (app, _td) = make_app();
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/v1/nodes")
.body(Body::from(r#"{"summary":"hi"}"#))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let j = body_to_json(resp.into_body()).await;
assert_eq!(j["schema"], "mnem.v1.err");
}
#[tokio::test]
async fn remote_fetch_blocks_missing_field_returns_envelope() {
let (app, _td) = make_app();
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/remote/v1/fetch-blocks")
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let ct = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
.to_string();
assert!(
ct.starts_with("application/json"),
"expected JSON envelope on /remote/v1/fetch-blocks, got content-type {ct:?}"
);
let j = body_to_json(resp.into_body()).await;
assert_eq!(j["schema"], "mnem.v1.err");
assert!(j["error"].is_string());
}
#[tokio::test]
async fn remote_fetch_blocks_malformed_json_returns_envelope() {
let (app, _td) = make_app();
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/remote/v1/fetch-blocks")
.header("content-type", "application/json")
.body(Body::from("NOT JSON"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let j = body_to_json(resp.into_body()).await;
assert_eq!(j["schema"], "mnem.v1.err");
}
#[tokio::test]
async fn remote_push_blocks_still_uses_problem_json() {
let (app, _td) = make_app();
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/remote/v1/push-blocks")
.header("content-type", "application/vnd.ipld.car")
.body(Body::from(""))
.unwrap(),
)
.await
.unwrap();
let ct = resp
.headers()
.get("content-type")
.and_then(|v| v.to_str().ok())
.unwrap_or_default()
.to_string();
let bytes = resp
.into_body()
.collect()
.await
.expect("collect")
.to_bytes();
let s = String::from_utf8_lossy(&bytes);
if ct.starts_with("application/problem+json") {
assert!(
!s.contains("\"schema\":\"mnem.v1.err\""),
"push-blocks must not emit mnem.v1.err envelope; body={s}"
);
} else {
assert!(
!s.contains("mnem.v1.err"),
"push-blocks body should not contain envelope schema; ct={ct:?} body={s}"
);
}
}
#[tokio::test]
async fn remote_advance_head_still_uses_problem_json() {
let (app, _td) = make_app();
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/remote/v1/advance-head")
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap(),
)
.await
.unwrap();
let bytes = resp
.into_body()
.collect()
.await
.expect("collect")
.to_bytes();
let s = String::from_utf8_lossy(&bytes);
assert!(
!s.contains("\"schema\":\"mnem.v1.err\""),
"advance-head must not emit mnem.v1.err envelope; body={s}"
);
}
#[tokio::test]
async fn handler_level_400_envelope_passes_through() {
let (app, _td) = make_app();
let resp = app
.oneshot(
Request::builder()
.method(Method::POST)
.uri("/v1/nodes")
.header("content-type", "application/json")
.body(Body::from("{}"))
.unwrap(),
)
.await
.unwrap();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let j = body_to_json(resp.into_body()).await;
assert_eq!(j["schema"], "mnem.v1.err");
let msg = j["error"].as_str().unwrap_or("");
assert!(
!msg.contains("invalid request body: {\"schema\":"),
"envelope double-wrap detected: {msg}"
);
}