tests/header_config.rs 11.6 KiB 336 lines Raw
1 //! Header config (m6, f0rge issues #21 + #24 + #27 + #28):2 //! `--repo-name`/`HU13_REPO_NAME` (flag > env > dirname, never blank) +3 //! `--description`/`HU13_DESCRIPTION` (flag > env > hidden, no empty `<p>`),4 //! threaded into `<title>`, og tags, feed title/subtitle, and the code-page5 //! repo-head — never into the minimal header (logo • nav only).6 //! Hostile `<script>` input renders as escaped text everywhere.7 8 use std::sync::atomic::{AtomicU64, Ordering};9 10 static SEQ: AtomicU64 = AtomicU64::new(0);11 12 use std::path::{Path, PathBuf};13 use std::process::Command;14 use std::sync::Mutex;15 16 static ENV_LOCK: Mutex<()> = Mutex::new(());17 18 fn git(repo: &Path, args: &[&str]) {19     let st = Command::new("git")20         .arg("-C")21         .arg(repo)22         .args(args)23         .status()24         .expect("spawn git");25     assert!(st.success(), "git {args:?} failed");26 }27 28 fn fresh(prefix: &str) -> PathBuf {29     let dir = std::env::temp_dir().join(format!(30         "hu13-header-{prefix}-{}-{}",31         std::process::id(),32         std::time::SystemTime::now()33             .duration_since(std::time::UNIX_EPOCH)34             .map(|d| d.as_nanos())35             .unwrap_or(0)36             .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))37     ));38     std::fs::create_dir_all(&dir).unwrap();39     dir40 }41 42 /// Minimal repo: README (proves the README line is NOT used as the43 /// description fallback) + one file, one commit, no `.forge/`.44 fn fixture_repo() -> PathBuf {45     let dir = fresh("repo");46     git(&dir, &["init", "-b", "main"]);47     git(&dir, &["config", "user.name", "hu13-test"]);48     git(&dir, &["config", "user.email", "[email protected]"]);49     git(&dir, &["config", "commit.gpgsign", "false"]);50     std::fs::write(dir.join("README.md"), "# Readme headline\n\nbody\n").unwrap();51     std::fs::write(dir.join("hello.txt"), "hello\n").unwrap();52     git(&dir, &["add", "-A"]);53     git(&dir, &["commit", "-m", "init"]);54     dir55 }56 57 fn build_with(repo: &Path, name_flag: Option<String>, desc_flag: Option<String>) -> PathBuf {58     let out = fresh("out");59     let bin = env!("CARGO_BIN_EXE_hu13");60     let mut cmd = Command::new(bin);61     cmd.arg("build").arg(repo).arg("-o").arg(&out);62     if let Some(n) = name_flag {63         cmd.arg("--repo-name").arg(n);64     }65     if let Some(d) = desc_flag {66         cmd.arg("--description").arg(d);67     }68     let st = cmd.status().expect("spawn hu13 build");69     assert!(st.success(), "build exits 0");70     out71 }72 73 fn read(rel: &Path, join: &str) -> String {74     std::fs::read_to_string(rel.join(join)).expect(join)75 }76 77 fn header_of(html: &str) -> &str {78     let start = html.find("<header").expect("header open");79     let end = html.find("</header>").expect("header close");80     &html[start..end]81 }82 83 fn set_env(k: &str, v: Option<&str>) {84     unsafe {85         match v {86             Some(val) => std::env::set_var(k, val),87             None => std::env::remove_var(k),88         }89     }90 }91 92 #[test]93 fn repo_name_chain_flag_beats_env_beats_dirname() {94     let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());95     let repo = fixture_repo();96     let dirname = repo.file_name().unwrap().to_string_lossy().into_owned();97 98     // Env only -> env wins over dirname.99     set_env("HU13_REPO_NAME", Some("Env Name"));100     set_env("HU13_DESCRIPTION", None);101     let out = build_with(&repo, None, None);102     let index = read(&out, "index.html");103     assert!(104         index.contains("<title>Env Name</title>"),105         "env name in title"106     );107     assert!(108         index.contains("og:title\" content=\"Env Name\""),109         "env name in og:title"110     );111     assert!(112         index.contains("<div class=\"repo-head\"><h1>Env Name</h1>"),113         "env name in code-page repo-head"114     );115     assert!(116         read(&out, "feed.atom").contains("<title>Env Name</title>"),117         "env name in feed title"118     );119     let header = header_of(&index).to_string();120     assert!(121         !header.contains("<span class=\"brand\">"),122         "no brand node in header"123     );124     assert!(!header.contains("hu13-word"), "no word node in header");125     assert_eq!(header.matches("•").count(), 1, "logo • nav only");126     assert!(!header.contains("Env Name"), "header carries zero names");127 128     // Flag beats env.129     set_env("HU13_REPO_NAME", Some("Env Name"));130     let out = build_with(&repo, Some("Flag Name".into()), None);131     let index = read(&out, "index.html");132     assert!(index.contains("<title>Flag Name</title>"), "flag wins");133     assert!(!index.contains("Env Name"), "env suppressed by flag");134     assert!(135         index.contains("og:title\" content=\"Flag Name\""),136         "og:title follows flag"137     );138 139     // Neither -> dirname fallback, never blank.140     set_env("HU13_REPO_NAME", None);141     let out = build_with(&repo, None, None);142     let index = read(&out, "index.html");143     assert!(144         index.contains(&format!("<title>{dirname}</title>")),145         "dirname fallback"146     );147     assert!(148         index.contains(&format!("<div class=\"repo-head\"><h1>{dirname}</h1>")),149         "repo-head fallback"150     );151     assert!(152         !header_of(&index).contains(&dirname),153         "header stays nameless"154     );155     assert!(!index.contains("<h1></h1>"), "header never blank");156     set_env("HU13_REPO_NAME", None);157 }158 159 #[test]160 fn description_chain_flag_beats_env_beats_hidden() {161     let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());162     let repo = fixture_repo();163     set_env("HU13_REPO_NAME", None);164 165     // Env only -> shown.166     set_env("HU13_DESCRIPTION", Some("Env desc"));167     let out = build_with(&repo, None, None);168     let index = read(&out, "index.html");169     assert!(170         index.contains("<p class=\"repo-desc\">Env desc</p>"),171         "env desc shown"172     );173 174     // Flag beats env.175     set_env("HU13_DESCRIPTION", Some("Env desc"));176     let out = build_with(&repo, None, Some("Flag desc".into()));177     let index = read(&out, "index.html");178     assert!(179         index.contains("<p class=\"repo-desc\">Flag desc</p>"),180         "flag wins"181     );182     assert!(!index.contains("Env desc"), "env suppressed by flag");183 184     // Neither -> hidden: no description line at all (and the README185     // headline is NOT used as a fallback).186     set_env("HU13_DESCRIPTION", None);187     let out = build_with(&repo, None, None);188     let index = read(&out, "index.html");189     assert!(!index.contains("repo-desc"), "no description line");190     assert!(!index.contains("No description"), "no placeholder text");191     // The README headline still renders in the README section, but must192     // not leak into the header/description surfaces.193     assert!(!index.contains("og:description"), "no og:description");194     let feed = read(&out, "feed.atom");195     assert!(!feed.contains("<subtitle>"), "no feed subtitle");196     set_env("HU13_DESCRIPTION", None);197 }198 199 #[test]200 fn hostile_name_and_description_escape_everywhere() {201     let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());202     let repo = fixture_repo();203     set_env("HU13_REPO_NAME", None);204     set_env("HU13_DESCRIPTION", None);205     let evil_name = "Evil <script>alert(1)</script> Hub";206     let evil_desc = "Desc <script>alert(2)</script> & \"quoted\"";207     let out = build_with(&repo, Some(evil_name.into()), Some(evil_desc.into()));208     let index = read(&out, "index.html");209     let feed = read(&out, "feed.atom");210     let commits = read(&out, "commits.html");211 212     for page in [&index, &commits] {213         assert!(!page.contains("<script>alert"), "no raw script passthrough");214     }215     assert!(!feed.contains("<script>alert"), "feed escapes too");216 217     // Escaped forms present in every naming surface; the header stays nameless.218     let name_e = "Evil &lt;script&gt;alert(1)&lt;/script&gt; Hub";219     let desc_e = "Desc &lt;script&gt;alert(2)&lt;/script&gt; &amp; &quot;quoted&quot;";220     assert!(221         !header_of(&index).contains(name_e),222         "header carries zero names"223     );224     assert!(225         !header_of(&index).contains("<span class=\"brand\">"),226         "no brand node to escape into"227     );228     assert!(index.contains(desc_e), "body escapes description");229     assert!(230         index.contains(&format!("<title>{name_e}</title>")),231         "title escapes name"232     );233     assert!(234         index.contains(&format!("og:title\" content=\"{name_e}\"")),235         "og:title matches"236     );237     assert!(238         index.contains(&format!("og:description\" content=\"{desc_e}\"")),239         "og:description matches"240     );241     assert!(commits.contains("<title>commit"), "subpage title kept");242     assert!(243         !header_of(&commits).contains(name_e),244         "subpage header stays nameless"245     );246     assert!(247         index.contains(&format!("<div class=\"repo-head\"><h1>{name_e}</h1>")),248         "code-page repo-head escapes name"249     );250     assert!(251         feed.contains(&format!("<title>{name_e}</title>")),252         "feed title escapes name"253     );254     assert!(255         feed.contains(&format!("<subtitle>{desc_e}</subtitle>")),256         "feed subtitle escapes description"257     );258     set_env("HU13_REPO_NAME", None);259     set_env("HU13_DESCRIPTION", None);260 }261 262 #[test]263 fn empty_values_fall_back_never_blank() {264     let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());265     let repo = fixture_repo();266     let dirname = repo.file_name().unwrap().to_string_lossy().into_owned();267 268     // Empty flag values fall back.269     set_env("HU13_REPO_NAME", None);270     set_env("HU13_DESCRIPTION", None);271     let out = build_with(&repo, Some("".into()), Some("   ".into()));272     let index = read(&out, "index.html");273     assert!(274         index.contains(&format!("<title>{dirname}</title>")),275         "empty name flag falls back to dirname"276     );277     assert!(!index.contains("repo-desc"), "empty desc flag hides line");278 279     // Empty env values fall back too.280     set_env("HU13_REPO_NAME", Some(""));281     set_env("HU13_DESCRIPTION", Some("  "));282     let out = build_with(&repo, None, None);283     let index = read(&out, "index.html");284     assert!(285         index.contains(&format!("<title>{dirname}</title>")),286         "empty name env falls back to dirname"287     );288     assert!(!index.contains("repo-desc"), "empty desc env hides line");289     assert!(!index.contains("<h1></h1>"), "header never blank");290     set_env("HU13_REPO_NAME", None);291     set_env("HU13_DESCRIPTION", None);292 }293 294 #[test]295 fn feed_title_matches_header_and_subtitle_matches_og() {296     let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());297     let repo = fixture_repo();298     set_env("HU13_REPO_NAME", None);299     set_env("HU13_DESCRIPTION", None);300     let out = build_with(&repo, Some("Feed Hub".into()), Some("Feed desc".into()));301     let index = read(&out, "index.html");302     let feed = read(&out, "feed.atom");303     assert!(feed.contains("<title>Feed Hub</title>"), "feed title");304     assert!(305         feed.contains("<subtitle>Feed desc</subtitle>"),306         "feed subtitle"307     );308     assert!(309         index.contains("og:title\" content=\"Feed Hub\""),310         "og:title agrees with feed title"311     );312     assert!(313         index.contains("og:description\" content=\"Feed desc\""),314         "og:description agrees with feed subtitle"315     );316 }317 318 #[test]319 fn entrypoint_and_dockerfile_pass_both_envs() {320     let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));321     let ep = std::fs::read_to_string(manifest.join("entrypoint.sh")).expect("entrypoint.sh");322     for needle in [323         "HU13_REPO_NAME",324         "HU13_DESCRIPTION",325         "--repo-name",326         "--description",327         "\"$HU13_REPO_NAME\"",328         "\"$HU13_DESCRIPTION\"",329     ] {330         assert!(ep.contains(needle), "entrypoint passes {needle}");331     }332     let docker = std::fs::read_to_string(manifest.join("Dockerfile")).expect("Dockerfile");333     for needle in ["HU13_REPO_NAME", "HU13_DESCRIPTION"] {334         assert!(docker.contains(needle), "Dockerfile defaults {needle}");335     }336 }