crates/monetize-server/src/estate_order/tests/standing_disks.rs
//! ★ **A plan change that GROWS A STANDING MACHINE'S DISK** (monetize v2.0 A4-FACTS, item 2).
//!
//! New numbers can grow the disks of machines that already stand: the flock's `node_gib` is each
//! node's data disk (the Lake's `store_gib`, spread over its backends, is the same kind of
//! number). Before A4-FACTS such a change was refused by name ("not taken yet"). Now the disks
//! are grown at the cloud by the shop itself, once paid: each one detached, resized and put back
//! at its own address on its own server, the provider's sizes read back into the run. What the
//! product's grow rule keeps (`never_shrinks node_gib`) never shrinks.
//!
//! ```text
//! ChangePlan {node_gib: 20 → 40} ─▶ node and node-2 stand: their data disks GROW
//! ─▶ priced whole, the difference for the days left HELD ─▶ paid
//! ─▶ resizing: each node's data set grown at the cloud (grow_estate_set), read back
//! ─▶ the boxes call home ─▶ CAPTURED ─▶ done; the run's price and laid sizes are the new ones
//! ChangePlan {node_gib: 40 → 30} ─▶ refused by name: node_gib never shrinks, nothing held
//! ```
use super::*;
use crate::testing::flock;
use monetize::testing::GrowScript;
fn flock_cfg() -> EstateOrderConfig {
flock::install();
EstateOrderConfig {
lifecycle: flock::KIND.into(),
..cfg()
}
}
fn data_gib(r: &Rig, role: &str) -> Vec<u64> {
r.state
.estates
.get("acme")
.unwrap()
.laid
.unwrap()
.machine(role)
.unwrap()
.disks
.iter()
.skip(1)
.map(|d| d.gib)
.collect()
}
/// ★ **New numbers grow the disks of the machines that stand, at the cloud, once paid.**
#[test]
fn a_change_that_grows_the_standing_nodes_disks_grows_them_at_the_cloud_and_they_never_shrink() {
let r = rig_with("standing-disks", flock_cfg(), "PASS");
start_for(
&r.state,
"acme",
"buyer@example.com",
"",
"alice",
&flock::sizes(true, 2, 20),
)
.expect("ordered");
r.pay.authorize(&handle_of(&r, "acme"));
drive(&r.state, "acme");
let before = r.state.estates.get("acme").unwrap();
assert_eq!(before.phase, phase::CAPTURED, "{:#?}", before.history);
assert_eq!(
(data_gib(&r, "node"), data_gib(&r, "node-2")),
(vec![20], vec![20])
);
// The provider's data sets: each node's one disk at 20 GiB.
r.cloud.grows(GrowScript {
current_gib: 20,
..GrowScript::default()
});
let c = change::change(&r.state, "acme", &flock::sizes(true, 2, 40))
.unwrap_or_else(|e| panic!("new numbers that grow standing disks are taken: {e:?}"));
assert_eq!(
c.grows
.iter()
.map(|g| (g.role.as_str(), g.from_gib, g.to_gib))
.collect::<Vec<_>>(),
[("node", 20, 40), ("node-2", 20, 40)],
"{c:#?}"
);
assert!(
c.adds.is_empty() && c.removes.is_empty() && c.steps.is_empty(),
"{c:#?}"
);
// The rig's cloud prices every resource flat (a bigger disk costs what a small one does), so
// what is held is exactly the price's rise, here none; the trials hold it at UpCloud's prices.
assert!(
c.quote.monthly.minor >= before.quote.monthly.minor,
"the bigger disks are priced in"
);
assert_eq!(
c.amount,
change::prorated(
before.quote.monthly,
c.quote.monthly,
r.state.now(),
before.paid_until_ms
),
"the difference for the days left is held"
);
assert_eq!(
c.status,
if c.amount.minor > 0 {
change::status::WAITING_FOR_PAYMENT
} else {
change::status::QUEUED
}
);
assert!(
c.words.contains("node's data disk grows from 20 to 40 GiB"),
"{}",
c.words
);
assert!(
!r.cloud
.calls()
.iter()
.any(|x| matches!(x, monetize::testing::CloudCall::Grow(_))),
"nothing grows before the money is held"
);
if let Some(h) = &c.handle {
r.pay.authorize(&h.id);
}
drive(&r.state, "acme");
drive(&r.state, "acme");
let run = r.state.estates.get("acme").unwrap();
assert!(
run.change.is_none(),
"{:#?}\n{:#?}",
run.change,
run.history
);
assert_eq!(
run.changes.last().unwrap().status,
change::status::DONE,
"{:#?}",
run.changes.last()
);
assert!(
r.cloud
.calls()
.iter()
.any(|x| matches!(x, monetize::testing::CloudCall::Grow(40))),
"grown at the cloud: {:?}",
r.cloud.calls()
);
assert_eq!(
(data_gib(&r, "node"), data_gib(&r, "node-2")),
(vec![40], vec![40]),
"the provider's sizes are read back into the run"
);
assert_eq!(
r.pay.captures(),
1 + u64::from(c.handle.is_some()),
"a hold is captured only once grown and heard"
);
assert_eq!(run.quote.monthly, c.quote.monthly, "the bill follows");
assert!(
r.mails.last().body.contains("node's data disk is 40 GiB"),
"{}",
r.mails.last().body
);
// Main's data disks were not touched: a numbers change grows only what its numbers grow.
assert!(
data_gib(&r, "main").iter().all(|g| *g == 10),
"{:?}",
data_gib(&r, "main")
);
// Data never shrinks: the flock's grow rule keeps node_gib.
let held = r.pay.0.requests.lock().unwrap().len();
let e = change::change(&r.state, "acme", &flock::sizes(true, 2, 30))
.expect_err("smaller is refused");
assert!(
format!("{e:?}").contains("data never shrinks: node_gib is 40 now, and 30 was asked"),
"{e:?}"
);
assert_eq!(r.pay.0.requests.lock().unwrap().len(), held, "nothing held");
}