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

nordisk/gunnar-ui

uitest/src/oneshop.rs

//! **One shop** (owner, 2026-10-07): only gunnar.rs's monetize sells. A SOLD
//! (provided) Gunnar — a private estate gunnar.rs laid — is bought and grown
//! at its provider; its own monetize only executes, and its console offers no
//! purchase of its own.
//!
//! The rig runs a second console for it ([`Rig::start_provided`]): the same
//! gunnar and the same bench monetize, told it is acme's, sold by
//! `https://gunnar.rs` (`GUNNAR_UI_PROVIDER`), as the kit feeds a laid
//! estate's front. The rig's monetize still sells (it is the rig's one), so
//! what a sold estate's own monetize holds — no shop — is the kit's unit tests
//! and its functional test (`functest::sold_window`); these rows hold the
//! CONSOLE: no account door, no sign-up page, no route that starts an order,
//! and on the front page "New instance" at the provider.
//!
//! Pictures go to `<work>/shots/oneshop/`.

use crate::cdp::Cdp;
use crate::http;
use crate::json::esc;
use crate::matrix::{go, want, Row};
use crate::rig::Rig;

const NAMES: [&str; 3] = [
    "one-shop-a-sold-gunnar-names-no-account-door",
    "one-shop-a-sold-gunnar-starts-no-order",
    "one-shop-a-sold-gunnars-front-page-leads-to-its-providers-sign-up",
];

/// One shop, items 2 and 3 (owner 2026-10-07): on the rig's own console, which
/// sells — a visitor buying a private Gunnar first creates their account here
/// in ONE round trip, and that account's Storage names what it bought instead
/// of a room of nothing.
const SHOP_NAMES: [&str; 2] = [
    "one-shop-a-visitor-creates-their-account-here-and-comes-back-to-the-private-order",
    "one-shop-an-account-here-without-room-shows-no-room-of-nothing",
];

/// The visitor's account name and address (example.com: never a real one).
const ALMA: &str = "alma";

pub fn shop_rows(r: &Rig, rows: &mut Vec<Row>, c: &mut Cdp, boxadmin: &str) {
    if r.bench_control.is_none() {
        for name in SHOP_NAMES {
            go(rows, name, || Err("NOT RUN: no --bench-monetize (monetize makes the account)".into()));
        }
        return;
    }
    let o = r.origin();
    let dir = r.work.join("shots").join("oneshop");
    let _ = std::fs::create_dir_all(&dir);
    let mut alma = String::new();

    go(rows, SHOP_NAMES[0], || {
        let sess = c.isolated_page()?;
        let auth = c.add_authenticator(&sess)?;
        c.viewport(&sess, 1440, 900)?;
        c.navigate(&sess, &format!("{o}/signup?plan=private"))?;
        c.wait_for(&sess, "!document.getElementById('first-account').hidden", 20_000)
            .map_err(|_| "the private choice never offered a visitor their account first".to_owned())?;
        let said = c.eval_str(&sess, "document.getElementById('first-account').textContent + ' | ' + document.getElementById('go').textContent")?;
        want(said.contains("is where you see and grow what you bought") && said.ends_with("| Create your account at localhost"), || format!("the first step says {said:?}"))?;
        want(!said.to_lowercase().contains("free"), || format!("{said:?}"))?;
        c.screenshot(&sess, &dir.join("2-create-your-account-here.png"))?;
        c.eval(&sess, &format!(
            "(() => {{ const set = (id, v) => {{ const i = document.getElementById(id); Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value').set.call(i, v); i.dispatchEvent(new Event('input', {{bubbles: true}})); }}; set('account', {}); set('email', {}); document.getElementById('go').click(); return 1; }})()",
            esc(ALMA),
            esc("alma@example.com")
        ))?;
        // The account is made at once, and the browser follows THIS console's own welcome
        // page, carrying where to come back to.
        c.wait_for(&sess, "location.pathname === '/welcome'", 60_000)
            .map_err(|_| format!("the account was not made: the page says {}", http::cut(&c.eval_str(&sess, "document.body.innerText").unwrap_or_default(), 300)))?;
        let search = c.eval_str(&sess, "location.search")?;
        let params: Vec<(String, String)> = search.trim_start_matches('?').split('&').filter_map(|kv| kv.split_once('=')).map(|(k, v)| (k.to_owned(), v.to_owned())).collect();
        let next = params.iter().find(|(k, _)| k == "next").map(|(_, v)| v.clone()).unwrap_or_default();
        want(next == "%2Fsignup%3Fplan%3Dprivate" || next == "/signup?plan=private", || format!("the welcome page is not told to come back to the private order: {search}"))?;
        // monetize says the account is made (with the open sink there is no principal at
        // Gunnar yet, so the page waits for it: "monetize says the account … exists").
        c.wait_for(&sess, "document.body.innerText.includes('Your account is ready') || document.body.innerText.includes('says the account')", 30_000)
            .map_err(|_| format!("monetize did not say the account is made: {}", http::cut(&c.eval_str(&sess, "document.body.innerText").unwrap_or_default(), 300)))?;
        // The bench's monetize runs the OPEN signup sink, which creates no principal at
        // Gunnar (the shop's sink, monetize-signup-impl, does: its own tests). The rig
        // stands in for it: the box admin's invite for alma, opened with where to come back.
        let j = c.eval(
            boxadmin,
            &format!("fetch('/auth/passkey/invites', {{method:'POST', headers:{{'Content-Type':'application/json'}}, body: JSON.stringify({{user:{}, role:'viewer'}})}}).then(r => r.json())", esc(ALMA)),
        )?;
        let invite = j.text("url");
        want(invite.contains("/login?invite="), || format!("no invite for alma: {}", http::cut(&j.dump(), 200)))?;
        let path = invite.find("/login?").map(|i| invite[i..].to_owned()).unwrap_or(invite.clone());
        c.navigate(&sess, &format!("{o}{path}&next=%2Fsignup%3Fplan%3Dprivate"))?;
        c.wait_for(&sess, "!!document.getElementById('register')", 30_000).map_err(|_| "alma's registration never opened".to_owned())?;
        c.eval(&sess, "(() => { document.getElementById('label').value = 'alma laptop'; document.getElementById('register').click(); return 1; })()")?;
        c.wait_for(&sess, "document.getElementById('msg').classList.contains('ok') || document.getElementById('msg').classList.contains('err')", 30_000)?;
        let registered = c.eval_str(&sess, "document.getElementById('msg').textContent")?;
        want(registered.contains("registered"), || format!("the registration said {registered:?}"))?;
        want(c.credentials(&sess, &auth)?.len() == 1, || "alma's authenticator holds no key".to_owned())?;
        c.eval(&sess, "document.getElementById('login').click()")?;
        // …and back on the private choice, signed in, the account locked.
        c.wait_for(&sess, "location.pathname === '/signup' && document.getElementById('account').readOnly === true", 30_000)
            .map_err(|_| "after the passkey the visitor did not come back to the private order".to_owned())?;
        let back = c.eval_str(&sess, "document.getElementById('heading').textContent + ' | ' + document.getElementById('account').value + ' | ' + new URLSearchParams(location.search).get('plan')")?;
        want(back == format!("Sign up for a new private Gunnar | {ALMA} | private"), || format!("back on {back:?}"))?;
        c.screenshot(&sess, &dir.join("3-back-on-the-private-order-signed-in.png"))?;
        alma = sess;
        Ok(back)
    });

    go(rows, SHOP_NAMES[1], || {
        want(!alma.is_empty(), || "the row before failed".to_owned())?;
        // An account that holds only an instance is a FREE account here: its
        // Plan reads "Free" and its ONE button "Buy a plan" (owner 2026-10-17).
        c.navigate(&alma, &format!("{o}/account"))?;
        c.wait_for(&alma, "!!document.getElementById('account-buy-grow')", 30_000)
            .map_err(|_| "alma's Account drew no button".to_owned())?;
        let plan = c.eval_str(&alma, "document.getElementById('account-plan').innerText")?;
        want(plan.contains("Free") && plan.contains("Buy a plan"), || format!("alma's Plan: {}", http::cut(&plan, 300)))?;
        for never in ["0 B", "0 GB"] {
            want(!plan.contains(never), || format!("{never:?} on alma's Plan: {}", http::cut(&plan, 300)))?;
        }
        c.screenshot(&alma, &dir.join("4-account-of-an-account-without-room.png"))?;
        Ok("no room of nothing; Buy a plan".into())
    });
}

pub fn rows(r: &Rig, rows: &mut Vec<Row>, c: &mut Cdp) {
    let (Some(at), Some(origin)) = (r.provided_addr(), r.provided_origin()) else {
        for name in NAMES {
            go(rows, name, || Err("NOT RUN: no sold Gunnar's console (it runs beside the bench monetize: --bench-monetize)".into()));
        }
        return;
    };
    let dir = r.work.join("shots").join("oneshop");
    let _ = std::fs::create_dir_all(&dir);

    go(rows, NAMES[0], || {
        let s = http::req(&at, "GET", "/auth/status", &[], None)?;
        want(s.status == 200, || format!("/auth/status answered {}", s.status))?;
        let j = crate::json::parse(&s.body)?;
        let text = |k: &str| j.path(k).and_then(|v| v.as_str().map(str::to_owned));
        want(text("buy_url").is_none(), || format!("a sold Gunnar names an account door: {}", http::cut(&s.body, 300)))?;
        want(text("private_url").as_deref() == Some("https://gunnar.rs/signup?plan=private"), || format!("New instance is not the provider's: {:?}", text("private_url")))?;
        want(j.path("shop_here").and_then(|v| v.as_bool()) == Some(false), || "the console says it sells".to_owned())?;
        let line = text("shop_line").unwrap_or_default();
        want(line.contains("gunnar.rs"), || format!("no line naming the provider: {line:?}"))?;
        // One shop 4: the admin page's "Grow this instance" is drawn from
        // these two (gunnar-ui-web offers::grow_this_instance) and leads to
        // <provider>/signup?plan=grow&instance=<name>.
        want(text("provider").as_deref() == Some("https://gunnar.rs") && text("instance").as_deref() == Some("acme"), || format!("the console is not told its provider and its name: {:?} {:?}", text("provider"), text("instance")))?;
        Ok(format!("no account door; New instance {}; {line:?}; grown at {}/signup?plan=grow&instance={}", text("private_url").unwrap_or_default(), text("provider").unwrap_or_default(), text("instance").unwrap_or_default()))
    });

    go(rows, NAMES[1], || {
        let bearer = [("authorization", format!("Bearer {}", r.token))];
        let mut said = Vec::new();
        // The pages are not this console's: a GET is no sign-up page — 404, or the console
        // app's own page (its `index.html`, which draws its not-found), never the shop's.
        for path in ["/signup", "/welcome", "/private"] {
            let resp = http::req(&at, "GET", path, &bearer, None)?;
            let console = resp.status == 200 && resp.body.contains("<title>Gunnar</title>") && !resp.body.contains("sign up") && !resp.body.contains("first-account");
            want(resp.status == 404 || console, || format!("GET {path} answered {} on a sold Gunnar: {}", resp.status, http::cut(&resp.body, 200)))?;
            said.push(format!("GET {path} {}", if resp.status == 404 { "404".to_owned() } else { "the console's own page".to_owned() }));
        }
        for (method, path, absent) in [
            ("POST", "/api/account/subscription/renew", 404),
            ("POST", "/api/account/subscription/buy-bundle", 404),
            ("POST", "/api/namespaces/acme/increase/bundle", 404),
            ("POST", "/api/namespaces/acme/increase", 405),
        ] {
            let body = (method == "POST").then_some("{}");
            let resp = http::req(&at, method, path, &bearer, body)?;
            // No such order route: 404, or 405 where the path answers another method only.
            want(resp.status == absent || (resp.status == 404 || resp.status == 405), || format!("{method} {path} answered {} on a sold Gunnar: {}", resp.status, http::cut(&resp.body, 200)))?;
            said.push(format!("{method} {path} {}", resp.status));
        }
        // The same door is open on the rig's own console, which sells: the
        // 404s above are the sold console's, not the rig's.
        let mine = http::req(&r.ui_addr(), "GET", "/signup", &bearer, None)?;
        want(mine.status == 200, || format!("/signup on the console that sells answered {}", mine.status))?;
        Ok(said.join(", "))
    });

    go(rows, NAMES[2], || {
        let anon = c.isolated_page()?;
        c.viewport(&anon, 1440, 900)?;
        c.navigate(&anon, &format!("{origin}/login"))?;
        // ONE sign-up button (owner 2026-10-15), "Sign up account" — on a
        // sold Gunnar it leads to its provider's sign-up page.
        c.wait_for(&anon, "!document.getElementById('paid-link').hidden", 20_000)
            .map_err(|_| "the front page never showed its sign-up button".to_owned())?;
        let shown = c.eval_str(&anon, "document.getElementById('paid-link').innerText.trim() + ' ' + document.getElementById('paid-link').getAttribute('href')")?;
        want(shown == "Sign up account https://gunnar.rs/signup", || format!("the sold Gunnar's front page offers: {shown}"))?;
        c.screenshot(&anon, &dir.join("1-sold-gunnar-front-page.png"))?;
        Ok(shown)
    });
}