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/add_twin.rs

//! ★ **ADDING A TWIN to a Gunnar-shaped estate** (monetize v2.0 A4-FACTS, item 3).
//!
//! The flock ([`crate::testing::flock`]) is Gunnar's three machines with its twin OPTIONAL: an
//! order `{twin: false}` lays a front and a main alone. Before A4-FACTS the cloud's plan check
//! held every plan that names a main or a twin to exactly one front, one main and one twin, so
//! such an estate could not be laid, and its twin could never be laid beside it. The check now
//! takes its shape from the product's facts (its roles, which may be left out), and a plan
//! change lays the twin, pairs it and lets it follow main.
//!
//! ```text
//!   order {twin: false} ─▶ front, main laid (the plan carries the flock's shape)
//!   ChangePlan {twin: true} ─▶ the twin is not laid: an ADD (facts.grow may_add twin)
//!     ─▶ its plan the twin rule's (chosen from main's), its data disk main's whole set
//!     ─▶ held, paid ─▶ the twin laid beside the estate (added=change-1), installed (paired),
//!        its key named to the guardian ─▶ it calls home ─▶ captured, done
//!     ─▶ it follows: the run's twin is the new one, the twin rule reads it
//! ```

use super::*;
use crate::testing::flock;

fn flock_cfg() -> EstateOrderConfig {
    flock::install();
    EstateOrderConfig {
        lifecycle: flock::KIND.into(),
        ..cfg()
    }
}

fn roles_laid(r: &Rig) -> Vec<String> {
    r.state
        .estates
        .get("acme")
        .and_then(|run| run.laid)
        .map(|l| flock::roles_of(&l))
        .unwrap_or_default()
}

/// A flock delivered and paid at `sizes`.
fn delivered_flock(name: &str, sizes: &EstateSizes) -> Rig {
    let r = rig_with(name, flock_cfg(), "PASS");
    start_for(&r.state, "acme", "buyer@example.com", "", "alice", sizes).expect("ordered");
    r.pay.authorize(&handle_of(&r, "acme"));
    drive(&r.state, "acme");
    let run = r.state.estates.get("acme").unwrap();
    assert_eq!(run.phase, phase::CAPTURED, "{:#?}", run.history);
    r
}

/// ★ **A Gunnar-shaped estate with no twin is laid, and a plan change lays its twin, pairs it
/// and lets it follow main.**
#[test]
fn a_gunnar_shaped_estate_laid_without_its_twin_takes_one_by_a_plan_change() {
    let r = delivered_flock("add-twin", &flock::sizes(false, 0, 10));
    assert_eq!(
        roles_laid(&r),
        ["front", "main"],
        "a front and a main alone: the flock's twin is optional"
    );
    let before = r.state.estates.get("acme").unwrap();
    assert!(
        before.quote.machines.iter().all(|m| m.role != "twin"),
        "nothing is priced for a twin it has not got: {:?}",
        before.quote.machines
    );
    let lays_before = r.cloud.lays().len();

    let c = change::change(
        &r.state,
        "acme",
        &EstateSizes {
            order: serde_json::json!({"twin": true}),
            ..EstateSizes::default()
        },
    )
    .unwrap_or_else(|e| panic!("a twin is added by a plan change: {e:?}"));
    assert_eq!(
        c.adds.iter().map(|a| a.role.as_str()).collect::<Vec<_>>(),
        ["twin"],
        "{c:#?}"
    );
    assert!(
        c.steps.is_empty(),
        "nothing that stands is resized: {:?}",
        c.steps
    );
    assert_eq!(c.status, change::status::WAITING_FOR_PAYMENT);
    assert!(
        c.quote.monthly.minor > before.quote.monthly.minor,
        "the twin is priced in"
    );
    // The twin follows main: its plan is the twin rule's for main's, its data disk main's set.
    let line = |role: &str| {
        c.quote
            .machines
            .iter()
            .find(|m| m.role == role)
            .cloned()
            .unwrap_or_else(|| panic!("no {role} in {:?}", c.quote.machines))
    };
    assert_eq!(
        line("twin").plan,
        c.quote.twin_plan,
        "the twin's plan is chosen from main's"
    );
    let main_set: u64 = line("main").disks_gib.iter().skip(1).sum();
    assert!(
        line("twin").disks_gib.get(1).copied().unwrap_or(0) >= main_set,
        "the twin's data disk holds main's whole set: {:?} / {:?}",
        line("twin"),
        line("main")
    );
    assert!(
        c.words
            .contains(&format!("a new twin ({})", c.quote.twin_plan)),
        "{}",
        c.words
    );
    assert_eq!(
        r.cloud.lays().len(),
        lays_before,
        "nothing is laid before the money is held"
    );

    r.pay.authorize(&c.handle.as_ref().expect("a hold").id);
    drive(&r.state, "acme");
    drive(&r.state, "acme");
    let run = r.state.estates.get("acme").unwrap();
    assert!(run.change.is_none(), "{:#?}", run.change);
    let done = run.changes.last().expect("the change is kept");
    assert_eq!(
        done.status,
        change::status::DONE,
        "{done:#?}\n{:#?}",
        run.history
    );
    assert_eq!(
        roles_laid(&r),
        ["front", "main", "twin"],
        "the twin is laid beside the estate"
    );
    assert_eq!(
        r.cloud.lays().len(),
        lays_before + 1,
        "one lay: the twin alone"
    );
    assert!(
        r.kit
            .roles
            .lock()
            .unwrap()
            .iter()
            .any(|role| role == "twin"),
        "the installer paired the twin: {:?}",
        r.kit.roles.lock().unwrap()
    );
    let g = r.state.fleet.guardian().unwrap();
    assert!(
        g.store
            .box_record("acme", "twin")
            .is_some_and(|b| b.heard_unix_ms > 0),
        "the twin called home"
    );
    assert_eq!(
        r.pay.captures(),
        2,
        "the change's hold captured once the twin was heard"
    );
    // It follows main: the run's twin is the one laid, at the rule's plan, and the shop's twin
    // reading names it beside main.
    assert_eq!(run.quote.twin_plan, c.quote.twin_plan);
    assert!(
        run.quote.machines.iter().any(|m| m.role == "twin"),
        "the bill follows: the renewal prices the twin"
    );
    let life = lifecycle::of(r.state.estates.cfg.as_ref().unwrap()).unwrap();
    assert_eq!(
        twin::reviewing_roles(&life.twin().expect("the flock has a twin")),
        ["main", "twin"]
    );
    assert!(
        r.mails
            .last()
            .body
            .contains(&format!("a new twin runs on {}", c.quote.twin_plan)),
        "{}",
        r.mails.last().body
    );
}

/// ★ **The plan the core hands the cloud carries the product's shape** (A4-FACTS): every lay and
/// every price is checked against the flock's roles — the twin optional, a node laid n times —
/// never against Gunnar's three machines.
#[test]
fn the_plan_the_core_lays_carries_the_products_shape() {
    let c = flock_cfg();
    let plan = lifecycle::lay_plan_of(&c, "acme", &flock::sizes(false, 2, 10), "STARTER-1xCPU-4GB")
        .unwrap();
    assert_eq!(
        plan.machines
            .iter()
            .map(|m| m.role.as_str())
            .collect::<Vec<_>>(),
        ["front", "main", "node", "node-2"]
    );
    let shape = plan.shape.clone().expect("the shape rides the plan");
    let twin = shape
        .roles
        .iter()
        .find(|r| r.role == "twin")
        .expect("the twin is a role");
    assert!(twin.optional && !twin.many);
    assert!(shape.roles.iter().any(|r| r.role == "node" && r.many));
    assert!(
        plan.check().is_ok(),
        "a Gunnar-shaped estate with no twin is an estate of this product: {:?}",
        plan.check()
    );
    // Gunnar's own shape keeps its three machines.
    crate::testing::lifecycles::install();
    let g = lifecycle::lay_plan_of(&cfg(), "acme", &EstateSizes::default(), "STARTER-1xCPU-4GB")
        .unwrap();
    let mut two = g.clone();
    two.machines.retain(|m| m.role != "twin");
    assert!(
        matches!(two.check(), Err(monetize_cloud::CloudError::Refused(w)) if w.contains("has no twin")),
        "Gunnar's twin is not optional"
    );
}