//! Tests del modo tarball del fetch — completamente locales, sin red ni rootfs. //! //! Estrategia: //! - Genero un tarball en un tempdir con contenido conocido. //! - Calculo su sha256 real con la misma crate (`sha2`) que usa el fetch. //! - Sirvo el tarball vía URL `file://` (curl la maneja nativamente). //! - Llamo a `hammer_build::fetch::fetch` con una receta TOML que apunta a esa URL. //! //! Cubre: happy path con strip_components, mismatch de sha256 no envenena la caché, reuso //! de caché en segunda llamada, strip_components = 0 vs 1. use std::path::{Path, PathBuf}; use std::process::Command; use hammer_build::fetch::fetch; use hammer_core::Recipe; use sha2::{Digest, Sha256}; /// Crea `/inner/{a.txt,sub/b.txt}` y empaqueta `inner/` en `/payload.tar.gz`. /// El tarball tiene UN componente top-level (`inner/`) — strip_components=1 lo elimina. fn make_tarball(dir: &Path) -> PathBuf { let inner = dir.join("inner"); std::fs::create_dir_all(inner.join("sub")).unwrap(); std::fs::write(inner.join("a.txt"), b"hola desde a\n").unwrap(); std::fs::write(inner.join("sub/b.txt"), b"hola desde b\n").unwrap(); let tar_path = dir.join("payload.tar.gz"); let st = Command::new("tar") .arg("-czf") .arg(&tar_path) .arg("-C") .arg(dir) .arg("inner") .status() .expect("spawn tar"); assert!(st.success(), "tar -czf falló"); tar_path } fn sha256_hex(path: &Path) -> String { let mut f = std::fs::File::open(path).unwrap(); let mut hasher = Sha256::new(); std::io::copy(&mut f, &mut hasher).unwrap(); hex::encode(hasher.finalize()) } fn recipe_for(url: &str, sha: &str, strip: Option) -> Recipe { let strip_line = match strip { Some(n) => format!("strip_components = {n}\n"), None => String::new(), }; let toml = format!( r#" name = "fixture" version = "0" [source] tarball = "{url}" sha256 = "{sha}" {strip_line} [build] "# ); Recipe::from_toml(&toml).unwrap() } #[test] fn happy_path_strip_components_default_one() { let fix = tempfile::tempdir().unwrap(); let tar_path = make_tarball(fix.path()); let sha = sha256_hex(&tar_path); let url = format!("file://{}", tar_path.display()); let recipe = recipe_for(&url, &sha, None); let work_root = tempfile::tempdir().unwrap(); let tree = fetch(&recipe, work_root.path()).expect("fetch tarball"); // strip_components=1 ⇒ no debe quedar `inner/`, los archivos suben un nivel. assert!(tree.join("a.txt").is_file(), "a.txt debe existir en la raíz del tree"); assert!(tree.join("sub/b.txt").is_file(), "sub/b.txt debe existir"); assert!(!tree.join("inner").exists(), "inner/ debe haber sido stripped"); // El tarball quedó cacheado por su sha256. let cached = work_root.path().join("tarballs").join(format!("{sha}.tar")); assert!(cached.is_file(), "tarball debe quedar cacheado en {}", cached.display()); } #[test] fn strip_components_zero_preserves_top_level() { let fix = tempfile::tempdir().unwrap(); let tar_path = make_tarball(fix.path()); let sha = sha256_hex(&tar_path); let url = format!("file://{}", tar_path.display()); let recipe = recipe_for(&url, &sha, Some(0)); let work_root = tempfile::tempdir().unwrap(); let tree = fetch(&recipe, work_root.path()).expect("fetch tarball strip=0"); assert!(tree.join("inner/a.txt").is_file(), "con strip=0, inner/ debe existir"); } #[test] fn sha256_mismatch_fails_and_does_not_poison_cache() { let fix = tempfile::tempdir().unwrap(); let tar_path = make_tarball(fix.path()); let url = format!("file://{}", tar_path.display()); let wrong_sha = "0000000000000000000000000000000000000000000000000000000000000000"; let recipe = recipe_for(&url, wrong_sha, None); let work_root = tempfile::tempdir().unwrap(); let err = fetch(&recipe, work_root.path()).unwrap_err().to_string(); assert!(err.contains("sha256 mismatch"), "msg = {err}"); // Crítico: la caché NO debe contener un archivo con el sha incorrecto, porque la próxima // corrida lo daría por bueno sin re-verificar (sólo verificamos cuando lo descargamos). let cached = work_root.path().join("tarballs").join(format!("{wrong_sha}.tar")); assert!(!cached.exists(), "caché no debe contener {}", cached.display()); // Tampoco debe quedar el .partial colgado. let partial = work_root .path() .join("tarballs") .join(format!("{wrong_sha}.tar.partial")); assert!(!partial.exists(), "no debe quedar partial"); } #[test] fn second_fetch_reuses_cached_tarball() { let fix = tempfile::tempdir().unwrap(); let tar_path = make_tarball(fix.path()); let sha = sha256_hex(&tar_path); let url = format!("file://{}", tar_path.display()); let recipe = recipe_for(&url, &sha, None); let work_root = tempfile::tempdir().unwrap(); let _ = fetch(&recipe, work_root.path()).expect("first fetch"); // Para forzar que la segunda corrida NO pueda descargar, "rompo" la fuente: muevo el // tarball original a otro lado. Si el fetch va a la caché, sigue funcionando; si intenta // re-descargar, falla. std::fs::remove_file(&tar_path).unwrap(); let tree2 = fetch(&recipe, work_root.path()).expect("second fetch (debe usar caché)"); assert!(tree2.join("a.txt").is_file()); } #[test] fn cached_tarball_with_wrong_content_is_rejected() { // Si un atacante o un bug deja en la caché un archivo con el nombre .tar pero // contenido distinto, NO debemos confiar. Verificamos sha256 incluso para entradas // de la caché. let fix = tempfile::tempdir().unwrap(); let tar_path = make_tarball(fix.path()); let sha = sha256_hex(&tar_path); let url = format!("file://{}", tar_path.display()); let work_root = tempfile::tempdir().unwrap(); let tarballs = work_root.path().join("tarballs"); std::fs::create_dir_all(&tarballs).unwrap(); // Plantar un archivo basura con el nombre del sha esperado. std::fs::write(tarballs.join(format!("{sha}.tar")), b"contenido FALSO").unwrap(); let recipe = recipe_for(&url, &sha, None); let err = fetch(&recipe, work_root.path()).unwrap_err().to_string(); assert!(err.contains("sha256 mismatch"), "msg = {err}"); }