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

nordisk/gunnar

crates/gunnar-front/tests/stderr_to_bootlog.rs

//! **monetize's lines reach the installer's log door** (2026-09-28).
//!
//! MEASURED on test2's front (gunnar 50c3e69) through `estate logs front`: a
//! tenant's order (`subscription.bought … state="pending"`) and then not one
//! line from monetize-server, which runs in the front's process and says
//! everything with `eprintln!`. The front now takes fd 2 over
//! (`gunnar_common::stderr_capture`) before tracing is installed, so what is
//! printed there is a log line in BOOT_LOG — and the VM's redaction withholds
//! a Stripe secret before it is held.
//!
//! Its own test binary: it takes over this process's fd 2 and installs the
//! global subscriber, as `gunnar-front`'s `main` does.

use std::sync::Arc;
use std::time::{Duration, Instant};

use gunnar_server::installer_listener::{serve_door, Door, BOOT_LOG};
use nordisk_pairing::installer::{read_log, Material};
use nordisk_pairing::installer_link::DirectPath;
use nordisk_pairing::mtls;
use nordisk_pairing::seed::{Kind, Seed, TunnelKind};

#[test]
fn a_monetize_line_reaches_read_log_and_a_stripe_secret_is_withheld() {
    // The order main() does it in.
    gunnar_server::installer_listener::capture();
    gunnar_common::stderr_capture::start().expect("fd 2 is taken over");
    gunnar_common::init_tracing(false);

    // What monetize-server says, on fd 2 as its `eprintln!` does. Written
    // through `io::stderr()` and not with `eprintln!` here: libtest captures
    // the macro inside a test, and the bytes would never reach fd 2.
    use std::io::Write as _;
    let mut fd2 = std::io::stderr();
    for line in [
        "monetize-server: checkout gunnar/alice/2026-09-28+pack_bytes=10737418240 minted at stripe (session cs_test_buyflow1)",
        "monetize-server: stripe: signing the webhook check with whsec_5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH",
        "monetize-server: stripe: POST /v1/checkout/sessions with sk_live_51HabcDEF",
        "monetize-server: grow FAILED: the cloud refused the resize",
    ] {
        writeln!(fd2, "{line}").unwrap();
    }

    // The installer reads the VM's log over mutual TLS, as `estate logs` does.
    let h = Duration::from_secs(3600);
    let ca = mtls::InstallerCa::mint("gunnar.rs", h).unwrap();
    let boot = ca.issue_bootstrap(h).unwrap();
    let seed = Seed {
        estate: "gunnar.rs".into(),
        tunnel: TunnelKind::Wireguard,
        front: nordisk_pairing::seed::Front::default(),
        kind: Kind::Front,
        installer_ca_pem: ca.cert_pem.clone(),
        bootstrap_cert_pem: boot.cert_pem,
        bootstrap_key_pem: boot.key_pem,
        peers_expected: 3,
        mullvad: None,
        tailscale: None,
        installer_link: None,
    };
    let me = ca.issue_installer(h).unwrap();
    let data = tempfile::tempdir().unwrap();
    let vm: Arc<dyn nordisk_pairing::bootstrap::Vm> =
        Arc::new(gunnar_front::vm::FrontVm::open(data.path(), seed.clone()).unwrap());
    let l = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
    let std::net::SocketAddr::V4(at) = l.local_addr().unwrap() else {
        panic!("v4")
    };
    let ca_pem = seed.installer_ca_pem.clone();
    std::thread::spawn(move || {
        serve_door(vm.as_ref(), &BOOT_LOG, &ca_pem, l, Door::Install, &|_| {})
    });
    let m = Material { ca, me, link: None };

    let deadline = Instant::now() + Duration::from_secs(20);
    let lines = loop {
        let chunk = read_log(Box::new(DirectPath { at }), &m, 0).expect("the log door answers");
        if chunk.lines.iter().any(|l| l.contains("grow FAILED")) || Instant::now() > deadline {
            break chunk.lines;
        }
        std::thread::sleep(Duration::from_millis(100));
    };

    let order = lines
        .iter()
        .find(|l| l.contains("cs_test_buyflow1"))
        .unwrap_or_else(|| panic!("monetize's line reached read_log: {lines:#?}"));
    assert!(
        order.contains(" INFO stderr: monetize-server: checkout"),
        "said through tracing, with a level and the `stderr` target: {order}"
    );
    assert!(
        order.starts_with("20") && order.contains('Z'),
        "and a timestamp: {order}"
    );
    let failed = lines
        .iter()
        .find(|l| l.contains("grow FAILED"))
        .expect("the failure line");
    assert!(
        failed.contains(" WARN stderr: "),
        "a line that says it failed is a WARN: {failed}"
    );
    for secret in [
        "whsec_",
        "sk_live_",
        "5WbX5kEWLlfzsGNjH64I8lOOqUB6e8FH",
        "51HabcDEF",
    ] {
        assert!(
            !lines.iter().any(|l| l.contains(secret)),
            "`{secret}` never reaches the installer: {lines:#?}"
        );
    }
    assert!(
        lines
            .iter()
            .any(|l| l.starts_with("<<WITHHELD:") && l.contains("token-class")),
        "the two secret lines are withheld as token-class: {lines:#?}"
    );
}