use super::*;
fn headers(pairs: &[(&str, &str)]) -> HeaderMap {
let mut map = HeaderMap::new();
for (name, value) in pairs {
let name = HeaderName::from_bytes(name.as_bytes())
.unwrap_or_else(|error| panic!("{name:?} is not a header name: {error}"));
let value = HeaderValue::from_str(value)
.unwrap_or_else(|error| panic!("{value:?} is not a header value: {error}"));
map.append(name, value);
}
map
}
fn debug(map: &HeaderMap) -> String {
format!("{:?}", redact(map))
}
#[test]
fn every_upstream_secret_header_is_redacted_and_its_neighbour_is_not() {
let names = [
"Authorization",
"Proxy-Authorization",
"X-API-Key",
"API-Key",
"Cookie",
"Set-Cookie",
"X-Access-Token",
"X-Client-Secret",
"x-MiXeD-ToKeN",
];
for name in names {
let map = headers(&[(name, "request-credential"), ("x-visible", "request-visible")]);
let lower = name.to_ascii_lowercase();
assert_eq!(
debug(&map),
format!(r#"{{"{lower}": "***", "x-visible": "request-visible"}}"#),
"header {name}"
);
}
}
#[test]
fn the_substring_rule_is_token_and_secret_only() {
let secret = ["token", "x-tokens", "tokenized", "secret", "x-secrets-hash", "client-secret-id"];
for name in secret {
let map = headers(&[(name, "value")]);
assert_eq!(debug(&map), format!(r#"{{"{name}": "***"}}"#), "{name} must be redacted");
}
let visible =
["x-toke", "x-secre", "x-authorization", "authorization-hint", "cookies", "x-cookie"];
for name in visible {
let map = headers(&[(name, "value")]);
assert_eq!(debug(&map), format!(r#"{{"{name}": "value"}}"#), "{name} must be printed");
}
}
#[test]
fn a_value_flagged_sensitive_is_redacted_under_any_name() {
let mut map = headers(&[("x-visible", "shown")]);
let mut hidden = HeaderValue::from_static("flagged-credential");
hidden.set_sensitive(true);
map.insert("x-innocent", hidden);
assert_eq!(debug(&map), r#"{"x-visible": "shown", "x-innocent": "***"}"#);
}
#[test]
fn every_value_of_a_repeated_name_is_rendered_and_redacted() {
let map = headers(&[
("set-cookie", "session=first-secret"),
("x-tag", "a"),
("set-cookie", "csrf=second-secret"),
("x-tag", "b"),
]);
assert_eq!(
debug(&map),
r#"{"set-cookie": "***", "set-cookie": "***", "x-tag": "a", "x-tag": "b"}"#
);
}
#[test]
fn an_empty_map_renders_as_empty_braces() {
let map = HeaderMap::new();
assert_eq!(debug(&map), "{}");
}
#[test]
fn a_value_with_a_byte_above_ascii_is_escaped() {
let mut map = HeaderMap::new();
let value = HeaderValue::from_bytes(b"caf\xe9\tlatte")
.expect("invariant: http accepts obs-text and tab in a value");
map.insert("x-order", value);
map.append("x-order", HeaderValue::from_static("tea\tpot"));
assert_eq!(debug(&map), "{\"x-order\": \"caf\\xe9\tlatte\", \"x-order\": \"tea\tpot\"}");
}
#[test]
fn a_real_looking_key_leaves_no_trace_in_the_rendering() {
let key = "sk-live-9f8e7d6c5b4a3210ZYXWVUTSRQ";
let bearer = format!("Bearer {key}");
let map = headers(&[
("authorization", bearer.as_str()),
("x-api-key", key),
("x-request-id", "req-123"),
]);
let rendered = debug(&map);
assert!(rendered.contains("req-123"), "a visible value went missing: {rendered}");
assert!(!rendered.contains("Bearer"), "the scheme leaked: {rendered}");
let characters: Vec<char> = key.chars().collect();
for window in characters.windows(4) {
let piece: String = window.iter().collect();
assert!(!rendered.contains(&piece), "{piece:?} of the key leaked: {rendered}");
}
}
#[test]
fn the_view_is_a_copyable_borrow() {
let map = headers(&[("x-a", "1")]);
let view = redact(&map);
let copy = view;
assert_eq!(format!("{view:?} {copy:?}"), r#"{"x-a": "1"} {"x-a": "1"}"#);
assert_eq!(size_of::<RedactedHeaders<'_>>(), size_of::<&HeaderMap>());
}