- `fetch_tarball.rs` (5 tests, sin red ni rootfs): genera tar.gz local, lo sirve vía `file://` y ejercita el path completo: strip_components default vs 0, sha256 mismatch que NO envenena caché ni deja .partial, reuso de caché en segunda llamada con la fuente original eliminada, y rechazo de un archivo cacheado con contenido distinto al sha esperado. - `end_to_end_grep.rs` (1 test, gated): grep 3.12 desde GNU, build + hidratación + ejecución en bwrap+Alpine + cache-hit en segunda build. Doble skip: HAMMER_NETWORK_TESTS!=1 o .dev-fs sin bootstrap. Comparte .dev-fs/cache con la dev para no repagar musl. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
165 lines
6.2 KiB
Rust
165 lines
6.2 KiB
Rust
//! 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 `<dir>/inner/{a.txt,sub/b.txt}` y empaqueta `inner/` en `<dir>/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<usize>) -> 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 <sha>.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}");
|
|
}
|