deploy/upcloud/tests/one_upcloud_client.rs
//! **The re-image talks to UpCloud through `upcloud_api::UpCloudApi` and
//! nothing else, and which UpCloud answers is decided by `--mock-api` alone.**
//!
//! Until 2026-09-21 `main.rs` built its client as `Api::new(DEFAULT_BASE)` —
//! `https://api.upcloud.com/1.3`, no override — so `private-gunnar-ops`'s
//! `cargo xtask mock reimage --apply`, which chains into this binary, would have
//! re-imaged the LIVE appliance with the estate's token while its banner said
//! FAKE. These are the guards that keep that unrepresentable.
use std::process::Command;
/// Nothing under `src/` spells the provider or builds an API path. Every
/// spelling lives in edda's `upcloud-api`, where the one wire builds requests
/// for the account and for a mock by the same code.
#[test]
fn nothing_in_this_crate_spells_the_provider() {
let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
upcloud_api::guard::assert_none(&[&src], &[]);
}
fn run(env: &[(&str, &str)], args: &[&str]) -> (bool, String) {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_gunnar-upcloud"));
for k in upcloud_api::MOCK_ENVS {
cmd.env_remove(k);
}
// Never a real credential in reach of these runs.
cmd.env_remove("UPCLOUD_TOKEN")
.env("HOME", std::env::temp_dir())
.env_remove("GUNNAR_SITE");
for (k, v) in env {
cmd.env(k, v);
}
let out = cmd.args(args).output().expect("spawn gunnar-upcloud");
(
out.status.success(),
format!(
"{}{}",
String::from_utf8_lossy(&out.stdout),
String::from_utf8_lossy(&out.stderr)
),
)
}
/// **FAILS-BEFORE**: at `a01a8e7` this binary had no idea a mock existed; a
/// shell carrying `UPCLOUD_API_BASE` got the account. Now it refuses BY NAME,
/// before the site file, the ISO or a token is read.
#[test]
fn a_shell_aimed_at_a_fake_with_no_flag_refuses_before_anything_is_read() {
for k in upcloud_api::MOCK_ENVS {
let (ok, out) = run(
&[(k, "http://127.0.0.1:8099")],
&["--iso", "/nonexistent.iso", "--dry-run"],
);
assert!(!ok, "{out}");
assert!(
out.contains("mock-variable-on-an-account-run") && out.contains(k),
"{k}: {out}"
);
assert!(
!out.contains("iso "),
"nothing may be read before the cloud is decided: {out}"
);
}
}
#[test]
fn a_mock_api_off_loopback_is_refused_by_name() {
let (ok, out) = run(
&[],
&[
"--iso",
"/nonexistent.iso",
"--dry-run",
"--mock-api",
"http://10.13.0.247:8099",
],
);
assert!(!ok && out.contains("mock-base-not-loopback"), "{out}");
let (ok, out) = run(
&[],
&[
"--iso",
"/nonexistent.iso",
"--dry-run",
"--mock-api",
"https://api.upcloud.com/1.3",
],
);
assert!(
!ok && out.contains("mock-base-not-loopback"),
"the flag cannot name the account: {out}"
);
}
/// The mock is chosen out loud, first: the banner is the first line, and it
/// says FAKE — whatever the run fails on afterwards.
#[test]
fn a_mock_run_says_fake_before_anything_else() {
let (_, out) = run(
&[(upcloud_api::MOCK_BASE_ENV, "http://127.0.0.1:8099")],
&[
"--iso",
"/nonexistent.iso",
"--dry-run",
"--mock-api",
"http://127.0.0.1:8099",
],
);
let first = out.lines().find(|l| !l.trim().is_empty()).unwrap_or("");
assert!(
first.contains("MOCK_UPCLOUD at http://127.0.0.1:8099/1.3") && first.contains("FAKE"),
"{out}"
);
assert!(!out.contains("THE ACCOUNT"), "{out}");
}
/// **A mock run dials loopback and nothing else.** The mock's pool once held
/// real hosts; a mock site naming one is refused before a byte moves.
#[test]
fn a_mock_site_naming_a_real_host_is_refused_before_anything_moves() {
let dir = std::env::temp_dir().join(format!("gunnar-upcloud-loopback-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let iso = dir.join("m.iso");
std::fs::write(&iso, b"x").unwrap();
for (verify, front) in [
("94.237.30.222:2222", "root@127.0.0.1:9"),
("127.0.0.1:9", "root@203.0.113.10"),
] {
let site = dir.join("site.toml");
std::fs::write(
&site,
format!("server_uuid = \"u\"\nverify_addr = \"{verify}\"\nfront = \"{front}\"\nzone = \"se-sto1\"\n"),
)
.unwrap();
let (ok, out) = run(
&[],
&[
"--iso",
iso.to_str().unwrap(),
"--site",
site.to_str().unwrap(),
"--dry-run",
"--mock-api",
"http://127.0.0.1:1",
],
);
assert!(
!ok && out.contains("mock-dials-outside-loopback"),
"{verify} / {front}: {out}"
);
}
}