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

nordisk/monetize

crates/monetize-site-buy/tests/buyflow4.rs

//! ★ **Lane BUYFLOW-4** (monetize v2.0, owner GO 2026-10-25): what the guide's trace of the buy
//! flows found (docs/INTEGRATION-v2.md §8.15), for both sites. Addresses are `*.mock.invalid`.
//!
//! 1. A renewal's way back from Stripe carries its company, as the first buy's does (B7a.2).
//! 2. The bundle and renew doors answer a buyer in buyer words: the customer's sentence the box
//!    sends beside a refusal, never the operator's; and no buyer sentence of the relay or of the
//!    buy pages names the operator's machinery (lay, estate, guest, operator, cap, …).
//! 3. A PermissionDenied from the ONE box is 403 with a plain buyer sentence, never 502.

mod sites;

use std::sync::Arc;

use axum::http::StatusCode;
use monetize_site_buy::testkit::{TestSite, BUNDLE};
use monetize_site_buy::Relay;
use serde_json::json;
use sites::{Kit, Shape};
use tonic::Code;

/// **The operator's vocabulary**: words a buyer page never says (owner: customer-never-sees-caps,
/// one-monetize-only).
const OPERATOR_WORDS: &[&str] = &[
    "lay",
    "lays",
    "laid",
    "laying",
    "estate",
    "estates",
    "guest",
    "guests",
    "operator",
    "operators",
    "cap",
    "caps",
    "monetize",
    "tenant",
    "tenants",
    "appliance",
    "vendor",
    "vendors",
    "ignalina",
];

/// The operator words `text` says, as whole words (case-insensitive), and "ONE box".
fn operator_words(text: &str) -> Vec<String> {
    let lower = text.to_lowercase();
    let mut said: Vec<String> = lower
        .split(|c: char| !c.is_alphanumeric())
        .filter(|w| OPERATOR_WORDS.contains(w))
        .map(str::to_owned)
        .collect();
    if lower.contains("one box") {
        said.push("one box".into());
    }
    said
}

fn assert_buyer_words(text: &str, at: &str) {
    assert!(!text.trim().is_empty(), "{at}: the buyer is told in words");
    let said = operator_words(text);
    assert!(said.is_empty(), "{at}: a buyer reads {said:?}: {text}");
}

for_both_sites!(
    a_renewal_comes_back_with_its_company,
    the_bundle_and_renew_doors_answer_in_buyer_words,
    a_refused_ticket_is_403_in_plain_words
);

/// 1. `/paid/estate/<co>/renewal-<n>` lands on the order's watch with `company=`, as
///    `/paid/estate/<co>` does; a change keeps its `change=1`.
async fn a_renewal_comes_back_with_its_company(shape: Shape) {
    let k = Kit::new(shape).await;
    let private = shape.pages().private;
    for (path, to) in [
        ("/paid/estate/acme", format!("{private}&company=acme")),
        (
            "/paid/estate/acme/renewal-1",
            format!("{private}&company=acme"),
        ),
        (
            "/paid/estate/acme/renewal-12",
            format!("{private}&company=acme"),
        ),
        (
            "/paid/estate/acme/change-2",
            format!("{private}&company=acme&change=1"),
        ),
        ("/paid/estate/acme/renewal-x", private.clone()),
    ] {
        let r = k.ask("GET", path, None, None).await;
        assert_eq!(
            (r.status, r.location.as_deref()),
            (StatusCode::SEE_OTHER, Some(to.as_str())),
            "{shape:?} {path}"
        );
    }
}

/// A kit whose site reads the held room from its own word (so a grow reaches the shop's
/// `Purchase.Bundles` even while the box refuses every call).
async fn kit_holding_room(shape: Shape) -> Kit {
    let fake = monetize_site_buy::testkit::FakeOneBox::spawn(shape.product(), sites::BEARER).await;
    let site = TestSite::over(shape, &fake).with_held(Arc::new(|_: &str| {
        std::collections::BTreeMap::from([("disk_bytes".to_owned(), 1u64 << 30)])
    }));
    let relay = Arc::new(Relay::new(site.clone(), fake.link()));
    Kit {
        shape,
        fake,
        site,
        relay,
        emitted: Default::default(),
    }
}

/// 2. A refusal on the bundle and renew doors: the customer's sentence the box sent beside the
///    operator's; without one, an operator's sentence is never passed on.
async fn the_bundle_and_renew_doors_answer_in_buyer_words(shape: Shape) {
    let k = kit_holding_room(shape).await;
    k.fake
        .add_account("x14", "x14@mock.invalid", &[("disk_bytes", 1 << 30)]);
    k.fake.add_estate("acme", "x14", "Mini");
    let operators = "the product cannot deliver this to x14-repo: the cap of 2 TiB is past the operator's lay room on estate gunnar-rs";
    let customers =
        "This size cannot be added to your account right now. Nothing has been charged.";
    let doors: Vec<(&str, &str, Option<serde_json::Value>)> = vec![
        ("GET", "/api/account/subscription/bundles", None),
        (
            "POST",
            "/api/account/subscription/buy-bundle",
            Some(json!({"bundle": BUNDLE})),
        ),
        (
            "POST",
            "/auth/private/renew",
            Some(json!({"company": "acme"})),
        ),
    ];
    for (method, path, body) in &doors {
        k.fake
            .fail_saying(Code::FailedPrecondition, operators, customers);
        let r = k.ask(method, path, Some("x14"), body.clone()).await;
        assert_eq!(
            (r.status, r.error()),
            (StatusCode::CONFLICT, customers),
            "{shape:?} {path}: the customer's sentence"
        );
        k.fake.fail(Some((Code::FailedPrecondition, operators)));
        let r = k.ask(method, path, Some("x14"), body.clone()).await;
        assert_eq!(
            r.status,
            StatusCode::CONFLICT,
            "{shape:?} {path}: {:?}",
            r.body
        );
        assert_buyer_words(
            r.error(),
            &format!("{shape:?} {path}, no customer sentence"),
        );
        k.fake.fail(Some((
            Code::FailedPrecondition,
            "there is no estate acme.estates.mock.invalid",
        )));
        let r = k.ask(method, path, Some("x14"), body.clone()).await;
        assert_buyer_words(r.error(), &format!("{shape:?} {path}, a renewal refused"));
    }
    // The account's own renewal on a shop that does not verify its tickets: buyer words too.
    k.fake.fail(None);
    // (A32-9: the fake says it checks tickets as spawned; this one is a shop that does not.)
    k.fake.checks_no_tickets();
    for (method, path) in [
        ("GET", "/api/account/subscription"),
        ("POST", "/api/account/subscription/renew"),
    ] {
        let r = k.ask(method, path, Some("x14"), None).await;
        assert_eq!(
            r.status,
            StatusCode::NOT_IMPLEMENTED,
            "{shape:?} {path}: {:?}",
            r.body
        );
        assert_buyer_words(r.error(), &format!("{shape:?} {path}"));
    }
    // The watch of an order named badly, and a market the shop does not sell in.
    let r = k
        .ask("GET", "/auth/private/status?company=..", Some("x14"), None)
        .await;
    assert_eq!(r.status, StatusCode::BAD_REQUEST);
    assert_buyer_words(r.error(), &format!("{shape:?} status, a bad name"));
    assert_buyer_words(
        &monetize_site_buy::rules::market(Some("US")).unwrap_err(),
        "a market not sold in",
    );
}

/// 3. The ONE box refusing the site's word for who asks (PermissionDenied): 403, and the
///    buyer's plain sentence, on the private-instance doors and the account's grow doors.
async fn a_refused_ticket_is_403_in_plain_words(shape: Shape) {
    let k = kit_holding_room(shape).await;
    k.fake
        .add_account("x14", "x14@mock.invalid", &[("disk_bytes", 1 << 30)]);
    k.fake.add_estate("acme", "x14", "Mini");
    let ok = k.ask("GET", "/auth/private/ladder", None, None).await;
    assert_eq!(
        ok.status,
        StatusCode::OK,
        "the shop's word on selling instances is kept"
    );
    k.fake.fail(Some((
        Code::PermissionDenied,
        "REFUSED [estate-unnamed] the ticket names no estate this monetize holds a key for",
    )));
    let doors: Vec<(&str, &str, Option<serde_json::Value>)> = vec![
        (
            "POST",
            "/auth/private/start",
            Some(
                json!({"company": "beta", "front_plan": "f-mini", "main_plan": "m-mini", "main_data_gib": 40}),
            ),
        ),
        (
            "POST",
            "/auth/private/change",
            Some(
                json!({"company": "acme", "front_plan": "f-mini", "main_plan": "m-small", "main_data_gib": 120}),
            ),
        ),
        ("GET", "/auth/private/status?company=acme", None),
        (
            "POST",
            "/auth/private/renew",
            Some(json!({"company": "acme"})),
        ),
        ("GET", "/api/account/subscription/bundles", None),
        (
            "POST",
            "/api/account/subscription/buy-bundle",
            Some(json!({"bundle": BUNDLE})),
        ),
        (
            "GET",
            "/api/account/subscription/status?reference=p-1",
            None,
        ),
    ];
    for (method, path, body) in &doors {
        let r = k.ask(method, path, Some("x14"), body.clone()).await;
        assert_eq!(
            (r.status, r.error()),
            (
                StatusCode::FORBIDDEN,
                monetize_site_buy::rules::WHO_NOT_CONFIRMED
            ),
            "{shape:?} {method} {path}"
        );
    }
    let r = k
        .ask(
            "POST",
            "/auth/signup/start",
            None,
            Some(json!({"username": "newbie", "email": "newbie@mock.invalid"})),
        )
        .await;
    assert_eq!(
        (r.status, r.error()),
        (
            StatusCode::FORBIDDEN,
            monetize_site_buy::rules::WHO_NOT_CONFIRMED
        ),
        "{shape:?} sign-up"
    );
    assert!(
        k.fake.book().checkouts.is_empty(),
        "nothing was sold while the box refused"
    );
}

// ── 2. the buy pages' own words ─────────────────────────────────────────────────────────

/// The string literals of a script or Rust source that read as a sentence (a space in them),
/// comment lines left out.
fn sentences(source: &str) -> Vec<String> {
    let mut out = Vec::new();
    for line in source.lines() {
        let t = line.trim_start();
        if t.starts_with("//") || t.starts_with("/*") || t.starts_with('*') {
            continue;
        }
        let mut chars = line.chars().peekable();
        let mut open: Option<char> = None;
        let mut cur = String::new();
        let mut prev = ' ';
        while let Some(c) = chars.next() {
            match open {
                None if (c == '\'' || c == '"' || c == '`')
                    && !(c == '\'' && prev.is_alphanumeric()) =>
                {
                    open = Some(c);
                    cur.clear();
                }
                None if c == '/' && chars.peek() == Some(&'/') => break,
                Some(q) if c == '\\' => {
                    let _ = q;
                    chars.next();
                }
                Some(q) if c == q => {
                    if cur.contains(' ') {
                        out.push(cur.clone());
                    }
                    open = None;
                }
                Some(_) => cur.push(c),
                None => {}
            }
            prev = c;
        }
    }
    out
}

/// Only the part of a Rust source before its tests.
fn before_tests(src: &str) -> &str {
    src.split("#[cfg(test)]").next().unwrap_or(src)
}

#[test]
fn the_buy_pages_and_the_relays_buyer_sentences_name_no_operator_machinery() {
    use monetize_site_buy::{relay, rules};
    let mut bad = Vec::new();
    for (name, js) in [
        ("buy.js", monetize_site_buy::assets::BUY_JS),
        ("welcome.js", monetize_site_buy::assets::WELCOME_JS),
        ("paid.js", monetize_site_buy::assets::PAID_JS),
    ] {
        for s in sentences(js) {
            if !operator_words(&s).is_empty() {
                bad.push(format!("{name}: {s}"));
            }
        }
    }
    // Every sentence a buy door refuses with (`Answer::refuse(…, "…")`).
    for line in before_tests(include_str!("../src/relay.rs"))
        .lines()
        .filter(|l| l.contains("Answer::refuse("))
    {
        for s in sentences(line) {
            if !operator_words(&s).is_empty() {
                bad.push(format!("relay.rs: {s}"));
            }
        }
    }
    for (name, words) in [
        ("SHOP_DID_NOT_ANSWER", rules::SHOP_DID_NOT_ANSWER),
        ("WHO_NOT_CONFIRMED", rules::WHO_NOT_CONFIRMED),
        ("NOT_DONE_HERE", rules::NOT_DONE_HERE),
        ("PAY_URL_REFUSED", rules::PAY_URL_REFUSED),
        ("SHOP_UNAVAILABLE", relay::SHOP_UNAVAILABLE),
        ("ACCOUNT_NOT_SOLD_HERE", relay::ACCOUNT_NOT_SOLD_HERE),
        ("START_TOO_SLOW", relay::START_TOO_SLOW),
        ("NOT_A_SESSION", relay::NOT_A_SESSION),
        ("RENEWAL_NOT_VERIFIED", relay::RENEWAL_NOT_VERIFIED),
        (
            "AWAITING_PROVIDER",
            monetize_site_buy::purchase::AWAITING_PROVIDER,
        ),
        ("PROVIDER_LATE", monetize_site_buy::purchase::PROVIDER_LATE),
        ("NOTHING_FITS", monetize_site_buy::purchase::NOTHING_FITS),
    ] {
        if !operator_words(words).is_empty() {
            bad.push(format!("{name}: {words}"));
        }
    }
    assert!(
        bad.is_empty(),
        "buyer sentences that name the operator's machinery:\n{}",
        bad.join("\n")
    );
}

/// ★ **PAID-2 (owner 2026-10-26): the welcome page's register button says what it does.** "Register
/// it here now and continue" was unclear. The shared page says "Register your security key now",
/// with ONE line under it: "Or open the link we sent to your e-mail." No site keeps a copy.
#[test]
fn the_welcome_pages_register_button_says_register_your_security_key_now() {
    let js = monetize_site_buy::assets::WELCOME_JS;
    for words in [
        "Register your security key now",
        "Or open the link we sent to your e-mail.",
    ] {
        assert!(
            js.contains(&format!("'{words}'")),
            "the welcome page says {words:?}"
        );
    }
    assert!(
        !js.contains("Register it here now"),
        "the old words are gone"
    );
}

/// ★ **The account line's words** (owner 2026-10-27): "Your <site> account shows everything you
/// have bought and lets you add more. You pay only for the storage you choose." The site is the
/// page's own host (`{host}`, filled from `location.hostname`), never a name written in.
#[test]
fn the_sign_up_pages_account_line_is_the_owners_words_with_the_sites_own_name() {
    let js = monetize_site_buy::assets::BUY_JS;
    assert!(js.contains("'account': 'Your {host} account shows everything you have bought and lets you add more. You pay only for the storage you choose.',"), "the owner's words");
    assert!(
        !js.contains(
            "is where you see and grow what you bought. Then you pay for the room you chose."
        ),
        "the old words are gone"
    );
    assert!(
        sentences(js)
            .iter()
            .all(|s| !s.contains("gunnar.rs") && !s.contains("holger.rs")),
        "no site's name is written into what the page says"
    );
}

/// ★ BUY-ONE-2: the owner's sentences and the two tab names live in the ONE buy page; the free
/// row's note, the card check and the beta line are the SITE's words (RA5-41: a Holger site, or
/// one on another vendor or currency, says its own), filled from its settings.
#[test]
fn the_one_buy_page_carries_the_owners_sentences_and_tab_names() {
    let said = sentences(monetize_site_buy::assets::BUY_JS);
    for words in [
        "Your {host} account shows everything you have bought and lets you add more. You pay only for the storage you choose.",
        "Grow your plan",
        "New private instance",
    ] {
        assert!(said.iter().any(|s| s == words), "the page says {words:?}");
    }
    let js = monetize_site_buy::assets::BUY_JS;
    assert!(
        js.contains("'account-free': '@@CARD_CHECK@@',"),
        "the free row's line is its card check, the site's words"
    );
    assert!(
        js.contains("note: '@@FREE_NOTE@@' };")
            && js.contains("const BETA_LINE = '@@BETA_LINE@@';")
    );
    for gunnars in [
        "Free: clone only. Pushing needs a paid plan.",
        "A card check at Stripe (0 kr) proves you are a person.",
        "Beta: we replicate your data, but cannot guarantee it.",
    ] {
        assert!(
            !said.iter().any(|s| s == gunnars),
            "Gunnar's words are its settings', not the shared page's: {gunnars:?}"
        );
    }
    for gone in ["New instance", "Private @@PRODUCT@@ instance"] {
        assert!(
            !said.iter().any(|s| s == gone),
            "the old tab name {gone:?} is still said"
        );
    }
}

#[test]
fn the_sentence_scanner_reads_what_a_page_says() {
    assert_eq!(
        sentences("show('Your estate is laid', 'err'); // a lay"),
        vec!["Your estate is laid".to_owned()]
    );
    assert!(sentences("// 'the operator says'").is_empty());
    assert_eq!(
        operator_words("The operator's cap"),
        vec!["operator".to_owned(), "cap".to_owned()]
    );
    assert!(operator_words("Your capital is capped? no: a capacity").is_empty());
}