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

nordisk/holger

holger-cli/tests/thin_and_airgapped.rs

//! **Thin, and it dials only `--at`** (.nornir/SUITES.md: the `cli` variant is a THIN
//! client; Holger's default is the airgap install).
//!
//! * Thin: no local database and no server code. Its dependencies are exactly the client
//!   half of tonic, the codec, the runtime, the argument parser, the code registry and a
//!   SHA-256 from ring, which tonic's TLS already links (the export's digest is checked here, RA2-5),
//!   plus the offline tools it carries (HOL-PRODUCTS): the bundle crate and the courier,
//!   the courier without its online connectors;
//!   `holger-server-lib` is a DEV dependency only (the tests' in-process server).
//! * Airgapped: the only network code it links is tonic's channel (no `reqwest`, `ureq`,
//!   `hyper` client of its own, no `server` or `router`), and that channel is opened to
//!   `--at`: a command against a listener sees its connection arrive there.

use std::collections::BTreeSet;

fn manifest() -> toml::Table {
    let text = std::fs::read_to_string(concat!(env!("CARGO_MANIFEST_DIR"), "/Cargo.toml")).unwrap();
    text.parse().unwrap()
}

#[test]
fn its_dependencies_are_the_client_half_and_nothing_else() {
    let m = manifest();
    let deps = m["dependencies"].as_table().expect("[dependencies]");
    let names: BTreeSet<&str> = deps.keys().map(|k| k.as_str()).collect();
    let allowed: BTreeSet<&str> = [
        "tonic",
        "tonic-prost",
        "prost",
        "tokio",
        "clap",
        "holger-errcode",
        "ring",
        "holger-update-bundle",
        "holger-agent-cli",
        "anyhow",
    ]
    .into_iter()
    .collect();
    let courier = deps["holger-agent-cli"]
        .as_table()
        .expect("the courier as a table");
    assert_eq!(
        courier.get("default-features").and_then(|v| v.as_bool()),
        Some(false),
        "the courier's connectors are `online`, never the default"
    );
    assert_eq!(
        names, allowed,
        "holger-cli's runtime dependencies changed: a thin client links only these"
    );
    let tonic = deps["tonic"].as_table().expect("tonic as a table");
    assert_eq!(
        tonic.get("default-features").and_then(|v| v.as_bool()),
        Some(false),
        "tonic's defaults carry the server and the router"
    );
    let features: BTreeSet<&str> = tonic["features"]
        .as_array()
        .unwrap()
        .iter()
        .filter_map(|v| v.as_str())
        .collect();
    for server_side in ["server", "router", "transport"] {
        assert!(
            !features.contains(server_side),
            "tonic `{server_side}` is server code"
        );
    }
    assert!(
        features.contains("channel"),
        "the one door out is tonic's channel"
    );
    assert!(
        !m.get("target")
            .is_some_and(|t| t.to_string().contains("dependencies")),
        "no target-specific dependencies sneak a client in"
    );
    let dev = m["dev-dependencies"].as_table().unwrap();
    assert!(
        dev.contains_key("holger-server-lib"),
        "the server is the tests' only"
    );
}

#[test]
fn the_only_connection_is_to_at() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    rt.block_on(async {
        let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap();
        let port = listener.local_addr().unwrap().port();
        let accepted = tokio::spawn(async move { listener.accept().await.map(|(_, peer)| peer) });
        let home = tempfile::tempdir().unwrap();
        let h = home.path().display().to_string();
        let env = move |k: &str| (k == "HOLGER_CLI_HOME").then(|| h.clone());
        let at = format!("127.0.0.1:{port}");
        // Not a gRPC server: the call fails, but only after it dialled --at.
        let run = holger_cli::run_args(["holger-cli", "--at", &at, "health"], env);
        let _ = tokio::time::timeout(std::time::Duration::from_secs(5), run).await;
        let peer = tokio::time::timeout(std::time::Duration::from_secs(5), accepted)
            .await
            .expect("the CLI dialled --at")
            .unwrap()
            .unwrap();
        assert!(peer.ip().is_loopback());
    });
}

#[test]
fn a_door_nobody_listens_on_says_holger_cli_4() {
    let rt = tokio::runtime::Runtime::new().unwrap();
    let port = std::net::TcpListener::bind("127.0.0.1:0")
        .unwrap()
        .local_addr()
        .unwrap()
        .port();
    let e = rt
        .block_on(holger_cli::run_args(
            ["holger-cli", "--at", &format!("127.0.0.1:{port}"), "health"],
            |_| None,
        ))
        .unwrap_err();
    assert_eq!(e.code().as_deref(), Some("holger-cli-4"), "{e}");
    let bad = rt
        .block_on(holger_cli::run_args(["holger-cli", "no-such-verb"], |_| {
            None
        }))
        .unwrap_err();
    assert_eq!(bad.code().as_deref(), Some("holger-cli-1"), "{bad}");
    let missing = rt
        .block_on(holger_cli::run_args(
            [
                "holger-cli",
                "--at",
                "127.0.0.1:1",
                "--token-file",
                "/no/such/file",
                "health",
            ],
            |_| None,
        ))
        .unwrap_err();
    assert_eq!(missing.code().as_deref(), Some("holger-cli-2"), "{missing}");
}