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