Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/gunnar

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}"
        );
    }
}