use crate::rejection_kinds::KINDS;
use camber::RuntimeError;
use camber::http::{
NegotiatedResponseMetadata, Rejection, RejectionContext, RejectionKind, RejectionProtocol,
Request, RequestId, Response,
};
const _: () = assert!(!KINDS.is_empty());
const HANDSHAKE_STATUSES: [u16; 3] = [400, 403, 426];
const _: () = assert!(!HANDSHAKE_STATUSES.is_empty());
const INTERNAL_STATUSES: [u16; 2] = [500, 503];
const _: () = assert!(!INTERNAL_STATUSES.is_empty());
fn generated_id() -> RequestId {
Request::builder()
.path("/items")
.finish()
.expect("the fixture target is an accepted request target")
.request_id()
}
fn assert_id_shape(id: &str) {
assert_eq!(id.len(), 32, "an identifier renders as 32 digits: {id:?}");
assert!(
id.bytes()
.all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)),
"an identifier renders as lowercase hexadecimal: {id:?}"
);
}
fn accepts_rejection_mapper<F>(_mapper: F)
where
F: Fn(&Rejection, &RejectionContext) -> Result<Response, RuntimeError> + Send + Sync + 'static,
{
}
#[test]
fn public_rejection_contract_exposes_only_client_safe_state() {
let mut rows = 0_usize;
for kind in KINDS {
let rejection =
Rejection::new(kind, 400, "safe detail").expect("400 is a representable status");
assert_eq!(rejection.kind(), kind);
assert_eq!(rejection.status(), 400);
assert_eq!(rejection.message(), "safe detail");
assert_eq!(rejection.headers().count(), 0);
assert_eq!(
rejection,
Rejection::new(kind, 400, "safe detail").expect("400 is a representable status"),
"two rejections built from the same data compare equal"
);
assert!(
format!("{rejection:?}").contains(&format!("{kind:?}")),
"the debug projection names its category"
);
rows += 1;
}
assert_eq!(rows, KINDS.len(), "every declared category was exercised");
assert_ne!(
Rejection::new(RejectionKind::Routing, 404, "not found")
.expect("404 is a representable status"),
Rejection::new(RejectionKind::Application, 404, "not found")
.expect("404 is a representable status"),
"the category is part of a rejection's identity, not its status alone"
);
}
#[test]
fn rejection_status_is_data_rather_than_a_function_of_its_category() {
let mut rows = 0_usize;
for status in HANDSHAKE_STATUSES {
let rejection = Rejection::new(RejectionKind::WebSocketHandshake, status, "refused")
.expect("every handshake status is representable");
assert_eq!(rejection.status(), status);
assert_eq!(rejection.kind(), RejectionKind::WebSocketHandshake);
rows += 1;
}
assert_eq!(rows, HANDSHAKE_STATUSES.len());
let mut internal_rows = 0_usize;
for status in INTERNAL_STATUSES {
let rejection = Rejection::new(RejectionKind::InternalService, status, "service state")
.expect("every internal status is representable");
assert_eq!(rejection.status(), status);
assert_eq!(rejection.kind(), RejectionKind::InternalService);
internal_rows += 1;
}
assert_eq!(internal_rows, INTERNAL_STATUSES.len());
assert!(
Rejection::new(RejectionKind::Routing, 42, "unrepresentable").is_err(),
"a status outside 100-599 cannot become a rejection"
);
}
#[test]
fn rejection_headers_are_borrowed_in_registration_order() {
let rejection = Rejection::new(RejectionKind::MethodSelection, 405, "method not allowed")
.expect("405 is a representable status")
.with_header("Allow", "GET, HEAD")
.with_header("X-Safe", "yes");
let headers: Vec<(&str, &str)> = rejection.headers().collect();
assert_eq!(headers, [("Allow", "GET, HEAD"), ("X-Safe", "yes")]);
assert_eq!(rejection.message(), "method not allowed");
}
#[test]
fn rejection_context_reports_absence_only_through_option() {
let id = generated_id();
let bare = RejectionContext::new(id, "GET", "/items");
assert_eq!(bare.request_id(), &id);
assert_eq!(bare.method(), "GET");
assert_eq!(bare.raw_path(), "/items");
assert_eq!(bare.remote_addr(), None);
assert_eq!(bare.route(), None);
assert!(bare.negotiated().is_none());
let remote: std::net::IpAddr = "203.0.113.9".parse().expect("a literal IPv4 address");
let established = RejectionContext::new(id, "PATCH", "/users/7")
.with_remote_addr(remote)
.with_route("/users/:id")
.with_negotiated(
NegotiatedResponseMetadata::new(RejectionProtocol::WebSocket).with_subprotocol("chat"),
);
assert_eq!(established.remote_addr(), Some(remote));
assert_eq!(established.route(), Some("/users/:id"));
let negotiated = established
.negotiated()
.expect("the fixture established negotiated metadata");
assert_eq!(negotiated.protocol(), RejectionProtocol::WebSocket);
assert_eq!(negotiated.subprotocol(), Some("chat"));
assert_eq!(
negotiated.content_type(),
None,
"an unestablished value stays absent rather than becoming an empty sentinel"
);
}
#[test]
fn request_id_display_and_accessor_agree_on_32_lowercase_hex_digits() {
let id = generated_id();
assert_eq!(id.to_string(), id.as_str());
assert_id_shape(id.as_str());
assert_eq!(id, id, "a copied identifier keeps value equality");
assert_ne!(
id,
generated_id(),
"two generated identifiers name two requests"
);
}
#[test]
fn mapper_signature_admits_only_borrowed_safe_inputs() {
accepts_rejection_mapper(|rejection: &Rejection, context: &RejectionContext| {
Response::text(rejection.status(), rejection.message())
.map(|response| response.with_header("X-Request-Id", context.request_id().as_str()))
});
}
#[cfg(not(any(feature = "jemalloc", feature = "mimalloc")))]
#[test]
fn request_id_generation_and_access_are_allocation_free() {
let warmup = camber::http::mock::generated_request_id();
assert_id_shape(warmup.as_str());
let calibration = allocation_counter::measure(|| {
drop(std::hint::black_box(Box::new(1_u32)));
});
assert!(
calibration.count_total > 0,
"a probe that counts nothing would make every zero below meaningless"
);
let mut generated = Vec::with_capacity(64);
let generation = allocation_counter::measure(|| {
for _ in 0..64 {
std::hint::black_box(camber::http::mock::generated_request_id());
}
});
assert_eq!(
generation.count_total, 0,
"generating an identifier writes fixed inline storage and allocates nothing"
);
let observed = camber::http::mock::generated_request_id();
let access = allocation_counter::measure(|| {
for _ in 0..64 {
std::hint::black_box(observed.as_str());
}
});
assert_eq!(
access.count_total, 0,
"reading an identifier borrows its inline digits"
);
for _ in 0..64 {
generated.push(camber::http::mock::generated_request_id());
}
for id in &generated {
assert_id_shape(id.as_str());
}
let mut unique: Vec<&str> = generated.iter().map(RequestId::as_str).collect();
unique.sort_unstable();
unique.dedup();
assert_eq!(
unique.len(),
generated.len(),
"every generated identifier names one request"
);
}