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

nordisk/monetize

crates/monetize-admin/tests/app_config.rs

//! ★ **CONFIG IN THE APPLICATION** (owner 2026-10-14) — the ONE box's own site book: a site
//! is ADDED and EDITED through the application (`SiteBook.PutSite`, the console, the CLI at
//! the door), judged by the box's own loader exactly as a fed feed is and refused whole by
//! name, kept 0600 on the box beside the bootstrap's feed, lying over it in the feed in force
//! (the app wins, by name), and audited — who, when, what, never a secret's value.

use std::collections::BTreeMap;

use base64::Engine as _;
use monetize_admin::app_config::{self, AppBook};
use monetize_admin::solo;
use monetize_server::site_book::{SiteBook as _, SiteBookRefusal, SitePut, SiteTerms};

const HELD_KEY: &str = "sk_test_HELDKEYVALUE0123456789";
const HELD_HOOK: &str = "whsec_HELDHOOKVALUE0123456789";
const NEW_KEY: &str = "sk_test_NEWKEYVALUE0123456789";
const NEW_HOOK: &str = "whsec_NEWHOOKVALUE0123456789";

fn b64(b: &[u8]) -> String {
    base64::engine::general_purpose::STANDARD.encode(b)
}

/// The bootstrap's feed, as `cargo xtask feed` puts it: gunnar served, test2's Stripe account
/// (`stripe/gunnar-test2`, the signed guest and its two secrets), test2 a fed operator site
/// with its own terms, DEV media and the box's DNS writer.
fn bootstrap() -> BTreeMap<String, String> {
    let wasm = b64(b"\0asm\x01\0\0\0");
    let stripe = "enabled = true\ncountry = \"IE\"\ngrants = [\"http\", \"hmac\", \"state\"]\nhttp_hosts = [\"api.stripe.com\"]\nhmac_secrets = [\"MONETIZE_STRIPE_GUNNAR_TEST2_WEBHOOK_SECRET\"]\nsuccess_url = \"https://test2.gunnar.rs/paid/{reference}\"\ncancel_url = \"https://test2.gunnar.rs/cancel/{reference}\"\nlivemode = false\ntax_code = \"txcd_10103001\"\nwebhook_secret = \"MONETIZE_STRIPE_GUNNAR_TEST2_WEBHOOK_SECRET\"\nguest_digest = \"cd\"\n\n[[http_auth]]\nhost = \"api.stripe.com\"\nprefix = \"Bearer \"\nsecret = \"MONETIZE_STRIPE_GUNNAR_TEST2_SECRET_KEY\"\n";
    let monetize = "[estates.test2-gunnar-rs]\nproduct = \"gunnar\"\nvendor = \"stripe/gunnar-test2\"\nowner = \"ignalina\"\nsite = \"test2.gunnar.rs\"\noperator_site = true\n\n[estates.test2-gunnar-rs.orders]\nterm_days = 1\ngrace_hours = 1\nmargin_bp = 2500\ndns = \"cloudflare\"\n\n[estates.test2-gunnar-rs.orders.media]\ndev = \"file:/media/dev.img\"\n";
    BTreeMap::from([
        ("plugin.gunnar.wasm".to_string(), wasm.clone()),
        ("plugin.gunnar.toml".to_string(), "vendor = \"stripe/gunnar-test2\"\ninstance = \"test2-gunnar-rs\"\nguest_digest = \"ab\"\ngrants = []\n".to_string()),
        ("plugin.gunnar.prices".to_string(), "[SE]\ndisk_gib_month = 1000\n".to_string()),
        ("vendor.stripe.gunnar-test2.wasm".to_string(), wasm),
        ("vendor.stripe.gunnar-test2.p7s".to_string(), b64(b"signed under the plugin root")),
        ("vendor.stripe.gunnar-test2.toml".to_string(), stripe.to_string()),
        ("secret.MONETIZE_STRIPE_GUNNAR_TEST2_SECRET_KEY".to_string(), HELD_KEY.to_string()),
        ("secret.MONETIZE_STRIPE_GUNNAR_TEST2_WEBHOOK_SECRET".to_string(), HELD_HOOK.to_string()),
        ("monetize.toml".to_string(), monetize.to_string()),
    ])
}

/// The box: its data root, the bootstrap on its door, its book over the base read from disk.
fn the_box() -> (tempfile::TempDir, AppBook) {
    let dir = tempfile::tempdir().unwrap();
    solo::write_door_feed(dir.path(), &bootstrap()).unwrap();
    let root = dir.path().to_path_buf();
    let book = AppBook::on(root);
    (dir, book)
}

fn add_test5() -> SitePut {
    SitePut {
        estate: "test5-gunnar-rs".into(),
        create: true,
        domain: "test5.gunnar.rs".into(),
        product: "gunnar".into(),
        first_admin: "ignalina".into(),
        operator_site: true,
        terminable: true,
        vendor: String::new(),
        vendor_secret_key: NEW_KEY.into(),
        vendor_webhook_secret: NEW_HOOK.into(),
        live: false,
        terms: SiteTerms {
            term_days: 1,
            grace_hours: 1,
            margin_bp: 2500,
            term_months: 0,
        },
        media_from: "test2-gunnar-rs".into(),
        actor: "console:op-1".into(),
    }
}

fn estates(f: &BTreeMap<String, String>) -> toml::Table {
    app_config::estates_of(f)
}

fn mode(path: &std::path::Path) -> u32 {
    use std::os::unix::fs::PermissionsExt as _;
    std::fs::metadata(path).unwrap().permissions().mode() & 0o777
}

/// Everything the box keeps on its disk for the application, as text.
fn kept_text(root: &std::path::Path) -> String {
    [app_config::SITES_REL, app_config::AUDIT_REL]
        .iter()
        .map(|r| std::fs::read_to_string(root.join(r)).unwrap_or_default())
        .collect()
}

/// ★ **A site ADDED in the application is in the feed in force** — its `[estates.<label>]`
/// (domain, first admin, terms, the media of the site it names, the box's ONE DNS writer), its
/// NEW Stripe account running the signed guest the box already holds (module and signature
/// copied, its own secret NAMES, its own return pages) and its two secrets — and the box's own
/// loader takes that feed. The secrets are kept 0600 apart from the book; the book and the
/// audit never hold a value; a show says "set" and a fingerprint.
#[test]
fn a_site_added_in_the_application_is_in_force_judged_kept_and_audited() {
    let (dir, book) = the_box();
    let root = dir.path();
    let done = book.put(&add_test5()).unwrap();
    assert!(
        done.words.contains("test5-gunnar-rs is added"),
        "{}",
        done.words
    );
    assert!(
        done.changed.iter().any(|c| c == "domain: test5.gunnar.rs"),
        "{:?}",
        done.changed
    );
    assert!(
        done.changed.iter().any(|c| c == "operator site: yes")
            && done.changed.iter().any(|c| c == "term days: 1"),
        "an add names what it set: {:?}",
        done.changed
    );
    assert!(
        done.changed
            .iter()
            .any(|c| c.starts_with("Secret key: set (sha256:")),
        "{:?}",
        done.changed
    );

    let f = solo::feed_now(root, &BTreeMap::new());
    let e = estates(&f);
    let t5 = e["test5-gunnar-rs"].as_table().unwrap();
    assert_eq!(
        (
            t5["site"].as_str(),
            t5["owner"].as_str(),
            t5["vendor"].as_str()
        ),
        (
            Some("test5.gunnar.rs"),
            Some("ignalina"),
            Some("stripe/test5-gunnar-rs")
        )
    );
    assert_eq!(
        (t5["operator_site"].as_bool(), t5["terminable"].as_bool()),
        (Some(true), Some(true))
    );
    let o = t5["orders"].as_table().unwrap();
    assert_eq!(
        (o["term_days"].as_integer(), o["dns"].as_str()),
        (Some(1), Some("cloudflare")),
        "the box's ONE DNS writer"
    );
    assert_eq!(
        o["media"]["dev"].as_str(),
        Some("file:/media/dev.img"),
        "the media of the site it names"
    );
    assert!(e.contains_key("test2-gunnar-rs"), "the fed site stays");
    assert_eq!(
        f["vendor.stripe.test5-gunnar-rs.wasm"], f["vendor.stripe.gunnar-test2.wasm"],
        "the signed guest the box holds"
    );
    assert_eq!(
        f["vendor.stripe.test5-gunnar-rs.p7s"],
        f["vendor.stripe.gunnar-test2.p7s"]
    );
    let block: toml::Table = toml::from_str(&f["vendor.stripe.test5-gunnar-rs.toml"]).unwrap();
    assert_eq!(
        block["webhook_secret"].as_str(),
        Some("MONETIZE_STRIPE_TEST5_GUNNAR_RS_WEBHOOK_SECRET")
    );
    assert_eq!(
        block["http_auth"][0]["secret"].as_str(),
        Some("MONETIZE_STRIPE_TEST5_GUNNAR_RS_SECRET_KEY")
    );
    assert_eq!(
        block["success_url"].as_str(),
        Some("https://test5.gunnar.rs/paid/{reference}")
    );
    assert_eq!(
        (block["livemode"].as_bool(), block["tax_code"].as_str()),
        (Some(false), Some("txcd_10103001"))
    );
    assert_eq!(
        f["secret.MONETIZE_STRIPE_TEST5_GUNNAR_RS_SECRET_KEY"],
        NEW_KEY
    );
    assert_eq!(
        f["secret.MONETIZE_STRIPE_TEST5_GUNNAR_RS_WEBHOOK_SECRET"],
        NEW_HOOK
    );
    assert_eq!(
        solo::judge(root, &f).unwrap(),
        vec!["gunnar".to_string()],
        "the box's own loader takes the feed in force"
    );

    for rel in [
        app_config::SITES_REL,
        app_config::SECRETS_REL,
        app_config::AUDIT_REL,
    ] {
        assert_eq!(mode(&root.join(rel)), 0o600, "{rel}");
    }
    let kept = kept_text(root);
    assert!(
        !kept.contains("NEWKEYVALUE") && !kept.contains("NEWHOOKVALUE"),
        "no secret value in the book or the audit: {kept}"
    );
    let audit = std::fs::read_to_string(root.join(app_config::AUDIT_REL)).unwrap();
    assert!(
        audit.contains("console:op-1")
            && audit.contains("\"added\"")
            && audit.contains("test5-gunnar-rs"),
        "{audit}"
    );

    let shown = book.show("test5-gunnar-rs").unwrap();
    let s = &shown.site;
    assert_eq!(
        (
            s.source.as_str(),
            s.vendor.as_str(),
            s.media_from.as_str(),
            s.terms.term_days
        ),
        ("app", "stripe/test5-gunnar-rs", "test2-gunnar-rs", 1)
    );
    assert!(
        s.secret_key_set
            && s.webhook_secret_set
            && s.secret_key_fingerprint.starts_with("sha256:")
            && !s.secret_key_fingerprint.contains("NEWKEY")
    );
    assert!(
        shown
            .vendors
            .contains(&"stripe/test5-gunnar-rs".to_string())
            && shown.vendors.contains(&"stripe/gunnar-test2".to_string())
    );
    assert_eq!(shown.history.len(), 1);
    assert!(
        shown.history[0].contains("console:op-1 — added test5-gunnar-rs"),
        "{:?}",
        shown.history
    );
    let blank = book.show("").unwrap();
    assert_eq!(
        (blank.site.product.as_str(), blank.site.source.as_str()),
        ("gunnar", "")
    );
    assert_eq!(
        blank.media_sites,
        vec!["test2-gunnar-rs".to_string(), "test5-gunnar-rs".to_string()],
        "test5 lays from test2's media now"
    );
}

/// ★ **A change the box would not start on is refused WHOLE, by name, and nothing is kept**
/// — the loader's own refusal (a customer's site sharing another site's account); the form's
/// own (a taken label, an unknown site, a new account without its two secrets, a live key on a
/// test site, a domain that is none, a media site that has none).
#[test]
fn a_change_is_refused_whole_by_name_and_nothing_is_kept() {
    let (dir, book) = the_box();
    let root = dir.path();
    let refused = |p: SitePut| book.put(&p).unwrap_err();
    let bad = |r: SiteBookRefusal, id: &str| match &r {
        SiteBookRefusal::Bad(w)
        | SiteBookRefusal::Taken(w)
        | SiteBookRefusal::NotFound(w)
        | SiteBookRefusal::Refused(w) => assert!(w.contains(id), "{id}: {w}"),
        SiteBookRefusal::Broken(w) => panic!("{id}: broken {w}"),
    };
    // The loader's own: a CUSTOMER's site (not operator_site) on test2's account.
    let r = refused(SitePut {
        operator_site: false,
        vendor: "stripe/gunnar-test2".into(),
        vendor_secret_key: String::new(),
        vendor_webhook_secret: String::new(),
        ..add_test5()
    });
    assert!(matches!(r, SiteBookRefusal::Refused(_)), "{r:?}");
    bad(r, "both bill through");
    bad(
        refused(SitePut {
            estate: "test2-gunnar-rs".into(),
            ..add_test5()
        }),
        "[site-taken]",
    );
    bad(
        refused(SitePut {
            create: false,
            ..add_test5()
        }),
        "[site-unknown]",
    );
    bad(
        refused(SitePut {
            vendor_webhook_secret: String::new(),
            ..add_test5()
        }),
        "[site-vendor-new]",
    );
    bad(
        refused(SitePut {
            vendor_secret_key: "sk_live_LIVEKEY".into(),
            ..add_test5()
        }),
        "[site-vendor-mode]",
    );
    bad(
        refused(SitePut {
            vendor_secret_key: "pk_test_PUBLISHABLE".into(),
            ..add_test5()
        }),
        "[site-vendor-key]",
    );
    bad(
        refused(SitePut {
            domain: "test5".into(),
            ..add_test5()
        }),
        "[site-domain]",
    );
    bad(
        refused(SitePut {
            estate: "Test5".into(),
            ..add_test5()
        }),
        "[site-label]",
    );
    bad(
        refused(SitePut {
            media_from: "nowhere".into(),
            ..add_test5()
        }),
        "[site-media-from]",
    );
    bad(
        refused(SitePut {
            terms: SiteTerms {
                term_days: 1,
                term_months: 1,
                ..Default::default()
            },
            ..add_test5()
        }),
        "[site-terms]",
    );
    for rel in [
        app_config::SITES_REL,
        app_config::SECRETS_REL,
        app_config::AUDIT_REL,
    ] {
        assert!(!root.join(rel).exists(), "nothing was kept: {rel}");
    }
    assert_eq!(
        solo::feed_now(root, &BTreeMap::new()),
        bootstrap(),
        "the feed in force is the bootstrap's"
    );
}

/// ★ **A FED site edited in the application** is taken over by name: what the form changes
/// moves (its terms), what it does not hold stays (its media, its DNS writer); a key typed in
/// replaces the held one (a fingerprint in the audit); the history is newest first.
#[test]
fn a_fed_site_edited_in_the_application_keeps_what_the_form_does_not_hold() {
    let (dir, book) = the_box();
    let root = dir.path();
    let mut edit = SitePut {
        estate: "test2-gunnar-rs".into(),
        create: false,
        domain: "test2.gunnar.rs".into(),
        product: "gunnar".into(),
        first_admin: "ignalina".into(),
        operator_site: true,
        vendor: "stripe/gunnar-test2".into(),
        terms: SiteTerms {
            term_days: 2,
            grace_hours: 1,
            margin_bp: 2500,
            term_months: 0,
        },
        actor: "door:installer".into(),
        ..Default::default()
    };
    let before = book.show("test2-gunnar-rs").unwrap().site;
    assert_eq!((before.source.as_str(), before.terms.term_days), ("fed", 1));
    let done = book.put(&edit).unwrap();
    assert_eq!(done.changed, vec!["term days: 1 → 2".to_string()]);
    let f = solo::feed_now(root, &BTreeMap::new());
    let o = estates(&f)["test2-gunnar-rs"]["orders"]
        .as_table()
        .unwrap()
        .clone();
    assert_eq!(
        (
            o["term_days"].as_integer(),
            o["dns"].as_str(),
            o["media"]["dev"].as_str()
        ),
        (Some(2), Some("cloudflare"), Some("file:/media/dev.img"))
    );
    assert_eq!(
        f["secret.MONETIZE_STRIPE_GUNNAR_TEST2_SECRET_KEY"], HELD_KEY,
        "no key typed: the held one stays"
    );
    edit.vendor_secret_key = NEW_KEY.into();
    let done = book.put(&edit).unwrap();
    assert!(
        done.changed
            .iter()
            .any(|c| c.starts_with("Secret key: replaced (sha256:")),
        "{:?}",
        done.changed
    );
    let f = solo::feed_now(root, &BTreeMap::new());
    assert_eq!(f["secret.MONETIZE_STRIPE_GUNNAR_TEST2_SECRET_KEY"], NEW_KEY);
    assert_eq!(
        f["vendor.stripe.gunnar-test2.wasm"],
        bootstrap()["vendor.stripe.gunnar-test2.wasm"]
    );
    let block: toml::Table = toml::from_str(&f["vendor.stripe.gunnar-test2.toml"]).unwrap();
    assert_eq!(
        block["success_url"].as_str(),
        Some("https://test2.gunnar.rs/paid/{reference}"),
        "a held account keeps its own return pages"
    );
    assert!(solo::judge(root, &f).is_ok());
    let shown = book.show("test2-gunnar-rs").unwrap();
    assert_eq!(shown.site.source, "app");
    assert_eq!(shown.history.len(), 2);
    assert!(
        shown.history[0].contains("Secret key: replaced")
            && shown.history[1].contains("term days: 1 → 2"),
        "newest first: {:?}",
        shown.history
    );
    assert!(!kept_text(root).contains("NEWKEYVALUE"));
    // The same put again: nothing changed, said so.
    assert!(book
        .put(&SitePut {
            vendor_secret_key: String::new(),
            ..edit
        })
        .unwrap()
        .words
        .contains("nothing changed"));
}

/// ★ **Bootstrap and app edits coexist by ONE rule: the app wins, by name.** A feed put on the
/// door AFTER a site was added in the application never drops it (the app layer is its own
/// file, over every door feed) — the door's answer names what the app keeps over it; a door
/// feed the app's sites cannot lie over (no signed Stripe guest left) is refused whole.
#[test]
fn a_bootstrap_feed_after_app_edits_never_drops_an_app_made_site() {
    let (dir, book) = the_box();
    let root = dir.path();
    book.put(&add_test5()).unwrap();
    // The operator re-feeds the bootstrap from his machine — test5 is in no solo.toml.
    let mut again = bootstrap();
    again.insert(
        "plugin.gunnar.prices".into(),
        "[SE]\ndisk_gib_month = 1100\n".into(),
    );
    solo::write_door_feed(root, &again).unwrap();
    let f = solo::feed_now(root, &BTreeMap::new());
    assert!(
        estates(&f).contains_key("test5-gunnar-rs"),
        "the app-made site stands over a later door feed"
    );
    assert_eq!(
        f["plugin.gunnar.prices"], "[SE]\ndisk_gib_month = 1100\n",
        "the door's own keys are taken"
    );
    // A door feed that ALSO declares test5 and test5's account: the app's stand, named.
    let mut clash = again.clone();
    let mut m: toml::Table = toml::from_str(&clash["monetize.toml"]).unwrap();
    m["estates"].as_table_mut().unwrap().insert("test5-gunnar-rs".into(), toml::Value::Table(toml::from_str("product = \"gunnar\"\nvendor = \"stripe/gunnar-test2\"\nowner = \"someone\"\nsite = \"x.example\"\noperator_site = true\n").unwrap()));
    clash.insert("monetize.toml".into(), toml::to_string(&m).unwrap());
    let kept = app_config::shadowed(
        &clash,
        &app_config::read_book(root),
        &app_config::read_secrets(root),
    );
    assert_eq!(kept, vec!["site test5-gunnar-rs".to_string()]);
    let over = app_config::overlay(
        &clash,
        &app_config::read_book(root),
        &app_config::read_secrets(root),
    )
    .unwrap();
    assert_eq!(
        estates(&over)["test5-gunnar-rs"]["site"].as_str(),
        Some("test5.gunnar.rs"),
        "the app's site, not the door's"
    );
    // A door feed with no signed Stripe guest at all: the app's account cannot run → refused.
    let bare: BTreeMap<String, String> = again
        .into_iter()
        .filter(|(k, _)| !k.starts_with("vendor."))
        .collect();
    let e = app_config::overlay(
        &bare,
        &app_config::read_book(root),
        &app_config::read_secrets(root),
    )
    .unwrap_err();
    assert!(e.contains("[site-no-vendor-guest]"), "{e}");
}

/// The box's feed with a Holger site fed beside test2 (lane DNS-WRITER): the shop sells Gunnar
/// (`[estate_orders] product`), test2.gunnar.rs writes with `cloudflare` (the gunnar.rs zone),
/// and — when `holger_site` — test2.holger.rs with `cloudflare-holger` (the holger.rs zone);
/// `holger_zone`: the feed holds the holger.rs zone's signed writer.
fn two_zones(holger_site: bool, holger_zone: bool) -> BTreeMap<String, String> {
    let mut f = bootstrap();
    let mut m = format!(
        "[estate_orders]\nproduct = \"gunnar\"\n\n{}",
        f["monetize.toml"]
    );
    if holger_site {
        m.push_str("\n[estates.test2-holger-rs]\nproduct = \"holger\"\nvendor = \"stripe/gunnar-test2\"\nsite = \"test2.holger.rs\"\n\n[estates.test2-holger-rs.orders]\ndns = \"cloudflare-holger\"\n");
    }
    f.insert("monetize.toml".into(), m);
    if holger_zone {
        f.insert(
            "dns_guest.cloudflare-holger.toml".into(),
            "zone_id = \"z\"\n".into(),
        );
    }
    f
}

/// An app-added site of `product` under `domain`.
fn app_site(label: &str, domain: &str, product: &str) -> app_config::Book {
    let site = app_config::AppSite {
        domain: domain.into(),
        product: product.into(),
        first_admin: "ignalina".into(),
        operator_site: true,
        terminable: true,
        vendor: "stripe/gunnar-test2".into(),
        ..Default::default()
    };
    app_config::Book {
        sites: BTreeMap::from([(label.to_string(), site)]),
        ..Default::default()
    }
}

/// What monetize-server makes of the feed in force: each site's zone writer.
fn writers_in_force(f: &BTreeMap<String, String>) -> BTreeMap<String, String> {
    let estates: BTreeMap<String, monetize_server::config::EstateConfig> = estates(f)
        .into_iter()
        .map(|(l, v)| {
            (
                l,
                v.try_into()
                    .expect("monetize-server reads each site of the feed in force"),
            )
        })
        .collect();
    monetize_server::estate_order::site_dns_writers(&estates)
}

/// The name a record gets in the zone its writer writes, as Cloudflare makes it: a name not
/// under the zone is taken as relative to it (`r34e.holger.rs` in gunnar.rs is
/// `r34e.holger.rs.gunnar.rs`).
fn as_written(writer: &str, name: &str) -> String {
    let zone = match writer {
        "cloudflare" => "gunnar.rs",
        "cloudflare-holger" => "holger.rs",
        other => panic!("no zone for the writer {other:?}"),
    };
    if name == zone || name.ends_with(&format!(".{zone}")) {
        name.to_owned()
    } else {
        format!("{name}.{zone}")
    }
}

/// ★ **An APP-ADDED site writes ITS product's zone, as a declared one does** (DNS-WRITER,
/// test2 2026-10-26: `monetize site add r34e … --product holger` laid, paired and called home,
/// then held at the sale check — "r34e.holger.rs does not resolve": the box's FIRST fed site's
/// writer, `cloudflare`, wrote it into the gunnar.rs zone as r34e.holger.rs.gunnar.rs). The
/// app-added Holger site takes the writer of Holger's zone — a fed Holger site's, else the
/// zone writer the feed holds — and monetize-server writes it with that; a Gunnar one keeps
/// Gunnar's; a product with no zone writer of its own takes NONE, never another's zone.
#[test]
fn an_app_added_site_writes_its_own_products_zone() {
    let none = app_config::Secrets::new();
    for (holger_site, holger_zone) in [(true, false), (false, true), (true, true)] {
        let base = two_zones(holger_site, holger_zone);
        let f = app_config::overlay(
            &base,
            &app_site("holger-r34e", "r34e.holger.rs", "holger"),
            &none,
        )
        .unwrap();
        let w = writers_in_force(&f);
        let named = as_written(
            &w["holger-r34e"],
            estates(&f)["holger-r34e"]["site"].as_str().unwrap(),
        );
        assert_eq!(
            named, "r34e.holger.rs",
            "named r34e.holger.rs in its own zone, never r34e.holger.rs.gunnar.rs"
        );
        assert_eq!(w.get("holger-r34e").map(String::as_str), Some("cloudflare-holger"), "the Holger site writes the holger.rs zone (fed Holger site {holger_site}, zone writer {holger_zone}): {w:?}");
        assert_eq!(
            w.get("test2-gunnar-rs").map(String::as_str),
            Some("cloudflare"),
            "test2 keeps the gunnar.rs zone"
        );
    }
    let g = app_config::overlay(
        &two_zones(true, true),
        &app_site("gunnar-r34g", "r34g.gunnar.rs", "gunnar"),
        &none,
    )
    .unwrap();
    assert_eq!(
        writers_in_force(&g).get("gunnar-r34g").map(String::as_str),
        Some("cloudflare"),
        "an app-added Gunnar site writes the gunnar.rs zone"
    );
    assert_eq!(as_written("cloudflare", "r34g.gunnar.rs"), "r34g.gunnar.rs");
    let lone = app_config::overlay(
        &two_zones(false, false),
        &app_site("holger-r34e", "r34e.holger.rs", "holger"),
        &none,
    )
    .unwrap();
    assert_eq!(
        writers_in_force(&lone).get("holger-r34e"),
        None,
        "no zone writer for Holger: none, never the gunnar.rs zone's"
    );
}

/// ★ T2-BOX (owner 2026-11-03: "an EMPTY monetize test2 … click and create Holger and/or Gunnar
/// roots WITHOUT AI"): the kit's feed of a box that serves holger and gunnar and has NO site —
/// each product block names no `instance` (no home yet), each product's Stripe account is held,
/// its media and its zone's writer are fed per product (`[site_media]`, `[site_dns]`).
fn empty_box_feed() -> BTreeMap<String, String> {
    let wasm = b64(b"\0asm\x01\0\0\0");
    let stripe = |inst: &str, site: &str| {
        let up = inst.to_ascii_uppercase().replace('-', "_");
        format!("enabled = true\ncountry = \"IE\"\ngrants = [\"http\", \"hmac\", \"state\"]\nhttp_hosts = [\"api.stripe.com\"]\nhmac_secrets = [\"MONETIZE_STRIPE_{up}_WEBHOOK_SECRET\"]\nsuccess_url = \"https://{site}/paid/{{reference}}\"\ncancel_url = \"https://{site}/cancel/{{reference}}\"\nlivemode = false\nwebhook_secret = \"MONETIZE_STRIPE_{up}_WEBHOOK_SECRET\"\nguest_digest = \"cd\"\n\n[[http_auth]]\nhost = \"api.stripe.com\"\nprefix = \"Bearer \"\nsecret = \"MONETIZE_STRIPE_{up}_SECRET_KEY\"\n")
    };
    let monetize = "[site_dns]\ngunnar = \"cloudflare\"\nholger = \"cloudflare-holger\"\n\n[site_media.gunnar-release]\nproduct = \"gunnar\"\n[site_media.gunnar-release.media]\ngunnar = \"file:/media/gunnar-release.img\"\n\n[site_media.holger-release]\nproduct = \"holger\"\n[site_media.holger-release.media]\nholger = \"file:/media/holger-release.img\"\n[site_media.holger-release.release_notes.\"holger-3.1.2\"]\nlabel = \"Holger 3.1.2\"\n";
    let mut f = BTreeMap::from([("monetize.toml".to_string(), monetize.to_string())]);
    for (product, inst, site) in [
        ("gunnar", "gunnar", "gunnar.rs"),
        ("holger", "holger", "holger.rs"),
    ] {
        let up = inst.to_ascii_uppercase();
        f.insert(format!("plugin.{product}.wasm"), wasm.clone());
        f.insert(
            format!("plugin.{product}.toml"),
            format!("vendor = \"stripe/{inst}\"\nguest_digest = \"ab\"\ngrants = []\n"),
        );
        f.insert(
            format!("plugin.{product}.prices"),
            "[SE]\ndisk_gib_month = 1000\n".to_string(),
        );
        f.insert(format!("vendor.stripe.{inst}.wasm"), wasm.clone());
        f.insert(
            format!("vendor.stripe.{inst}.p7s"),
            b64(b"signed under the plugin root"),
        );
        f.insert(format!("vendor.stripe.{inst}.toml"), stripe(inst, site));
        f.insert(
            format!("secret.MONETIZE_STRIPE_{up}_SECRET_KEY"),
            HELD_KEY.to_string(),
        );
        f.insert(
            format!("secret.MONETIZE_STRIPE_{up}_WEBHOOK_SECRET"),
            HELD_HOOK.to_string(),
        );
    }
    f
}

fn empty_box() -> (tempfile::TempDir, AppBook) {
    let dir = tempfile::tempdir().unwrap();
    solo::write_door_feed(dir.path(), &empty_box_feed()).unwrap();
    let book = AppBook::on(dir.path().to_path_buf());
    (dir, book)
}

fn root_site(label: &str, domain: &str, product: &str, media: &str) -> SitePut {
    SitePut {
        estate: label.into(),
        create: true,
        domain: domain.into(),
        product: product.into(),
        first_admin: "ignalina".into(),
        operator_site: true,
        terminable: false,
        vendor: format!("stripe/{product}"),
        vendor_secret_key: String::new(),
        vendor_webhook_secret: String::new(),
        live: false,
        terms: SiteTerms::default(),
        media_from: media.into(),
        actor: "console:op-1".into(),
    }
}

/// ★ **T2-BOX: an EMPTY box that serves holger and gunnar loads** — the box's own loader takes
/// a feed whose products have no home site yet (they are HELD, nothing refused), and the Sites
/// page offers both products, both held Stripe accounts and each product's fed media.
#[test]
fn an_empty_box_serving_holger_and_gunnar_loads_and_offers_both() {
    let (dir, book) = empty_box();
    let f = solo::feed_now(dir.path(), &BTreeMap::new());
    assert_eq!(
        solo::judge(dir.path(), &f).unwrap(),
        Vec::<String>::new(),
        "the loader takes the empty box; no product is served before its home"
    );
    let blank = book.show("").unwrap();
    assert_eq!(
        blank.products,
        vec!["gunnar".to_string(), "holger".to_string()]
    );
    assert_eq!(
        blank.vendors,
        vec!["stripe/gunnar".to_string(), "stripe/holger".to_string()]
    );
    assert_eq!(
        blank.media_sites,
        vec!["gunnar-release".to_string(), "holger-release".to_string()]
    );
}

/// ★ **T2-BOX: the FIRST operator site added for a product becomes its HOME** — typed as a ROOT
/// domain (holger.rs), its product, its Stripe account and its media picked: the feed in force
/// then pulls the product from that site (`instance`), the site lays from the product's fed
/// media and writes the product's own zone, and the loader serves the product. A second site
/// keeps the home. Every other refusal stands, and three new ones are said by name.
#[test]
fn the_first_site_added_for_a_product_becomes_its_home() {
    let (dir, book) = empty_box();
    let root = dir.path();
    let e = book
        .put(&root_site(
            "holger-rs",
            "holger.rs",
            "holger",
            "gunnar-release",
        ))
        .unwrap_err();
    assert!(
        matches!(&e, SiteBookRefusal::Bad(w) if w.contains("[site-media-product]")),
        "{e:?}"
    );
    let e = book
        .put(&root_site("holger-rs", "holger.rs", "holger", ""))
        .unwrap_err();
    assert!(
        matches!(&e, SiteBookRefusal::Bad(w) if w.contains("[site-media-none]") && w.contains("holger-release")),
        "{e:?}"
    );
    let e = book
        .put(&SitePut {
            operator_site: false,
            ..root_site("holger-rs", "holger.rs", "holger", "holger-release")
        })
        .unwrap_err();
    assert!(
        matches!(&e, SiteBookRefusal::Bad(w) if w.contains("[site-product-homeless]")),
        "{e:?}"
    );

    let done = book
        .put(&root_site(
            "holger-rs",
            "holger.rs",
            "holger",
            "holger-release",
        ))
        .unwrap();
    assert!(done.words.contains("holger-rs is added"), "{}", done.words);
    let f = solo::feed_now(root, &BTreeMap::new());
    let block: toml::Table = toml::from_str(&f["plugin.holger.toml"]).unwrap();
    assert_eq!(
        block["instance"].as_str(),
        Some("holger-rs"),
        "holger is pulled from its first site"
    );
    assert!(
        !toml::from_str::<toml::Table>(&f["plugin.gunnar.toml"])
            .unwrap()
            .contains_key("instance"),
        "gunnar still has no home"
    );
    let site = estates(&f)["holger-rs"].as_table().unwrap().clone();
    assert_eq!(
        (
            site["site"].as_str(),
            site["vendor"].as_str(),
            site["operator_site"].as_bool()
        ),
        (Some("holger.rs"), Some("stripe/holger"), Some(true))
    );
    let o = site["orders"].as_table().unwrap();
    assert_eq!(
        o["media"]["holger"].as_str(),
        Some("file:/media/holger-release.img"),
        "the product's fed media"
    );
    assert_eq!(
        o["dns"].as_str(),
        Some("cloudflare-holger"),
        "the product's own zone writer"
    );
    assert!(o.contains_key("release_notes"));
    assert_eq!(
        solo::judge(root, &f).unwrap(),
        vec!["holger".to_string()],
        "the loader serves holger now"
    );

    book.put(&root_site(
        "gunnar-rs",
        "gunnar.rs",
        "gunnar",
        "gunnar-release",
    ))
    .unwrap();
    book.put(&SitePut {
        terminable: true,
        ..root_site(
            "test2-holger-rs",
            "test2.holger.rs",
            "holger",
            "holger-release",
        )
    })
    .unwrap();
    let f = solo::feed_now(root, &BTreeMap::new());
    assert_eq!(
        toml::from_str::<toml::Table>(&f["plugin.holger.toml"]).unwrap()["instance"].as_str(),
        Some("holger-rs"),
        "a second site keeps the home"
    );
    assert_eq!(
        toml::from_str::<toml::Table>(&f["plugin.gunnar.toml"]).unwrap()["instance"].as_str(),
        Some("gunnar-rs")
    );
    assert_eq!(
        estates(&f)["gunnar-rs"]["orders"]["dns"].as_str(),
        Some("cloudflare")
    );
    assert_eq!(
        solo::judge(root, &f).unwrap(),
        vec!["gunnar".to_string(), "holger".to_string()]
    );
}