use chrono::DateTime;
use hyper::StatusCode;
use serde::Serialize;
use serde_json::{json, Value};
use hyperlite::response::with_correlation_id;
use hyperlite::{empty, failure, not_found, success, ApiError};
mod test_helpers;
use test_helpers::*;
#[derive(Serialize)]
struct TestData {
value: String,
}
#[tokio::test]
async fn test_success_response_status() {
let response = success(StatusCode::CREATED, TestData { value: "ok".into() });
assert_status(&response, StatusCode::CREATED);
}
#[tokio::test]
async fn test_success_response_content_type() {
let response = success(StatusCode::OK, TestData { value: "ok".into() });
assert_content_type_json(&response);
}
#[tokio::test]
async fn test_success_response_envelope() {
let response = success(
StatusCode::OK,
TestData {
value: "data".into(),
},
);
let json = read_body_json(response).await;
assert_json_envelope(&json, true);
}
#[tokio::test]
async fn test_success_response_data() {
let response = success(
StatusCode::OK,
TestData {
value: "hello".into(),
},
);
let json = read_body_json(response).await;
assert_eq!(json["data"]["value"], "hello");
}
#[tokio::test]
async fn test_success_response_timestamp() {
let response = success(
StatusCode::OK,
TestData {
value: "hello".into(),
},
);
let json = read_body_json(response).await;
let timestamp = json["meta"]["timestamp"]
.as_str()
.expect("timestamp missing");
assert!(DateTime::parse_from_rfc3339(timestamp).is_ok());
}
#[tokio::test]
async fn test_success_response_no_errors() {
let response = success(
StatusCode::OK,
TestData {
value: "hello".into(),
},
);
let json = read_body_json(response).await;
assert!(json.get("errors").is_none());
}
#[tokio::test]
async fn test_failure_response_status() {
let response = failure(
StatusCode::BAD_REQUEST,
vec![ApiError::new("VALIDATION", "invalid")],
);
assert_status(&response, StatusCode::BAD_REQUEST);
}
#[tokio::test]
async fn test_failure_response_content_type() {
let response = failure(
StatusCode::BAD_REQUEST,
vec![ApiError::new("VALIDATION", "invalid")],
);
assert_content_type_json(&response);
}
#[tokio::test]
async fn test_failure_response_envelope() {
let response = failure(
StatusCode::BAD_REQUEST,
vec![ApiError::new("VALIDATION", "invalid")],
);
let json = read_body_json(response).await;
assert_json_envelope(&json, false);
}
#[tokio::test]
async fn test_failure_response_errors() {
let response = failure(
StatusCode::BAD_REQUEST,
vec![ApiError::new("VALIDATION", "invalid")],
);
let json = read_body_json(response).await;
assert_eq!(json["errors"].as_array().unwrap().len(), 1);
}
#[tokio::test]
async fn test_failure_response_multiple_errors() {
let response = failure(
StatusCode::UNPROCESSABLE_ENTITY,
vec![
ApiError::new("VALIDATION", "email"),
ApiError::new("VALIDATION", "password"),
],
);
let json = read_body_json(response).await;
assert_eq!(json["errors"].as_array().unwrap().len(), 2);
}
#[tokio::test]
async fn test_failure_response_no_data() {
let response = failure(
StatusCode::BAD_REQUEST,
vec![ApiError::new("VALIDATION", "invalid")],
);
let json = read_body_json(response).await;
assert!(json.get("data").is_none());
}
#[tokio::test]
async fn test_not_found_response_status() {
let response = not_found("/missing".to_owned());
assert_status(&response, StatusCode::NOT_FOUND);
}
#[tokio::test]
async fn test_not_found_response_envelope() {
let response = not_found("/missing".to_owned());
let json = read_body_json(response).await;
assert_json_envelope(&json, false);
}
#[tokio::test]
async fn test_not_found_response_path() {
let response = not_found("/users/123".to_owned());
let json = read_body_json(response).await;
assert_eq!(json["data"]["path"], "/users/123");
}
#[tokio::test]
async fn test_not_found_response_message() {
let response = not_found("/users/123".to_owned());
let json = read_body_json(response).await;
assert_eq!(json["message"], "Resource not found");
}
#[tokio::test]
async fn test_not_found_response_error_code() {
let response = not_found("/users/123".to_owned());
let json = read_body_json(response).await;
assert_eq!(json["errors"][0]["code"], "NOT_FOUND");
}
#[tokio::test]
async fn test_empty_response_status() {
let response = empty(StatusCode::ACCEPTED);
assert_status(&response, StatusCode::ACCEPTED);
}
#[tokio::test]
async fn test_empty_response_no_body() {
let bytes = read_body_bytes(empty(StatusCode::NO_CONTENT)).await;
assert_eq!(bytes.len(), 0);
}
#[tokio::test]
async fn test_empty_response_no_content_type() {
let response = empty(StatusCode::NO_CONTENT);
assert!(response.headers().get("content-type").is_none());
}
#[tokio::test]
async fn test_empty_response_204() {
let response = empty(StatusCode::NO_CONTENT);
assert_status(&response, StatusCode::NO_CONTENT);
}
#[tokio::test]
async fn test_with_correlation_id_in_meta() {
let response = with_correlation_id(
StatusCode::OK,
true,
Some(json!({"status": "ok"})),
None,
None,
Some("req-123".into()),
);
let json = read_body_json(response).await;
assert_eq!(json["meta"]["correlationId"], "req-123");
}
#[tokio::test]
async fn test_with_correlation_id_in_header() {
let response = with_correlation_id(
StatusCode::OK,
true,
Some(json!({"status": "ok"})),
None,
None,
Some("req-456".into()),
);
let header = response
.headers()
.get("x-request-id")
.expect("missing x-request-id header");
assert_eq!(header, "req-456");
}
#[tokio::test]
async fn test_with_correlation_id_matches() {
let response = with_correlation_id(
StatusCode::OK,
true,
Some(json!({"status": "ok"})),
None,
None,
Some("req-789".into()),
);
let header = response
.headers()
.get("x-request-id")
.expect("missing header")
.to_str()
.unwrap()
.to_owned();
let json = read_body_json(response).await;
assert_eq!(json["meta"]["correlationId"], header);
}
#[tokio::test]
async fn test_with_correlation_id_none() {
let response = with_correlation_id::<Value>(StatusCode::OK, true, None, None, None, None);
assert!(response.headers().get("x-request-id").is_none());
let json = read_body_json(response).await;
assert!(json["meta"].get("correlationId").is_none());
}
#[test]
fn test_api_error_new() {
let error = ApiError::new("CODE", "message");
assert_eq!(error.code, "CODE");
assert_eq!(error.message.as_deref(), Some("message"));
}
#[test]
fn test_api_error_with_code() {
let error = ApiError::with_code("CODE");
assert_eq!(error.code, "CODE");
assert!(error.message.is_none());
}
#[test]
fn test_api_error_with_optional_message() {
let error = ApiError::with_optional_message("CODE", Some("message".into()));
assert_eq!(error.message.as_deref(), Some("message"));
}
#[test]
fn test_api_error_serialization() {
let error = ApiError::new("CODE", "message");
let value = serde_json::to_value(&error).unwrap();
assert_eq!(value["code"], "CODE");
assert_eq!(value["message"], "message");
}
#[test]
fn test_api_error_message_omitted_when_none() {
let error = ApiError::with_code("CODE");
let value = serde_json::to_value(&error).unwrap();
assert!(value.get("message").is_none());
}
#[tokio::test]
async fn test_envelope_success_fields() {
let response = success(StatusCode::OK, TestData { value: "ok".into() });
let json = read_body_json(response).await;
assert!(json.get("data").is_some());
assert!(json.get("errors").is_none());
}
#[tokio::test]
async fn test_envelope_failure_fields() {
let response = failure(StatusCode::BAD_REQUEST, vec![ApiError::new("CODE", "oops")]);
let json = read_body_json(response).await;
assert!(json.get("data").is_none());
assert!(json.get("errors").is_some());
}
#[tokio::test]
async fn test_envelope_meta_always_present() {
let success_json =
read_body_json(success(StatusCode::OK, TestData { value: "ok".into() })).await;
let failure_json = read_body_json(failure(
StatusCode::BAD_REQUEST,
vec![ApiError::new("CODE", "oops")],
))
.await;
assert!(success_json.get("meta").is_some());
assert!(failure_json.get("meta").is_some());
}
#[tokio::test]
async fn test_envelope_timestamp_format() {
let response = success(StatusCode::OK, TestData { value: "ok".into() });
let json = read_body_json(response).await;
let timestamp = json["meta"]["timestamp"].as_str().unwrap();
assert!(DateTime::parse_from_rfc3339(timestamp).is_ok());
}
#[derive(Serialize)]
struct EmptyStruct {}
#[tokio::test]
async fn test_success_with_empty_struct() {
let response = success(StatusCode::OK, EmptyStruct {});
let json = read_body_json(response).await;
assert!(json["data"].is_object());
}
#[derive(Serialize)]
struct NestedData {
nested: InnerData,
}
#[derive(Serialize)]
struct InnerData {
value: i32,
}
#[tokio::test]
async fn test_success_with_nested_data() {
let response = success(
StatusCode::OK,
NestedData {
nested: InnerData { value: 10 },
},
);
let json = read_body_json(response).await;
assert_eq!(json["data"]["nested"]["value"], 10);
}
#[tokio::test]
async fn test_failure_with_empty_errors() {
let response = failure(StatusCode::BAD_REQUEST, vec![]);
let json = read_body_json(response).await;
assert!(json["errors"].is_array());
assert_eq!(json["errors"].as_array().unwrap().len(), 0);
}
#[tokio::test]
async fn test_not_found_with_special_chars() {
let response = not_found("/naïve path".to_owned());
let json = read_body_json(response).await;
assert_eq!(json["data"]["path"], "/naïve path");
}