tests/clone_origin.rs 13.8 KiB 395 lines Raw
1 //! Clone-from-server (m6, f0rge issue #32; dumb-clone #54):2 //! `--public-origin` / `HU13_PUBLIC_ORIGIN` (flag > env > unset),3 //! clone-block both-mode asserts, serve `/repo.bundle` byte-identical,4 //! live `git clone http://host:port/repo.bundle` (smart, serve-only) +5 //! `git clone <origin>/repo.git` (dumb, any static server), entrypoint passthrough.6 7 use std::sync::atomic::{AtomicU64, Ordering};8 9 static SEQ: AtomicU64 = AtomicU64::new(0);10 11 use std::io::{BufRead, BufReader, Read, Write};12 use std::net::TcpStream;13 use std::path::{Path, PathBuf};14 use std::process::{Child, Command, Stdio};15 use std::sync::Mutex;16 use std::time::Duration;17 18 static ENV_LOCK: Mutex<()> = Mutex::new(());19 20 fn bin() -> &'static str {21     env!("CARGO_BIN_EXE_hu13")22 }23 24 fn fresh(prefix: &str) -> PathBuf {25     let dir = std::env::temp_dir().join(format!(26         "hu13-origin-{prefix}-{}-{}",27         std::process::id(),28         std::time::SystemTime::now()29             .duration_since(std::time::UNIX_EPOCH)30             .map(|d| d.as_nanos())31             .unwrap_or(0)32             .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))33     ));34     std::fs::create_dir_all(&dir).unwrap();35     dir36 }37 38 fn git(repo: &Path, args: &[&str]) {39     let st = Command::new("git")40         .arg("-C")41         .arg(repo)42         .args(args)43         .status()44         .expect("spawn git");45     assert!(st.success(), "git {args:?} failed");46 }47 48 fn fixture_repo() -> PathBuf {49     let dir = fresh("repo");50     git(&dir, &["init", "-b", "main"]);51     git(&dir, &["config", "user.name", "hu13-test"]);52     git(&dir, &["config", "user.email", "[email protected]"]);53     git(&dir, &["config", "commit.gpgsign", "false"]);54     std::fs::write(dir.join("README.md"), "# origin fixture\n").unwrap();55     std::fs::write(dir.join("hello.txt"), "hello\n").unwrap();56     git(&dir, &["add", "-A"]);57     git(&dir, &["commit", "-m", "init"]);58     dir59 }60 61 fn set_env(k: &str, v: Option<&str>) {62     unsafe {63         match v {64             Some(val) => std::env::set_var(k, val),65             None => std::env::remove_var(k),66         }67     }68 }69 70 fn build_with(repo: &Path, extra: &[&str]) -> PathBuf {71     let out = fresh("out");72     let st = Command::new(bin())73         .arg("build")74         .arg(repo)75         .arg("-o")76         .arg(&out)77         .args(extra)78         .status()79         .expect("spawn hu13 build");80     assert!(st.success(), "build exits 0");81     out82 }83 84 // ---- block asserts -------------------------------------------------------85 86 #[test]87 fn unset_mode_is_bundle_first_with_server_note() {88     let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());89     set_env("HU13_PUBLIC_ORIGIN", None);90     let repo = fixture_repo();91     let out = build_with(&repo, &[]);92     let index = std::fs::read_to_string(out.join("index.html")).expect("index");93     assert!(index.contains("clone-block"), "clone block present");94     assert!(95         index.contains("git clone repo.bundle"),96         "bundle-first cmd, got: {}",97         &index[index.find("clone-block").unwrap_or(0)..]98             [..2000.min(index.len() - index.find("clone-block").unwrap_or(0))]99     );100     assert!(101         index.contains("served from this server: ask the host for its address"),102         "server note in unset mode"103     );104     assert!(105         !index.contains("git clone http"),106         "no absolute bundle cmd when unset"107     );108     assert!(index.contains("/repo.bundle"), "bundle linked");109     assert!(index.contains("/repo.tar.gz"), "tarball linked");110     set_env("HU13_PUBLIC_ORIGIN", None);111 }112 113 #[test]114 fn public_origin_primary_is_absolute_bundle_with_snapshot_note() {115     let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());116     set_env("HU13_PUBLIC_ORIGIN", None);117     let repo = fixture_repo();118     let origin = "http://127.0.0.1:39313";119     let out = build_with(&repo, &["--public-origin", origin]);120     let index = std::fs::read_to_string(out.join("index.html")).expect("index");121     assert!(122         index.contains(&format!("git clone {origin}/repo.git")),123         "primary dumb clone cmd"124     );125     assert!(index.contains("exact snapshot of"), "snapshot note present");126     assert!(index.contains("main"), "branch in snapshot note");127     assert!(index.contains("/repo.tar.gz"), "secondary tarball link");128     assert!(129         index.contains(&format!("curl -O {origin}/repo.bundle")),130         "bundle dumb download secondary"131     );132     assert!(133         out.join("repo.git").join("HEAD").exists(),134         "repo.git emitted"135     );136     assert!(137         out.join("repo.git").join("info/refs").exists(),138         "update-server-info ran"139     );140     set_env("HU13_PUBLIC_ORIGIN", None);141 }142 143 #[test]144 fn public_origin_with_clone_url_shows_mirror_source() {145     let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());146     set_env("HU13_PUBLIC_ORIGIN", None);147     let repo = fixture_repo();148     let origin = "http://10.0.0.5:39313";149     let mirror = "https://example.com/mirror.git";150     let out = build_with(&repo, &["--public-origin", origin, "--clone-url", mirror]);151     let index = std::fs::read_to_string(out.join("index.html")).expect("index");152     assert!(153         index.contains(&format!("git clone {origin}/repo.git")),154         "public origin still primary (dumb)"155     );156     assert!(index.contains("mirror source"), "mirror source label");157     assert!(index.contains(mirror), "origin remote shown");158     // Primary (dumb) must come before the mirror source line.159     let primary = index160         .find(&format!("git clone {origin}/repo.git"))161         .expect("primary pos");162     let mirror_pos = index.find("mirror source").expect("mirror pos");163     assert!(primary < mirror_pos, "primary before mirror source");164     set_env("HU13_PUBLIC_ORIGIN", None);165 }166 167 #[test]168 fn public_origin_flag_beats_env_and_trailing_slash_stripped() {169     let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());170     // Env only -> used.171     set_env("HU13_PUBLIC_ORIGIN", Some("http://env.example:39313/"));172     let repo = fixture_repo();173     let out = build_with(&repo, &[]);174     let index = std::fs::read_to_string(out.join("index.html")).expect("index");175     assert!(176         index.contains("git clone http://env.example:39313/repo.git"),177         "env origin used, trailing slash stripped (no //)"178     );179     assert!(!index.contains("//repo.git"), "no doubled slash");180     // Flag beats env.181     let out = build_with(&repo, &["--public-origin", "http://flag.example:39313"]);182     let index = std::fs::read_to_string(out.join("index.html")).expect("index");183     assert!(184         index.contains("git clone http://flag.example:39313/repo.git"),185         "flag wins over env"186     );187     assert!(!index.contains("env.example"), "env suppressed by flag");188     // Empty values fall back to unset.189     set_env("HU13_PUBLIC_ORIGIN", Some("   "));190     let out = build_with(&repo, &[]);191     let index = std::fs::read_to_string(out.join("index.html")).expect("index");192     assert!(193         index.contains("served from this server: ask the host for its address"),194         "empty env falls back to unset"195     );196     set_env("HU13_PUBLIC_ORIGIN", None);197 }198 199 // ---- entrypoint / docker -------------------------------------------------200 201 #[test]202 fn entrypoint_and_dockerfile_pass_public_origin() {203     let manifest = PathBuf::from(env!("CARGO_MANIFEST_DIR"));204     let ep = std::fs::read_to_string(manifest.join("entrypoint.sh")).expect("entrypoint.sh");205     for needle in [206         "HU13_PUBLIC_ORIGIN",207         "--public-origin",208         "\"$HU13_PUBLIC_ORIGIN\"",209     ] {210         assert!(ep.contains(needle), "entrypoint passes {needle}");211     }212     let docker = std::fs::read_to_string(manifest.join("Dockerfile")).expect("Dockerfile");213     assert!(214         docker.contains("HU13_PUBLIC_ORIGIN"),215         "Dockerfile defaults HU13_PUBLIC_ORIGIN"216     );217 }218 219 // ---- serve ---------------------------------------------------------------220 221 struct Server {222     child: Child,223     addr: String,224 }225 226 impl Drop for Server {227     fn drop(&mut self) {228         self.child.kill().ok();229         self.child.wait().ok();230     }231 }232 233 fn start_server(www: &Path, inbox: &Path) -> Server {234     let mut child = Command::new(bin())235         .arg("serve")236         .arg("--www")237         .arg(www)238         .arg("--inbox-dir")239         .arg(inbox)240         .arg("--listen")241         .arg("127.0.0.1:0")242         .stdout(Stdio::piped())243         .stderr(Stdio::piped())244         .spawn()245         .expect("spawn hu13 serve");246     let stdout = child.stdout.take().expect("piped stdout");247     let mut lines = BufReader::new(stdout).lines();248     let mut addr = String::new();249     for _ in 0..50 {250         match lines.next() {251             Some(Ok(line)) if line.contains("listening on ") => {252                 addr = line253                     .rsplit("listening on ")254                     .next()255                     .unwrap_or("")256                     .trim()257                     .to_string();258                 break;259             }260             Some(Ok(_)) => continue,261             _ => break,262         }263     }264     assert!(!addr.is_empty(), "serve printed no `listening on` line");265     std::thread::sleep(Duration::from_millis(100));266     Server { child, addr }267 }268 269 fn get(addr: &str, path: &str) -> Vec<u8> {270     let mut s = TcpStream::connect(addr).expect("connect");271     s.set_read_timeout(Some(Duration::from_secs(10))).unwrap();272     s.set_write_timeout(Some(Duration::from_secs(10))).unwrap();273     let req = format!("GET {path} HTTP/1.1\r\nHost: x\r\nConnection: close\r\n\r\n");274     s.write_all(req.as_bytes()).expect("write");275     let mut out = Vec::new();276     s.read_to_end(&mut out).expect("read");277     out278 }279 280 fn split_response(raw: &[u8]) -> (&[u8], &[u8]) {281     let pos = raw282         .windows(4)283         .position(|w| w == b"\r\n\r\n")284         .expect("header/body split");285     (&raw[..pos], &raw[pos + 4..])286 }287 288 #[test]289 fn serve_gets_repo_bundle_bytes_identical() {290     let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());291     set_env("HU13_PUBLIC_ORIGIN", None);292     let repo = fixture_repo();293     let out = build_with(&repo, &[]);294     let inbox = fresh("inbox");295     let srv = start_server(&out, &inbox);296 297     let want = std::fs::read(out.join("repo.bundle")).expect("repo.bundle on disk");298     assert!(!want.is_empty(), "bundle non-empty");299     let raw = get(&srv.addr, "/repo.bundle");300     let (head, body) = split_response(&raw);301     let head = String::from_utf8_lossy(head).into_owned();302     assert!(head.contains("200"), "GET /repo.bundle -> 200, got: {head}");303     assert!(304         head.contains("X-Content-Type-Options: nosniff"),305         "nosniff on bundle"306     );307     assert_eq!(body, want.as_slice(), "bundle bytes identical");308 309     // Tarball serves too.310     let want_tar = std::fs::read(out.join("repo.tar.gz")).expect("tarball on disk");311     let raw = get(&srv.addr, "/repo.tar.gz");312     let (head, body) = split_response(&raw);313     let head = String::from_utf8_lossy(head).into_owned();314     assert!(head.contains("200"), "GET /repo.tar.gz -> 200, got: {head}");315     assert_eq!(body, want_tar.as_slice(), "tarball bytes identical");316     set_env("HU13_PUBLIC_ORIGIN", None);317 }318 319 #[test]320 fn git_clone_over_http_bundle_works() {321     let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());322     set_env("HU13_PUBLIC_ORIGIN", None);323     let repo = fixture_repo();324     let out = build_with(&repo, &[]);325     let inbox = fresh("inbox");326     let srv = start_server(&out, &inbox);327     let url = format!("http://{}/repo.bundle", srv.addr);328     let dest_parent = fresh("cloned");329     let dest = dest_parent.join("cloned");330     let st = Command::new("git")331         .arg("clone")332         .arg("-b")333         .arg("main")334         .arg(&url)335         .arg(&dest)336         .status()337         .expect("spawn git clone");338     assert!(st.success(), "git clone {url} works");339     assert!(dest.join("README.md").exists(), "cloned README present");340     assert!(dest.join("hello.txt").exists(), "cloned file present");341     set_env("HU13_PUBLIC_ORIGIN", None);342 }343 344 #[test]345 fn dumb_repo_git_clones_file_and_http() {346     // #54: dist/repo.git is a bare dumb-HTTP dir — `git clone <path>` and347     // `git clone http://host/repo.git` both work, no smart HTTP, no GitHub.348     let _guard = ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner());349     set_env("HU13_PUBLIC_ORIGIN", None);350     let repo = fixture_repo();351     let out = build_with(&repo, &[]);352     let git_dir = out.join("repo.git");353     assert!(git_dir.join("HEAD").exists(), "HEAD emitted");354     assert!(git_dir.join("info/refs").exists(), "info/refs emitted");355     assert!(356         git_dir.join("objects/info/packs").exists(),357         "objects/info/packs emitted"358     );359     // File-path clone (works on file:// + any static host).360     let dest_parent = fresh("dumb-file");361     let dest = dest_parent.join("cloned");362     let st = Command::new("git")363         .arg("clone")364         .arg("-b")365         .arg("main")366         .arg(&git_dir)367         .arg(&dest)368         .status()369         .expect("spawn git clone");370     assert!(st.success(), "file clone of repo.git works");371     assert!(dest.join("README.md").exists(), "cloned README present");372     // HTTP clone via hu13 serve (same bytes Pages/nginx would serve).373     let inbox = fresh("inbox-dumb");374     let srv = start_server(&out, &inbox);375     let raw = get(&srv.addr, "/repo.git/info/refs");376     let (head, body) = split_response(&raw);377     let head = String::from_utf8_lossy(head).into_owned();378     assert!(head.contains("200"), "GET info/refs -> 200, got: {head}");379     assert!(!body.is_empty(), "info/refs non-empty");380     let url = format!("http://{}/repo.git", srv.addr);381     let dest_parent = fresh("dumb-http");382     let dest = dest_parent.join("cloned");383     let st = Command::new("git")384         .arg("clone")385         .arg("-b")386         .arg("main")387         .arg(&url)388         .arg(&dest)389         .status()390         .expect("spawn git clone");391     assert!(st.success(), "git clone {url} works");392     assert!(dest.join("README.md").exists(), "http cloned README");393     assert!(dest.join("hello.txt").exists(), "http cloned file");394     set_env("HU13_PUBLIC_ORIGIN", None);395 }