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

nordisk/holger

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