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()]
);
}