crates/gunnar-public-site/tests/head.rs
//! **S-4: the head tags on every public page**, from ONE shared helper: the
//! title, the meta description, the canonical URL, Open Graph, the Twitter card
//! and JSON-LD (Organization and SoftwareApplication on the home).
mod common;
use common::*;
use http::StatusCode;
/// The `content` of `<meta {attr}="{key}" content="…">`.
fn meta<'a>(html: &'a str, attr: &str, key: &str) -> Option<&'a str> {
let start = format!("<meta {attr}=\"{key}\" content=\"");
let at = html.find(&start)? + start.len();
Some(&html[at..at + html[at..].find('"')?])
}
/// Every JSON-LD block of a page, parsed.
fn json_ld(html: &str) -> Vec<serde_json::Value> {
let open = "<script type=\"application/ld+json\">";
html.match_indices(open)
.map(|(i, _)| {
let rest = &html[i + open.len()..];
let body = &rest[..rest.find("</script>").expect("the block is closed")];
serde_json::from_str(body).unwrap_or_else(|e| panic!("JSON-LD does not parse ({e}): {body}"))
})
.collect()
}
fn types(ld: &[serde_json::Value]) -> Vec<String> {
let mut out = Vec::new();
for v in ld {
let items = v.as_array().cloned().unwrap_or_else(|| vec![v.clone()]);
for i in items {
if let Some(t) = i.get("@type").and_then(|t| t.as_str()) {
out.push(t.to_owned());
}
}
}
out
}
#[tokio::test]
async fn the_home_carries_every_head_tag() {
let (site, _) = site().await;
let a = get(&app(site), "/").await;
assert_eq!(a.status, StatusCode::OK);
let h = &a.body;
assert!(h.contains("<title>Gunnar — a European git server in Rust</title>"), "{h}");
assert_eq!(h.matches("<title>").count(), 1, "{h}");
assert!(meta(h, "name", "description").is_some_and(|d| d == "Gunnar is a European git server in Rust: self-hosted or SaaS, with SLSA Level 3 releases."), "{h}");
assert!(h.contains("<link rel=\"canonical\" href=\"https://gunnar.example/\">"), "{h}");
assert_eq!(meta(h, "property", "og:type"), Some("website"));
assert_eq!(meta(h, "property", "og:site_name"), Some("Gunnar"));
assert_eq!(meta(h, "property", "og:url"), Some("https://gunnar.example/"));
assert!(meta(h, "property", "og:title").is_some_and(|t| t.starts_with("Gunnar")));
assert!(meta(h, "property", "og:description").is_some_and(|d| !d.is_empty()));
assert_eq!(meta(h, "name", "twitter:card"), Some("summary"));
assert!(meta(h, "name", "twitter:title").is_some() && meta(h, "name", "twitter:description").is_some());
let ld = json_ld(h);
let t = types(&ld);
assert!(t.contains(&"Organization".to_owned()) && t.contains(&"SoftwareApplication".to_owned()), "{t:?}");
}
#[tokio::test]
async fn every_html_page_has_the_tags_from_the_one_helper() {
let (site, _) = site().await;
let built = site.built();
let mut html = 0;
for (path, page) in &built.pages {
if !page.content_type.starts_with("text/html") {
continue;
}
html += 1;
let b = &page.body;
for tag in ["<title>", "<meta name=\"description\"", "<link rel=\"canonical\" href=\"https://gunnar.example", "<meta property=\"og:title\"", "<meta name=\"twitter:card\"", "<script type=\"application/ld+json\">"] {
assert_eq!(b.matches(tag).count(), 1, "{path}: {tag} once");
}
assert!(!json_ld(b).is_empty(), "{path}");
}
assert!(html >= 1, "no HTML page was built");
}
#[tokio::test]
async fn a_hostile_description_cannot_break_out_of_the_head() {
let mut c = config();
c.description = "Fast \"quoted\" & <b>bold</b> </script><script>alert(1)</script>".into();
let (site, _) = site_with(Fake::new(), c).await;
let h = get(&app(site), "/").await.body;
assert_eq!(meta(&h, "name", "description"), Some("Fast "quoted" & <b>bold</b> </script><script>alert(1)</script>"), "{h}");
assert!(!h.contains("<script>alert"), "{h}");
let ld = json_ld(&h);
let app = ld.iter().flat_map(|v| v.as_array().cloned().unwrap_or_else(|| vec![v.clone()])).find(|i| i["@type"] == "SoftwareApplication").expect("SoftwareApplication");
assert_eq!(app["description"].as_str(), Some("Fast \"quoted\" & <b>bold</b> </script><script>alert(1)</script>"), "JSON-LD keeps the words, escaped for the script element");
}