crates/monetize-server/src/estate_order/tests/owed_site.rs
//! **F3 (2026-10-20): an operator site whose setup failed after payment (`owed`).**
//!
//! * **Terminate is monetize's to offer**: an owed site whose machines still stand at the
//! cloud lists `terminable` (its boxes can be removed), and its words say so — or, protected,
//! say how to allow it. The Sites page shows what monetize says, nothing of its own.
//! * **"Set up again" never re-lays onto the old boxes**: the operator's retry first gives
//! back what the failed setup left (servers, disks, clones, DNS — the give-back a terminate
//! and Gunnar's own paid retry take), then lays fresh from the CURRENT release.
use super::*;
pub(super) const SITE_REF: &str = "estate/gunnarrs";
/// The release media a site rig lays from (the front's and gunnar's kinds).
fn media(release: &str) -> BTreeMap<String, String> {
[
("front".to_string(), format!("{release}-front-release")),
("gunnar".to_string(), format!("{release}-gunnar-release")),
]
.into()
}
/// [`site_rig_with`] on a given dir, cloud, vendor and kit, laying from `release`'s media — so
/// a second rig on the same dir is the ONE box after an upgrade, reading the same runs.
pub(super) fn site_rig_on(
dir: &std::path::Path,
pay: &HoldPay,
cloud: &Arc<MockCloud>,
kit: &Arc<FakeKit>,
terminable: bool,
release: &str,
) -> Arc<AppState> {
let site =
|vendor: &str, site: &str, owner: &str, operator_site: bool| crate::config::EstateConfig {
product: "gunnar".into(),
vendor: vendor.into(),
site: site.into(),
owner: owner.into(),
operator_site,
..Default::default()
};
let reg = Registry::new()
.sells("gunnar", pay.clone())
.vendor(SitePay("sitepay-kunde"))
.cloud(cloud.clone())
.shared_product(shop_product())
.home("gunnar", "gunnar-rs")
.estate(
"gunnar-rs",
crate::config::EstateConfig {
terminable,
..site("holdpay", "gunnar.rs", "ignalina", true)
},
);
let (state, _clock, _mails, _) = build_in(
reg,
dir,
EstateOrderConfig {
vendor: String::new(),
apex: String::new(),
media: media(release),
..cfg()
},
kit.clone(),
None,
None,
);
state
}
/// The site laid and running on `release`, then its setup failed after payment with its
/// machines left standing (`laid` kept as `keep_laid` says): the run is `owed`.
pub(super) fn owed_with_machines(
name: &str,
terminable: bool,
keep_laid: bool,
) -> (
std::path::PathBuf,
HoldPay,
Arc<MockCloud>,
Arc<FakeKit>,
Arc<AppState>,
Vec<String>,
) {
let dir = tmp(name);
let (pay, cloud, kit) = (
HoldPay::default(),
Arc::new(MockCloud::new()),
FakeKit::new("PASS"),
);
let state = site_rig_on(&dir, &pay, &cloud, &kit, terminable, "r4");
let run = site::start_site(
&state,
"gunnar-rs",
"ops@example.invalid",
&EstateSizes::default(),
)
.unwrap();
pay.authorize(&run.handle.clone().unwrap().id);
drive(&state, "gunnarrs");
let mut run = state.estates.get("gunnarrs").unwrap();
assert_eq!(run.phase, phase::CAPTURED, "{:#?}", run.history);
let laid = run.laid.clone().unwrap();
let old: Vec<String> = laid
.machines
.iter()
.flat_map(|m| std::iter::once(m.server.clone()).chain(m.disks.iter().map(|d| d.id.clone())))
.collect();
assert!(
cloud.live_under(SITE_REF) >= old.len(),
"the old boxes stand"
);
let now = state.now();
run.phase = phase::OWED.into();
run.why = String::new();
run.failures = 2;
run.history.push(Transition {
phase: phase::FAILED.into(),
at_ms: now,
note: "the installer failed: twin did not pair".into(),
});
run.history.push(Transition {
phase: phase::OWED.into(),
at_ms: now,
note: "paid and failed 2 time(s): an operator retries it or cancels it with a refund"
.into(),
});
if !keep_laid {
run.laid = None;
}
{
let _pen = state.estates.pen("gunnarrs");
save(&state, &run).unwrap();
}
(dir, pay, cloud, kit, state, old)
}
/// ★ **An owed site whose machines stand offers Terminate**, in monetize's own word, and says
/// that Terminate removes its boxes — even when the run no longer names them (laid is gone).
#[test]
fn an_owed_site_with_machines_standing_is_terminable_and_says_so() {
let (_dir, _pay, _cloud, _kit, state, _old) =
owed_with_machines("f3-owed-terminable", true, false);
let s = site::sites(&state).remove(0);
assert_eq!(s.state, "owed", "{s:?}");
assert!(
s.terminable,
"its machines stand: Terminate is offered ({s:?})"
);
assert!(
s.words.contains("Terminate removes its boxes."),
"{}",
s.words
);
// The server allows what it offered.
let t =
terminate::terminate(&state, None, "gunnar-rs", "gunnar-rs").expect("terminate is allowed");
assert!(t.done || !t.words.is_empty(), "{t:?}");
}
/// ★ **Protected stays a guard**: a protected owed site with machines offers no Terminate and
/// says how to allow it (its Edit form), word for word as a stopped install does.
#[test]
fn a_protected_owed_site_with_machines_says_how_to_allow_terminate() {
let (_dir, _pay, _cloud, _kit, state, _old) =
owed_with_machines("f3-owed-protected", false, true);
let s = site::sites(&state).remove(0);
assert_eq!(s.state, "owed", "{s:?}");
assert!(!s.terminable, "protected: no Terminate");
assert!(s.words.contains(PROTECTED_FAILED), "{}", s.words);
assert!(
terminate::terminate(&state, None, "gunnar-rs", "gunnar-rs").is_err(),
"and terminate stays refused"
);
}
/// ★ **"Set up again" deletes the failed machines first, then lays fresh from the current
/// release** (the ONE box upgraded r4 → r5 meanwhile): every old server and disk is destroyed
/// at the cloud, none is adopted, and the new boxes are cloned from the r5 media.
#[test]
fn a_retry_of_an_owed_site_gives_back_the_old_boxes_and_lays_fresh_from_the_current_release() {
for keep_laid in [true, false] {
let (dir, pay, cloud, kit, _r4, old) =
owed_with_machines(&format!("f3-owed-retry-{keep_laid}"), true, keep_laid);
let state = site_rig_on(&dir, &pay, &cloud, &kit, true, "r5");
let retried = settle(&state, "gunnarrs", "retry").expect("the operator's retry is taken");
assert_ne!(retried.phase, phase::OWED, "{:#?}", retried.history);
drive(&state, "gunnarrs");
let run = state.estates.get("gunnarrs").unwrap();
assert_eq!(
run.phase,
phase::CAPTURED,
"keep_laid {keep_laid}: {:#?}",
run.history
);
let destroyed: Vec<String> = cloud.destroys().into_iter().map(|r| r.id).collect();
for id in &old {
assert!(
destroyed.contains(id),
"keep_laid {keep_laid}: the old {id} was not deleted: {destroyed:?}"
);
}
let laid = run.laid.clone().unwrap();
let new: Vec<String> = laid
.machines
.iter()
.flat_map(|m| {
std::iter::once(m.server.clone()).chain(m.disks.iter().map(|d| d.id.clone()))
})
.collect();
assert!(
new.iter().all(|id| !old.contains(id)),
"keep_laid {keep_laid}: an old box was reused: old {old:?} new {new:?}"
);
assert_eq!(
cloud.live_under(SITE_REF),
new.len(),
"keep_laid {keep_laid}: only the new boxes stand"
);
let media: BTreeMap<String, String> = cloud.last_media().into_iter().collect();
assert_eq!(
media.get("front").map(String::as_str),
Some("r5-front-release"),
"{media:?}"
);
assert_eq!(
media.get("main").map(String::as_str),
Some("r5-gunnar-release"),
"{media:?}"
);
assert_eq!(
media.get("twin").map(String::as_str),
Some("r5-gunnar-release"),
"{media:?}"
);
assert_eq!((pay.captures(), pay.refunds()), (1, 0), "paid once");
assert!(
run.history
.iter()
.any(|t| t.note.contains("given back first")),
"{:#?}",
run.history
);
}
}