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

nordisk/monetize

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