use super::PQC_NOTE_LINES;
use serde_json::{json, Value};
use std::collections::BTreeMap;
pub(super) fn render(
headers: &[(String, String)],
page_csp: &BTreeMap<String, String>,
) -> Result<String, serde_json::Error> {
let headers_arr: Vec<Value> = headers
.iter()
.map(|(k, v)| json!({ "key": k, "value": v }))
.collect();
let pqc_note: Vec<Value> = PQC_NOTE_LINES
.iter()
.map(|s| Value::String((*s).into()))
.collect();
let mut groups: Vec<Value> = vec![json!({
"source": "/(.*)",
"headers": headers_arr,
})];
for (path, policy) in page_csp {
groups.push(json!({
"source": path,
"headers": [
{ "key": "Content-Security-Policy", "value": policy }
],
}));
}
let config = json!({
"_pqc_note": pqc_note,
"headers": groups,
});
serde_json::to_string_pretty(&config)
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::postprocess::edge_headers::merged_headers;
use std::collections::BTreeMap;
#[test]
fn render_includes_pqc_note_field() {
let body = render(&merged_headers(&BTreeMap::new()), &BTreeMap::new())
.unwrap();
let parsed: Value = serde_json::from_str(&body).unwrap();
let note = parsed.get("_pqc_note").and_then(|v| v.as_array()).unwrap();
let joined: String = note
.iter()
.filter_map(|v| v.as_str())
.collect::<Vec<_>>()
.join(" ");
assert!(joined.contains("X25519+ML-KEM-768"));
assert!(joined.contains("vercel.com"));
}
#[test]
fn render_uses_canonical_headers_array_shape() {
let body = render(&merged_headers(&BTreeMap::new()), &BTreeMap::new())
.unwrap();
let parsed: Value = serde_json::from_str(&body).unwrap();
let group = parsed
.get("headers")
.and_then(|v| v.as_array())
.and_then(|a| a.first())
.unwrap();
assert_eq!(group.get("source").and_then(|v| v.as_str()), Some("/(.*)"));
let arr = group.get("headers").and_then(|v| v.as_array()).unwrap();
assert_eq!(arr.len(), 5);
for item in arr {
assert!(item.get("key").is_some(), "each entry needs a key");
assert!(item.get("value").is_some(), "each entry needs a value");
}
}
#[test]
fn render_emits_one_csp_entry_only() {
let body = render(&merged_headers(&BTreeMap::new()), &BTreeMap::new())
.unwrap();
let parsed: Value = serde_json::from_str(&body).unwrap();
let arr = parsed["headers"][0]["headers"].as_array().unwrap();
let count = arr
.iter()
.filter(|h| {
h.get("key").and_then(|k| k.as_str()).is_some_and(|s| {
s.eq_ignore_ascii_case("Content-Security-Policy")
})
})
.count();
assert_eq!(count, 1);
}
#[test]
fn render_keys_are_in_baseline_order() {
let body = render(&merged_headers(&BTreeMap::new()), &BTreeMap::new())
.unwrap();
let parsed: Value = serde_json::from_str(&body).unwrap();
let arr = parsed["headers"][0]["headers"].as_array().unwrap();
let keys: Vec<&str> =
arr.iter().map(|h| h["key"].as_str().unwrap()).collect();
assert_eq!(
keys,
vec![
"Strict-Transport-Security",
"Content-Security-Policy",
"X-Content-Type-Options",
"Referrer-Policy",
"Permissions-Policy",
]
);
}
#[test]
fn render_emits_pretty_printed_json() {
let body = render(&merged_headers(&BTreeMap::new()), &BTreeMap::new())
.unwrap();
assert!(body.contains("\n "), "pretty output expected");
}
#[test]
fn render_with_empty_headers_produces_valid_json() {
let body = render(&[], &BTreeMap::new()).unwrap();
let parsed: Value = serde_json::from_str(&body).unwrap();
let arr = parsed["headers"][0]["headers"].as_array().unwrap();
assert!(arr.is_empty());
}
#[test]
fn render_pqc_note_lines_match_constant() {
let body = render(&merged_headers(&BTreeMap::new()), &BTreeMap::new())
.unwrap();
let parsed: Value = serde_json::from_str(&body).unwrap();
let note = parsed.get("_pqc_note").and_then(|v| v.as_array()).unwrap();
assert_eq!(note.len(), PQC_NOTE_LINES.len());
for (i, line) in PQC_NOTE_LINES.iter().enumerate() {
assert_eq!(note[i].as_str(), Some(*line));
}
}
#[test]
fn render_appends_per_page_csp_routes_after_global() {
let mut pages: BTreeMap<String, String> = BTreeMap::new();
let _ = pages.insert(
"/blog/post/".to_string(),
"script-src 'self' 'sha256-abc'".to_string(),
);
let body = render(&merged_headers(&BTreeMap::new()), &pages).unwrap();
let parsed: Value = serde_json::from_str(&body).unwrap();
let groups = parsed["headers"].as_array().unwrap();
assert_eq!(groups.len(), 2);
assert_eq!(groups[0]["source"].as_str(), Some("/(.*)"));
assert_eq!(groups[1]["source"].as_str(), Some("/blog/post/"));
assert_eq!(
groups[1]["headers"][0]["key"].as_str(),
Some("Content-Security-Policy")
);
assert_eq!(
groups[1]["headers"][0]["value"].as_str(),
Some("script-src 'self' 'sha256-abc'")
);
}
#[test]
fn render_with_custom_headers_round_trips_through_serde() {
let custom = vec![
("Strict-Transport-Security".to_string(), "x".to_string()),
("Custom-Key".to_string(), "value".to_string()),
];
let body = render(&custom, &BTreeMap::new()).unwrap();
let parsed: Value = serde_json::from_str(&body).unwrap();
let arr = parsed["headers"][0]["headers"].as_array().unwrap();
assert_eq!(arr.len(), 2);
assert_eq!(arr[1]["key"].as_str(), Some("Custom-Key"));
assert_eq!(arr[1]["value"].as_str(), Some("value"));
}
#[test]
fn render_source_route_matches_every_path() {
let body = render(&merged_headers(&BTreeMap::new()), &BTreeMap::new())
.unwrap();
let parsed: Value = serde_json::from_str(&body).unwrap();
let source = parsed["headers"][0]["source"].as_str().unwrap();
assert_eq!(source, "/(.*)");
}
#[test]
fn render_with_special_characters_in_value_escapes_correctly() {
let custom =
vec![("Backslash-Test".to_string(), "a\"b\\c".to_string())];
let body = render(&custom, &BTreeMap::new()).unwrap();
let parsed: Value = serde_json::from_str(&body).unwrap();
let value = parsed["headers"][0]["headers"][0]["value"]
.as_str()
.unwrap();
assert_eq!(value, "a\"b\\c");
}
}