server/lib/tests/the_front_unfolds_and_servers_link.rs
//! ★ **HOL-FLOW-FRONT: the front unfolds, and another Holger leads to its own flow**
//! (Henrik's Q1 and Q4 of HOL-FLOW-SMART).
//!
//! * **Q1.** Main did not know its front's doors, hosts or routes, so the front could not be
//! unfolded. The front now REPORTS them (`holger_downstream_rules::front`, proven under the
//! key its console key and the box's share), main keeps the last report, and the flow draws
//! the front unfolded at level 2 and level 3 with the same box and port rules as every other
//! machine. Nothing that is not reported is drawn: a front that never reported stays closed.
//! * **Q4.** A box that is another Holger server leads to THAT server's own flow, at its
//! console, when this server knows the console's address (a downstream's proven report
//! carries it); otherwise there is nothing to click.
#![cfg_attr(not(feature = "sync"), allow(dead_code, unused_imports))]
use std::collections::BTreeSet;
use std::io::Write as _;
use holger_server_lib::flow::{
self, ConsoleView, DownstreamView, EstateBox, FlowGraph, FrontDoorView, FrontRouteView,
FrontView, Measured, NodeKind, Open, RonSummary,
};
fn holger_of(ron: &str) -> holger_server_lib::Holger {
let mut f = tempfile::NamedTempFile::new().unwrap();
f.write_all(ron.as_bytes()).unwrap();
holger_server_lib::read_ron_config(f.path()).expect("the fixture declaration parses")
}
const KEY: &str = "abababababababababababababababababababababababababababababababab";
fn ron() -> String {
format!(
r#"(
repositories: [
(ron_name: "crates", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
ron_out: Some((ron_storage_endpoint: "store", ron_exposed_endpoint: "web"))),
],
exposed_endpoints: [
(ron_name: "web", ron_url: "127.0.0.1:50051", ron_http_url: Some("127.0.0.1:8443")),
],
storage_endpoints: [ (ron_name: "store", ron_storage_type: "znippy", ron_path: "/data/store") ],
sources: [ (name: "team", kind: upload) ],
sync: Some((name: "main", downstreams: [(name: "lab-1", key: "{KEY}")])),
)"#
)
}
const FRONT: &str = "estate:front";
const WWW: &str = "estate:front:host:www.acme.example";
const REPO: &str = "estate:front:host:repo.acme.example";
const PAY: &str = "estate:front:host:pay.acme.example";
const HTTPS: &str = "estate:front:door:https";
const HTTP: &str = "estate:front:door:http";
/// What the front of `acme.example` reported: its two web doors, the console's name, the
/// repository's name carried to main, and a payment vendor's callbacks.
fn front_report() -> FrontView {
let route = |host: &str, to: &str, what: &str| FrontRouteView {
host: host.into(),
door: "https".into(),
to: to.into(),
what: what.into(),
};
FrontView {
front: "front".into(),
last_seen_unix_ms: 1_799_999_970_000,
seen_ago_ms: 30_000,
doors: vec![
FrontDoorView {
name: "https".into(),
listen: "tcp/443".into(),
what: "the web face".into(),
},
FrontDoorView {
name: "http".into(),
listen: "tcp/80".into(),
what: "the challenges".into(),
},
],
routes: vec![
route("www.acme.example", "console", "holger-ui"),
route(
"repo.acme.example",
"main",
"main http :8443, gRPC :50051 (holger.*)",
),
route(
"pay.acme.example",
"monetize",
"a payment vendor's callbacks",
),
],
console_url: "https://www.acme.example".into(),
}
}
fn summary(console_url: &str) -> Option<RonSummary> {
Some(RonSummary {
boundary: "Fetch".into(),
repositories: vec!["pypi".into()],
first_import: "admin".into(),
console_url: console_url.into(),
..Default::default()
})
}
/// main, a front, a console that called a minute ago, and three downstreams: lab-1 reported
/// its console's address, lab-2 reported none, lab-3 reported one that is no plain URL.
fn estate(front: Option<FrontView>) -> Measured {
let down = |name: &str, s: Option<RonSummary>| DownstreamView {
name: name.into(),
via: "front".into(),
state: "up".into(),
headline: "current".into(),
last_seen_unix_ms: 1_799_999_940_000,
seen_ago_ms: 60_000,
paired: true,
summary: s,
..Default::default()
};
Measured {
now_unix: 1_800_000_000,
estate_name: "acme".into(),
estate: vec![
EstateBox {
name: "main".into(),
role: "primary".into(),
this_box: true,
state: "up".into(),
..Default::default()
},
EstateBox {
name: "front".into(),
role: "front".into(),
state: "up".into(),
..Default::default()
},
],
console: Some(ConsoleView {
last_seen_unix_ms: 1_799_999_940_000,
seen_ago_ms: 60_000,
}),
downstreams: vec![
down("lab-1", summary("https://www.lab-1.example/")),
down("lab-2", summary("")),
down("lab-3", summary("javascript:alert(1)")),
],
front,
..Default::default()
}
}
fn graph(front: Option<FrontView>) -> FlowGraph {
flow::measure(&FlowGraph::from_config(&holger_of(&ron())), &estate(front))
}
/// The ports a graph's lines attach to on `machine`, as `(port, edge kind)`.
fn ports_on(g: &FlowGraph, machine: &str) -> BTreeSet<(String, String)> {
let prefix = format!("{machine}/");
g.edges
.iter()
.flat_map(|e| {
[e.from_port.clone(), e.to_port.clone()]
.into_iter()
.flatten()
.filter(|p| p.starts_with(&prefix))
.map(move |p| (p, e.kind.css().to_string()))
})
.collect()
}
fn fact(g: &FlowGraph, node: &str, label: &str) -> Option<String> {
g.node(node)?
.facts
.iter()
.find(|r| r.label == label)
.map(|r| r.cell.render())
}
/// ★ **Q1: the front unfolds to what it reported**, at level 2 and 3, its lines ending at the
/// same ports on its border as at level 1: the console comes in at the console's name, the
/// names routed to main leave by the `main` port, and a name routed elsewhere (monetize) is
/// drawn inside the front with no line to main.
#[cfg(feature = "sync")]
#[test]
fn the_front_unfolds_to_the_doors_and_routes_it_reported_with_the_ports_of_level_one() {
let g = graph(Some(front_report()));
assert!(
g.openable.contains(&FRONT.to_string()),
"a front that reported has parts: {:?}",
g.openable
);
assert_eq!(
fact(&g, FRONT, "console").as_deref(),
Some("https://www.acme.example")
);
assert_eq!(fact(&g, FRONT, "reported").as_deref(), Some("30s ago"));
let l1 = g.zoom(&Open::none());
assert!(
l1.node_ids()
.iter()
.all(|n| !n.starts_with("estate:front:")),
"level 1: nothing inside the front"
);
for e in ["estate:console->estate:front", "estate:front->estate:main"] {
assert!(
l1.edge_ids().contains(e),
"level 1 has {e}: {:?}",
l1.edge_ids()
);
}
let at_one = ports_on(&l1, FRONT);
assert_eq!(
at_one,
[
(
"estate:front/console".to_string(),
"consoleedge".to_string()
),
("estate:front/main".to_string(), "fronts".to_string()),
]
.into_iter()
.collect(),
"the console's port and main's port on the front's border"
);
let (_, links) = l1.render_with_links();
assert!(
links
.iter()
.any(|l| l.href == "flow?open=front" && l.label == "unfold front"),
"a click on the front unfolds it: {links:?}"
);
let l2 = g.zoom(&Open::parse("front"));
assert_eq!((l2.level, l2.open.clone()), (2, vec![FRONT.to_string()]));
for part in [HTTPS, HTTP, WWW, REPO, PAY] {
let n = l2
.node(part)
.unwrap_or_else(|| panic!("the front's part {part}: {:?}", l2.node_ids()));
assert_eq!(
n.machine.as_deref(),
Some(FRONT),
"{part} runs on the front"
);
}
assert_eq!(l2.node(WWW).unwrap().kind, NodeKind::Host);
assert_eq!(l2.node(HTTPS).unwrap().kind, NodeKind::Door);
assert_eq!(fact(&l2, REPO, "to").as_deref(), Some("main"));
assert_eq!(fact(&l2, HTTPS, "listens").as_deref(), Some("tcp/443"));
let edges = l2.edge_ids();
for e in [
format!("estate:console->{WWW}"),
format!("{WWW}->estate:main"),
format!("{REPO}->estate:main"),
format!("{HTTPS}->{REPO}"),
format!("{HTTPS}->{PAY}"),
] {
assert!(edges.contains(&e), "level 2 has {e}: {edges:?}");
}
assert!(
!edges.contains(&format!("{PAY}->estate:main")),
"a name routed to monetize does not reach main"
);
assert!(
!edges.contains("estate:front->estate:main"),
"unfolded, the line leaves from the names that carry it"
);
assert_eq!(ports_on(&l2, FRONT), at_one, "the same ports at level 2");
assert!(
l2.node("door:web").is_none(),
"main stays folded at open=front"
);
let l3 = g.zoom(&Open::All);
assert_eq!(l3.level, 3);
let edges = l3.edge_ids();
assert!(
edges.contains(&format!("{REPO}->door:web")),
"level 3: the repository's name ends at main's door: {edges:?}"
);
assert_eq!(ports_on(&l3, FRONT), at_one, "the same ports at level 3");
let to_main = l3
.edges
.iter()
.find(|e| e.from == REPO && e.to == "door:web")
.unwrap();
assert_eq!(
(to_main.from_port.as_deref(), to_main.to_port.as_deref()),
(Some("estate:front/main"), Some("estate:main/doors"))
);
let svg = l2.render_svg();
for part in [WWW, REPO, HTTPS] {
assert!(
svg.contains(&format!("data-node=\"{part}\"")),
"the drawing shows {part}"
);
}
}
/// ★ **Q1: nothing that is not reported is drawn**: a front that never reported (an older
/// front, a box fed no key) is one closed box that says so, with its line to main.
#[cfg(feature = "sync")]
#[test]
fn a_front_that_never_reported_stays_one_closed_box() {
let g = graph(None);
assert!(
!g.openable.contains(&FRONT.to_string()),
"nothing to unfold: {:?}",
g.openable
);
assert_eq!(fact(&g, FRONT, "report").as_deref(), Some("no report"));
let all = g.zoom(&Open::All);
assert!(
all.node_ids()
.iter()
.all(|n| !n.starts_with("estate:front:")),
"no part of the front is invented"
);
assert!(all.edge_ids().contains("estate:front->door:web"));
assert!(all.edge_ids().contains("estate:console->estate:front"));
}
/// ★ **Q4: a box that is another Holger server leads to its own flow, at its console**, when
/// this server knows the console's address; it is not clickable otherwise, and an address
/// that is no plain URL is never a link.
#[cfg(feature = "sync")]
#[test]
fn another_holger_servers_box_leads_to_its_own_flow_only_when_its_console_is_known() {
let g = graph(Some(front_report()));
let link = |id: &str| g.node(id).unwrap().link.clone();
assert_eq!(
link("downstream:lab-1").as_deref(),
Some("https://www.lab-1.example/flow")
);
assert_eq!(link("downstream:lab-2"), None, "no console known: no link");
assert_eq!(link("downstream:lab-3"), None, "no plain URL: no link");
assert_eq!(link("estate:main"), None, "this server is no other server");
assert_eq!(link(FRONT), None, "the front is no Holger server");
for open in [Open::none(), Open::parse("lab-1"), Open::All] {
let z = g.zoom(&open);
let (svg, links) = z.render_with_links();
let own: Vec<_> = links
.iter()
.filter(|l| !l.href.starts_with("flow?"))
.collect();
assert_eq!(own.len(), 1, "one link out, to lab-1's own flow: {links:?}");
assert_eq!(own[0].href, "https://www.lab-1.example/flow");
assert_eq!(own[0].label, "open lab-1's own flow at its console");
assert!(
svg.contains(r#"data-own-flow="downstream:lab-1""#)
&& !svg.contains(r#"data-own-flow="downstream:lab-2""#),
"the drawing carries it too"
);
assert!(!svg.contains("javascript:"), "never a script link");
// It lies over lab-1's box: inside the place that unfolds or folds it.
let toggle = links
.iter()
.find(|l| l.href.starts_with("flow?") && l.label.ends_with(" lab-1"))
.unwrap_or_else(|| panic!("lab-1 unfolds: {links:?}"));
let o = own[0];
assert!(
o.left >= toggle.left - 1e-9
&& o.left + o.width <= toggle.left + toggle.width + 1e-9
&& o.top >= toggle.top - 1e-9,
"over lab-1's box: {o:?} in {toggle:?}"
);
assert!(
links.iter().position(|l| l == o) > links.iter().position(|l| l == toggle),
"laid after the box's own link, so it is on top"
);
}
assert_eq!(
fact(&g, "downstream:lab-1", flow::OWN_FLOW_ROW).as_deref(),
Some("↗ www.lab-1.example/flow")
);
}
// ── the door: the front's report over main's HTTP door, end to end ─────────────────
mod door {
use super::*;
use holger_server_lib::front_report::front::{
self as rules, FrontDoor, FrontReport, FrontRoute, Sent,
};
use holger_server_lib::{read_ron_config, wire_holger, Holger};
use traits::test_support::hold_port;
const CONSOLE: &str = "c0nsole-bearer-0123456789abcdefghij";
pub struct Running {
pub data: tempfile::TempDir,
_cfg: tempfile::NamedTempFile,
_holger: Holger,
pub base: String,
}
pub fn start() -> Running {
let mut last = String::new();
for _ in 0..8 {
match try_start(hold_port(), hold_port()) {
Ok(r) => return r,
Err(e) => last = e,
}
}
panic!("server never started; last error: {last}");
}
fn try_start(grpc: u16, http: u16) -> Result<Running, String> {
let data = tempfile::tempdir().unwrap();
let d = data.path().display().to_string();
std::fs::create_dir_all(data.path().join("audit")).unwrap();
let ron = format!(
r#"(
repositories: [
(ron_name: "crates", ron_repo_type: "raw", ron_upstreams: [], ron_in: None,
ron_out: Some((ron_storage_endpoint: "storage", ron_exposed_endpoint: "door")),
ron_data_dir: Some("{d}/crates")),
],
exposed_endpoints: [
(ron_name: "door", ron_url: "127.0.0.1:{grpc}", ron_http_url: Some("127.0.0.1:{http}")),
],
storage_endpoints: [ (ron_name: "storage", ron_storage_type: "znippy", ron_path: "{d}") ],
audit: (dir: "{d}/audit", enabled: true),
auth: Some((
methods: [ StaticToken(subject: "console", token: Some("{CONSOLE}")) ],
roles: {{"console": Admin}},
)),
)"#
);
let mut cfg = tempfile::NamedTempFile::new().unwrap();
cfg.write_all(ron.as_bytes()).unwrap();
let mut holger: Holger = read_ron_config(cfg.path()).map_err(|e| format!("config: {e}"))?;
holger
.instantiate_backends()
.map_err(|e| format!("backends: {e}"))?;
wire_holger(&mut holger).map_err(|e| format!("wire: {e}"))?;
holger.start().map_err(|e| format!("start: {e}"))?;
Ok(Running {
data,
_cfg: cfg,
_holger: holger,
base: format!("http://127.0.0.1:{http}"),
})
}
pub async fn wait_healthy(c: &reqwest::Client, base: &str) {
for _ in 0..200 {
if let Ok(r) = c.get(format!("{base}/healthz")).send().await {
if r.status() == 200 {
return;
}
}
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
}
panic!("gateway never became healthy at {base}");
}
pub fn report(at_unix: i64) -> FrontReport {
FrontReport {
front: "front".into(),
at_unix,
doors: vec![FrontDoor {
name: "https".into(),
listen: "tcp/443".into(),
what: "the web face".into(),
}],
routes: vec![FrontRoute {
host: "repo.acme.example".into(),
door: "https".into(),
to: rules::TO_MAIN.into(),
what: "main http :8443".into(),
}],
console_url: "https://www.acme.example".into(),
}
}
pub async fn post(c: &reqwest::Client, base: &str, sent: &Sent) -> (u16, String) {
let r = c
.post(format!("{base}{}", rules::PATH))
.json(sent)
.send()
.await
.expect("the door answers");
(r.status().as_u16(), r.text().await.unwrap_or_default())
}
pub async fn flow(c: &reqwest::Client, base: &str, open: &str) -> serde_json::Value {
let r = c
.get(format!("{base}/-/flow.json?open={open}"))
.bearer_auth(CONSOLE)
.send()
.await
.expect("the flow door answers");
assert_eq!(r.status(), 200);
r.json().await.unwrap()
}
}
/// ★ **Q1 end to end: the front reports over main's HTTP door, main keeps it, and the flow
/// unfolds the front to it**. No bearer: the proof is the credential. A report under another
/// key, an old one, one from a front the pairing does not name, and one to a box fed no key
/// are refused by name and nothing is kept; a GET is no report.
#[cfg(feature = "sync")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_front_reports_over_mains_http_door_and_the_flow_unfolds_it() {
use holger_server_lib::front_report::{self, front::Sent};
use holger_server_lib::topology::{set_pairing, PairedBox, Pairing};
set_pairing(Some(Pairing {
estate: "acme".into(),
me: "main".into(),
boxes: vec![
PairedBox {
name: "main".into(),
role: "primary".into(),
..Default::default()
},
PairedBox {
name: "front".into(),
role: "front".into(),
..Default::default()
},
],
not_after_unix: 0,
}));
let (main, bare) = (door::start(), door::start());
let c = reqwest::Client::new();
door::wait_healthy(&c, &main.base).await;
door::wait_healthy(&c, &bare.base).await;
let key = front_report::front::key_of(&[0x42; 32]);
assert_eq!(front_report::keep_key(main.data.path(), &key), Ok(true));
assert_eq!(
front_report::keep_key(main.data.path(), &key),
Ok(false),
"an unchanged key is a no-op"
);
let now = holger_server_lib::clock::now_unix_secs() as i64;
let sent = |r: holger_server_lib::front_report::front::FrontReport, k: &[u8; 32]| Sent {
proof: front_report::front::proof(k, &r),
report: r,
};
// Before any report: the front is one closed box.
let v = door::flow(&c, &main.base, "all").await;
let ids: Vec<String> = v["nodes"]
.as_array()
.unwrap()
.iter()
.map(|n| n["id"].as_str().unwrap().to_string())
.collect();
assert!(
ids.contains(&FRONT.to_string()),
"the pairing names a front: {ids:?}"
);
assert!(
!ids.iter().any(|n| n.starts_with("estate:front:")),
"{ids:?}"
);
// Refused, by name, and nothing kept.
let other = front_report::front::key_of(&[0x43; 32]);
let mut stranger = door::report(now);
stranger.front = "vm9".into();
for (why, s, status) in [
("another key", sent(door::report(now), &other), 403),
("an old report", sent(door::report(now - 3600), &key), 403),
(
"a front the pairing does not name",
sent(stranger, &key),
403,
),
] {
let (code, words) = door::post(&c, &main.base, &s).await;
assert_eq!(code, status, "{why}: {words}");
assert!(words.contains("[holger-front-5]"), "{why}: {words}");
}
assert!(
front_report::kept(main.data.path()).is_none(),
"nothing kept"
);
let (code, words) = door::post(&c, &bare.base, &sent(door::report(now), &key)).await;
assert_eq!(code, 409, "a box fed no key: {words}");
assert!(words.contains("fed no front console key"), "{words}");
let r = c
.get(format!("{}{}", main.base, front_report::front::PATH))
.send()
.await
.unwrap();
assert_eq!(r.status(), 405, "a GET is no report");
// The front's own report: kept, and drawn.
let (code, words) = door::post(&c, &main.base, &sent(door::report(now), &key)).await;
assert_eq!(code, 200, "{words}");
let kept = front_report::kept(main.data.path()).expect("kept");
assert_eq!(kept.report.routes[0].host, "repo.acme.example");
assert_eq!(
front_report::console_url(main.data.path()),
"https://www.acme.example"
);
let v = door::flow(&c, &main.base, "front").await;
let node = |id: &str| {
v["nodes"]
.as_array()
.unwrap()
.iter()
.find(|n| n["id"] == id)
.cloned()
};
assert_eq!(v["level"], 2);
let repo = node(REPO).expect("the repository's name, inside the front");
assert_eq!(
(repo["kind"].as_str(), repo["box"].as_str()),
(Some("host"), Some(FRONT))
);
assert!(node(HTTPS).is_some(), "the https door, inside the front");
assert!(
v["edges"]
.as_array()
.unwrap()
.iter()
.any(|e| e["from"] == REPO
&& e["to"] == "estate:main"
&& e["from_port"] == "estate:front/main"),
"the name's line leaves by the front's main port: {}",
v["edges"]
);
}