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

nordisk/monetize

crates/monetize-ui-server/tests/screens_floors.rs

//! **Authorisation floors, and the sentences that must arrive whole.**
//!
//! Every route that moves money or writes state is refused for a Viewer and
//! for an Operator with **403 and zero calls recorded on the mock** — then
//! allowed for an Admin with the counter moving, because a counter that is
//! never seen to move proves nothing (measure, never assert). The break-glass token retires the
//! moment an ADMIN passkey exists, and not before: a viewer's passkey does not
//! close the door the first admin has to walk through.
//!
//! And `can_absorb`'s refusal — the sealed-disk paragraph — reaches the JSON
//! verbatim, down to `"not another disk"`, the clause a truncation takes first.

#[path = "../../../uitest/src/fixture.rs"]
mod fixture;
#[path = "../../../uitest/src/mock.rs"]
mod mock;
mod screens_rig;

use fixture as fx;
use http::{Method, StatusCode};
use monetize_ui_server::auth::StoredCred;
use monetize_ui_server::role::Role;
use screens_rig::{Auth, Rig};
use serde_json::json;

/// Every money route with a body an Admin could actually send.
fn money_routes() -> Vec<(Method, &'static str, serde_json::Value)> {
    vec![
        (Method::POST, "/api/purchase", json!({ "product": "gunnar", "tenant": "bjorn", "caps": { "pack_bytes": fx::CAP_10.raw }, "vendor": "mockpay" })),
        (Method::POST, "/api/purchase/abandon", json!({ "reference": fx::STUCK_REF, "reason": "gunnar will never accept the fact" })),
        (Method::POST, "/api/backup", json!({})),
        (Method::POST, "/api/entitlement/gunnar/bjorn", json!({ "state": "paid", "paid_until_unix_ms": fx::PAID_UNTIL })),
    ]
}

/// (r) RED first: with the Viewer's expected status changed to `OK` every row
/// fails with 403 — and with the mutation counter asserted `> 0` after the
/// viewer pass it fails at 0, which is the half that matters.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn every_money_route_is_refused_below_admin_with_nothing_reaching_monetize() {
    let rig = Rig::up().await;
    for role in [Role::Viewer, Role::Operator] {
        let cookie = rig.cookie(role);
        for (method, path, body) in money_routes() {
            let (status, reply, _) = rig.call(method.clone(), path, Auth::Cookie(&cookie), Some(body)).await;
            assert_eq!(status, StatusCode::FORBIDDEN, "{role} on {method} {path}: {reply}");
            assert!(reply.as_str().is_some_and(|s| s.contains("admin")), "the refusal names the role needed: {reply}");
        }
        assert_eq!(rig.mock.calls.mutations(), 0, "a refusal for {role} must not reach monetize");
    }
    // Anonymous: 401, and still nothing.
    for (method, path, body) in money_routes() {
        let (status, ..) = rig.call(method.clone(), path, Auth::None, Some(body)).await;
        assert_eq!(status, StatusCode::UNAUTHORIZED, "anonymous {method} {path}");
    }
    assert_eq!(rig.mock.calls.mutations(), 0);

    // ADMIN: every one goes through, and the counter is seen to move by
    // exactly one per route — the proof the zero above was a measurement.
    let admin = rig.cookie(Role::Admin);
    for (i, (method, path, body)) in money_routes().into_iter().enumerate() {
        let (status, reply, _) = rig.call(method.clone(), path, Auth::Cookie(&admin), Some(body)).await;
        assert_eq!(status, StatusCode::OK, "admin on {method} {path}: {reply}");
        assert_eq!(rig.mock.calls.mutations(), i + 1, "{method} {path} must have reached monetize exactly once");
    }
    let c = &rig.mock.calls;
    assert_eq!(
        (c.starts.lock().unwrap().len(), c.abandons.lock().unwrap().len(), c.exports.lock().unwrap().len(), c.sets.lock().unwrap().len()),
        (1, 1, 1, 1)
    );
    // And what reached monetize is what was asked, not a shape the BFF invented.
    let set = c.sets.lock().unwrap()[0].clone();
    assert_eq!((set.product.as_str(), set.fact.as_ref().unwrap().tenant.as_str(), set.fact.as_ref().unwrap().source.as_str()), ("gunnar", "bjorn", "operator"));
    assert!(set.fact.as_ref().unwrap().signature.is_empty(), "\"sign it for me\"");
    let start = c.starts.lock().unwrap()[0].clone();
    assert_eq!((start.tenant.as_str(), start.caps["pack_bytes"], start.vendor.as_str()), ("bjorn", fx::CAP_10.raw, "mockpay"), "the ORDER reached monetize as it was placed");
    // A QUOTE is a read: a viewer may ask, and nothing is spent by it.
    let (status, q, _) = rig.call(Method::GET, &format!("/api/quote?product=gunnar&tenant=bob&caps=pack_bytes%3D{}", 100u64 << 30), Auth::Cookie(&rig.cookie(Role::Viewer)), None).await;
    assert_eq!(status, StatusCode::OK, "{q}");
    assert_eq!(q["delta"]["pack_bytes"].as_u64(), Some(90u64 << 30), "the 90 they lack: {q}");
    assert_eq!(q["price_minor"].as_u64(), Some(fx::ALICE_QUOTE_90_GIB_MINOR), "{q}");
    assert_eq!(c.quotes.lock().unwrap().len(), 1);
    assert_eq!(c.mutations(), 4, "a quote moved nothing");
}

/// (r) ★ **A narrated site's boot logs are an ADMIN's, reads included** (lane N2): a Viewer
/// and an Operator are refused with 403 naming the role, a stranger with 401 — the page and
/// the text route both; an Admin passes the gate (this rig holds no operator credential, so
/// what answers it is the "no operator credential" sentence, never a 403).
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_narrate_logs_are_refused_below_admin() {
    let rig = Rig::up().await;
    let routes = ["/api/narrate/test3-holger-rs", "/api/narrate/test3-holger-rs/main"];
    for role in [Role::Viewer, Role::Operator] {
        let cookie = rig.cookie(role);
        for path in routes {
            let (status, reply, _) = rig.call(Method::GET, path, Auth::Cookie(&cookie), None).await;
            assert_eq!(status, StatusCode::FORBIDDEN, "{role} on GET {path}: {reply}");
            assert!(reply.as_str().is_some_and(|s| s.contains("admin")), "the refusal names the role needed: {reply}");
        }
    }
    for path in routes {
        let (status, ..) = rig.call(Method::GET, path, Auth::None, None).await;
        assert_eq!(status, StatusCode::UNAUTHORIZED, "anonymous GET {path}");
    }
    let admin = rig.cookie(Role::Admin);
    for path in routes {
        let (status, reply, _) = rig.call(Method::GET, path, Auth::Cookie(&admin), None).await;
        assert_ne!(status, StatusCode::FORBIDDEN, "admin on GET {path}: {reply}");
        assert_ne!(status, StatusCode::UNAUTHORIZED, "admin on GET {path}: {reply}");
    }
}

/// (r) **The reads are a Viewer's; a stranger and a forged cookie get nothing.**
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn every_screen_endpoint_needs_a_session_and_a_viewer_is_enough() {
    let rig = Rig::up().await;
    let viewer = rig.cookie(Role::Viewer);
    for path in ["/api/products", "/api/products/gunnar/tenants", "/api/tenants/gunnar/alice", "/api/ledger?product=gunnar", "/api/events", "/api/health", "/api/quote?product=gunnar&tenant=bob&caps=pack_bytes%3D107374182400"] {
        let (status, ..) = rig.call(Method::GET, path, Auth::None, None).await;
        assert_eq!(status, StatusCode::UNAUTHORIZED, "{path} answered a stranger");
        let (status, ..) = rig.call(Method::GET, path, Auth::Cookie("bm90LWEtc2Vzc2lvbg.bm90LWEtbWFj"), None).await;
        assert_eq!(status, StatusCode::UNAUTHORIZED, "{path} accepted a forged cookie");
        let (status, ..) = rig.call(Method::GET, path, Auth::Cookie(&viewer), None).await;
        assert_eq!(status, StatusCode::OK, "{path} refused a viewer");
    }
    // A browser navigation (Accept: text/html) is sent to the login page
    // rather than handed a 401 it cannot act on.
    let (status, headers, _) = rig.raw(Method::GET, "/api/products", Auth::None, None, &[("accept", "text/html")]).await;
    assert!(status.is_redirection(), "{status}");
    assert_eq!(headers.get("location").and_then(|v| v.to_str().ok()), Some("/login"));
}

/// (r) **The break-glass token retires when an ADMIN passkey exists — not
/// when any passkey does.**
///
/// The second half is the one `tests/api.rs` does not hold: a viewer enrolled
/// first must not spend the token, or the first admin could never be enrolled.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_admin_token_retires_on_the_first_admin_passkey_and_not_on_a_viewers() {
    let rig = Rig::up().await;
    let cred = |id: &str, role: Role| StoredCred { id: id.into(), x: vec![0; 32], y: vec![0; 32], sign_count: 0, label: "yubikey".into(), created: 0, user: "bob".into(), role, last_used: None, disabled: false };

    let (status, ..) = rig.call(Method::GET, "/api/products", Auth::Token, None).await;
    assert_eq!(status, StatusCode::OK, "the token opens the console before any passkey exists");
    let (_, st, _) = rig.call(Method::GET, "/auth/status", Auth::None, None).await;
    assert_eq!(st["bootstrap"], true);

    rig.webauthn.adopt(cred("viewer-key", Role::Viewer)).unwrap();
    let (status, ..) = rig.call(Method::GET, "/api/products", Auth::Token, None).await;
    assert_eq!(status, StatusCode::OK, "a VIEWER passkey must not retire the token: the first admin still has to be enrolled with it");
    let (_, st, _) = rig.call(Method::GET, "/auth/status", Auth::None, None).await;
    assert_eq!(st["bootstrap"], true, "still bootstrapping");

    rig.webauthn.adopt(cred("admin-key", Role::Admin)).unwrap();
    let (status, ..) = rig.call(Method::GET, "/api/products", Auth::Token, None).await;
    assert_eq!(status, StatusCode::UNAUTHORIZED, "the token is spent the moment an admin passkey exists");
    let (status, ..) = rig.call(Method::POST, "/api/backup", Auth::Token, None).await;
    assert_eq!(status, StatusCode::UNAUTHORIZED, "…on a money route too");
    assert_eq!(rig.mock.calls.exports.lock().unwrap().len(), 0);
    let (_, st, _) = rig.call(Method::GET, "/auth/status", Auth::None, None).await;
    assert_eq!(st["bootstrap"], false);
    let (status, ..) = rig.call(Method::GET, "/api/products", Auth::Cookie(&rig.cookie(Role::Admin)), None).await;
    assert_eq!(status, StatusCode::OK, "the passkey session is the credential now");
}

/// (r) **A refusal carries its sentence intact.** Two places the sealed
/// paragraph reaches the console, and both must end with the clause that
/// stops the wrong purchase.
///
/// RED first: with `SEALED_REFUSAL` truncated at "short 10.0 GiB" in the mock,
/// the `ends_with` fails — which is what a BFF that shortened messages would
/// look like from here.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_sealed_refusal_and_the_growth_paragraph_arrive_verbatim_to_their_last_clause() {
    let rig = Rig::up().await;
    let admin = rig.cookie(Role::Admin);
    let body = json!({ "product": "gunnar", "tenant": "alice", "caps": { "pack_bytes": 100u64 << 30, "cache_bytes": fx::CAP_CACHE_20.raw }, "vendor": "mockpay" });
    let (status, out, _) = rig.call(Method::POST, "/api/purchase", Auth::Cookie(&admin), Some(body)).await;
    assert_eq!(status, StatusCode::BAD_REQUEST, "{out}");
    assert_eq!(out["code"], "failedprecondition", "the console reads the code to head the box correctly");
    let msg = out["message"].as_str().expect("a refusal carries a message");
    assert_eq!(msg, fx::SEALED_REFUSAL, "not shortened, summarised or re-worded");
    assert!(msg.ends_with("not another disk"), "the LAST clause: {msg}");
    assert!(msg.contains("BILLED AND INVISIBLE") && msg.contains("short 10.0 GiB"));
    assert_eq!(rig.mock.calls.starts.lock().unwrap().len(), 0, "a refused order is not recorded as one");
    // The QUOTE before it says the same, as a quote and not an error: the
    // price of what was refused is still what the operator sees.
    let (status, q, _) = rig.call(Method::GET, &format!("/api/quote?product=gunnar&tenant=alice&caps=pack_bytes%3D{}", 100u64 << 30), Auth::Cookie(&admin), None).await;
    assert_eq!(status, StatusCode::OK, "{q}");
    assert_eq!(q["can_serve"], false);
    assert_eq!(q["reason"].as_str(), Some(fx::SEALED_REFUSAL), "the same sentence, whole");
    assert_eq!(q["price_minor"].as_u64(), Some(fx::ALICE_QUOTE_90_GIB_MINOR), "the price of the 90 GiB refused: {q}");
    assert_eq!(q["cost_minor"].as_u64(), Some(fx::ALICE_QUOTE_90_GIB_COST_MINOR));

    // The same paragraph as the growth fact behind `⚠ cannot grow`, on both
    // product-shaped routes.
    for path in ["/api/products/gunnar/tenants", "/api/products"] {
        let body = rig.get(path).await;
        let fleet = if path == "/api/products" { &body["products"][0]["fleet"] } else { &body["fleet"] };
        assert_eq!(fleet["growth"]["state"], "sealed", "{path}");
        let detail = fleet["growth"]["detail"].as_str().unwrap();
        assert_eq!(detail, fx::SEALED_DETAIL, "{path}");
        assert!(detail.ends_with("not another disk"), "{path}: {detail}");
    }
}