//! Integration coverage for `hive-forge credential-helper get` — the //! `host=` check in `verbs::credential_helper::run` exists to close a //! reported credential leak, and git invokes the helper fresh over //! stdin/env on every fetch/push. The check is inlined in `run()` //! (real stdin, real env, real stdout), so it can only be driven the way //! git actually drives it: run the compiled binary. use std::io::Write as _; use std::path::{Path, PathBuf}; use std::process::{Command, Stdio}; const TOKEN: &str = "test-token-value"; /// A unique scratch `HYPERHIVE_STATE_DIR`, seeded with a `forge-token` /// file. Matches the manual-tmpdir pattern `hive-c0re`'s /// `dashboard::state_files` tests use — this crate carries no `tempfile` /// dependency to reach for instead. fn scratch_state_dir(tag: &str) -> PathBuf { let ts = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .map_or(0, |d| d.as_nanos()); let dir = std::env::temp_dir().join(format!("hive-forge-cred-test-{tag}-{ts}")); std::fs::create_dir_all(&dir).expect("create scratch state dir"); std::fs::write(dir.join("forge-token"), format!("{TOKEN}\n")).expect("seed forge-token"); dir } /// Run `hive-forge credential-helper get` against `base_url` + /// `state_dir`, feeding `request` as the git credential-protocol /// request body on stdin. Returns `(exit success, stdout, stderr)`. fn run_get(base_url: &str, state_dir: &Path, request: &str) -> (bool, String, String) { run_get_with(base_url, state_dir, None, request) } /// [`run_get`], with `HIVE_FORGE_TOKEN_FILE` set to `fetched` when given and /// removed otherwise, so the caller's own environment never leaks in. fn run_get_with( base_url: &str, state_dir: &Path, fetched: Option<&Path>, request: &str, ) -> (bool, String, String) { let mut cmd = Command::new(env!("CARGO_BIN_EXE_hive-forge")); match fetched { Some(p) => cmd.env("HIVE_FORGE_TOKEN_FILE", p), None => cmd.env_remove("HIVE_FORGE_TOKEN_FILE"), }; let mut child = cmd .arg("credential-helper") .arg("get") .env("HIVE_FORGE_URL", base_url) .env("HYPERHIVE_STATE_DIR", state_dir) .env_remove("HIVE_LABEL") .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() .expect("spawn hive-forge credential-helper get"); child .stdin .take() .expect("piped stdin") .write_all(request.as_bytes()) .expect("write credential request to stdin"); let out = child.wait_with_output().expect("wait for hive-forge"); ( out.status.success(), String::from_utf8_lossy(&out.stdout).into_owned(), String::from_utf8_lossy(&out.stderr).into_owned(), ) } /// This is the check the module doc says exists to close a reported /// credential leak: refusing to hand the forge token to a host git /// didn't ask this invocation to serve. If the host comparison were ever /// inverted, a mismatched host would get the token instead of being /// refused. #[test] fn credential_helper_get_refuses_a_mismatched_host_and_withholds_the_token() { let dir = scratch_state_dir("mismatch"); let (ok, stdout, stderr) = run_get( "http://forge.internal.example", &dir, "protocol=https\nhost=not-the-forge.example\n", ); assert!(!ok, "a mismatched host= must fail, stderr={stderr}"); assert!( !stdout.contains(TOKEN), "the token must never reach stdout on a host mismatch: stdout={stdout}" ); let _ = std::fs::remove_dir_all(&dir); } /// The other side of the same check: a `host=` that matches the /// configured forge must still print the credentials, or every real git /// fetch/push would break. #[test] fn credential_helper_get_answers_when_host_matches() { let dir = scratch_state_dir("match"); let (ok, stdout, stderr) = run_get( "http://forge.internal.example", &dir, "protocol=https\nhost=forge.internal.example\n", ); assert!(ok, "a matching host= must succeed, stderr={stderr}"); assert!(stdout.contains("username="), "stdout={stdout}"); assert!( stdout.contains(&format!("password={TOKEN}")), "stdout={stdout}" ); let _ = std::fs::remove_dir_all(&dir); } /// Backwards compatibility: the request shape git sent before this check /// existed — no `host=` line at all — must still work. #[test] fn credential_helper_get_still_works_with_no_host_line() { let dir = scratch_state_dir("nohost"); let (ok, stdout, stderr) = run_get("http://forge.internal.example", &dir, "protocol=https\n"); assert!( ok, "a request with no host= line must not be treated as a failure, stderr={stderr}" ); assert!( stdout.contains(&format!("password={TOKEN}")), "stdout={stdout}" ); let _ = std::fs::remove_dir_all(&dir); } /// The token the agent fetched from the swarm secret store wins over the /// state-dir file the hive used to write, so a rotated token takes effect /// even while the old file is still on disk. #[test] fn the_fetched_token_wins_over_the_state_file() { let dir = scratch_state_dir("fetched"); let fetched = dir.join("fetched-token"); std::fs::write(&fetched, "fetched-token-value\n").expect("seed fetched token"); let (ok, stdout, stderr) = run_get_with( "http://forge.internal.example", &dir, Some(&fetched), "protocol=https\nhost=forge.internal.example\n", ); assert!(ok, "stderr={stderr}"); assert!( stdout.contains("password=fetched-token-value"), "stdout={stdout}" ); assert!(!stdout.contains(TOKEN), "stdout={stdout}"); let _ = std::fs::remove_dir_all(&dir); } /// An agent whose token was not minted yet has the variable set and no file /// behind it; it keeps working off the state-dir file. #[test] fn a_fetched_token_that_is_not_there_falls_back_to_the_state_file() { let dir = scratch_state_dir("fallback"); let missing = dir.join("never-fetched"); let (ok, stdout, stderr) = run_get_with( "http://forge.internal.example", &dir, Some(&missing), "protocol=https\nhost=forge.internal.example\n", ); assert!(ok, "stderr={stderr}"); assert!( stdout.contains(&format!("password={TOKEN}")), "stdout={stdout}" ); let _ = std::fs::remove_dir_all(&dir); }