1use std::collections::BTreeMap;
10
11use monetize_embed::fleet::{GrowGoAhead, OrderAck};
12use monetize_product::pull::{AddPrincipal, GrowRoom, PurgeAnswer, PurgeTenant};
13use monetize_product::EntitlementFact;
14
15use crate::order::Order;
16
17pub use crate::order::Recharge as RechargeAsk;
18
19pub const SECTORS_PER_GIB: u64 = 2_097_152;
21
22#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum Applied {
25 New(String),
27 Held(String),
29}
30
31#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum GrowState {
34 Running(String),
35 WaitingMembers(String),
37 Done(String),
38 Failed(String),
39}
40
41pub trait GrowSet {
43 fn grow_set(&mut self, gib: u64) -> Result<String, String>;
44 fn fetch(&mut self, f: &monetize_embed::fleet::Fetch) -> Result<Vec<u8>, String> {
47 Err(format!(
48 "this box has no pull channel to fetch {} on",
49 f.param
50 ))
51 }
52}
53
54pub struct NoAsk;
56impl GrowSet for NoAsk {
57 fn grow_set(&mut self, _: u64) -> Result<String, String> {
58 Err("this box has no pull channel to ask monetize for its disks".into())
59 }
60}
61
62pub trait HolgerBox {
65 fn serving(&self) -> bool;
67 fn health(&self) -> BTreeMap<String, String> {
69 BTreeMap::new()
70 }
71 fn product_report(&self) -> Option<String> {
73 None
74 }
75 fn own_review(&self) -> Option<String> {
77 None
78 }
79 fn entitle(&self, fact: &EntitlementFact) -> Result<Applied, String> {
81 Err(format!(
82 "this box holds no entitlements, so the fact for {} is not applied",
83 fact.tenant.0
84 ))
85 }
86 fn purge(&self, p: &PurgeTenant) -> Result<PurgeAnswer, String> {
88 Err(format!(
89 "this box purges no repositories, so {} is not purged",
90 p.tenant
91 ))
92 }
93 fn sign_in_again(&self, s: &crate::order::SignInAgain) -> Result<String, String> {
96 let _ = (&s.to, &s.host, s.ttl_secs);
97 Err(format!(
98 "this box mints no sign-in links, so it cannot run sign_in_again for {}",
99 s.account
100 ))
101 }
102 fn keep_refeed(&self, r: &crate::order::Refeed) -> Result<String, String> {
105 Err(format!(
106 "this box keeps no estate keys fed again, so {} is not kept",
107 r.keys.keys().cloned().collect::<Vec<_>>().join(", ")
108 ))
109 }
110 fn keep_setting(&self, name: &str, value: &[u8]) -> Result<String, String> {
114 let _ = value;
115 Err(format!("this box keeps no settings, so {name} is not kept"))
116 }
117 fn add_principal(&self, a: &AddPrincipal) -> Result<String, String> {
120 Err(format!(
121 "this box makes no repositories for a signup, so {} is not made",
122 a.username
123 ))
124 }
125 fn want_room(&self, g: &GrowRoom) -> Result<String, String> {
127 Err(format!(
128 "this box grows no data set, so {} bytes wanted cannot be kept",
129 g.need_bytes
130 ))
131 }
132 fn grow_carried(&self, data_gib: u64) -> Result<bool, String> {
134 Err(format!(
135 "this box grows no data set, so it cannot grow to {data_gib} GiB"
136 ))
137 }
138 fn twin_holds(&self, data_gib: u64) -> Result<String, String> {
140 Err(format!(
141 "this box cannot see whether its twin holds {data_gib} GiB a disk"
142 ))
143 }
144 fn grow_start(&self, data_gib: u64, nonce: &str) -> Result<GrowState, String> {
146 let _ = (data_gib, nonce);
147 Err("this box grows no data set".into())
148 }
149 fn grow_settle(&self, past_waiting: bool) -> GrowState {
151 let _ = past_waiting;
152 GrowState::Failed("this box grows no data set".into())
153 }
154 fn grow_look_again(&self, give_up: bool) -> Result<(), String> {
156 let _ = give_up;
157 Err("this box grows no data set".into())
158 }
159 fn recharge(&self, r: &RechargeAsk) -> Result<String, String> {
161 Err(format!(
162 "this box runs no recharge lane, so {} is not filled from {}",
163 r.main, r.twin
164 ))
165 }
166 fn fence(&self, primary: &str, epoch: u64) -> Result<String, String> {
169 let _ = epoch;
170 Err(format!("this box cannot fence the primary at {primary}: the guardian fences it at the cloud (the recharge's `fenced` proof)"))
171 }
172 fn adopt(&self, twin: &str, principal: &str) -> Result<String, String> {
174 let _ = principal;
175 Err(format!(
176 "this box cannot certify the twin at {twin}: the front's certification does"
177 ))
178 }
179 fn product_order(
185 &self,
186 by: &OrderedBy,
187 kind: &str,
188 params: &BTreeMap<String, String>,
189 file: Option<&[u8]>,
190 ) -> Result<String, String> {
191 let _ = (by, params, file);
192 Err(format!("this box runs no {kind} order"))
193 }
194 fn become_twin(&self, primary: &str) -> Result<String, String> {
195 Err(format!("this box cannot become the twin of {primary}"))
196 }
197 fn review(&self) -> Result<serde_json::Value, String> {
199 Err("this box cannot review a twin".into())
200 }
201}
202
203pub use monetize_estate_side::pump::Ctx;
205
206fn refeed<B: HolgerBox + ?Sized>(b: &B, r: &crate::order::Refeed) -> Result<String, String> {
211 let mut said: Vec<String> = Vec::new();
212 let kept = r.kept_keys();
213 if !kept.keys.is_empty() {
214 said.push(b.keep_refeed(&kept)?);
215 }
216 for (name, value) in r.settings() {
217 match b.keep_setting(name, &value) {
218 Ok(words) => said.push(words),
219 Err(why) if !said.is_empty() => {
220 return Err(format!("{why} (kept before it: {})", said.join("; ")))
221 }
222 Err(why) => return Err(why),
223 }
224 }
225 Ok(said.join("; "))
226}
227
228fn said(seq: u64, r: Result<String, String>) -> OrderAck {
229 match r {
230 Ok(detail) => OrderAck {
231 seq,
232 ok: true,
233 detail,
234 receipt_json: String::new(),
235 },
236 Err(detail) => OrderAck {
237 seq,
238 ok: false,
239 detail,
240 receipt_json: String::new(),
241 },
242 }
243}
244
245#[derive(Debug, Clone, PartialEq, Eq)]
249pub struct OrderedBy {
250 pub seq: u64,
251 pub signer: String,
252}
253
254impl OrderedBy {
255 pub fn principal(&self) -> String {
257 format!("fleet-order {} signed {}", self.seq, self.signer)
258 }
259}
260
261pub fn execute<B: HolgerBox + ?Sized>(
264 b: &B,
265 ctx: &Ctx,
266 seq: u64,
267 order: &Order,
268 ask: &mut dyn GrowSet,
269) -> OrderAck {
270 let not_serving = || {
271 Err::<String, String>("this box does not serve as the primary (a twin, or awaiting its recharge): nothing applied".into())
272 };
273 match order {
274 Order::Entitlement(fact) => {
275 if !b.serving() {
276 return said(seq, not_serving());
277 }
278 if let Err(e) = monetize_embed::signing::verify_fact(fact, ctx.key) {
279 return said(seq, Err(format!("the fact for {} does not verify under this box's monetize key ({e}): nothing moved", fact.tenant.0)));
280 }
281 said(
282 seq,
283 b.entitle(fact).map(|a| match a {
284 Applied::New(w) | Applied::Held(w) => w,
285 }),
286 )
287 }
288 Order::RestateEntitlements(snap) => {
289 if !b.serving() {
290 return said(seq, not_serving());
291 }
292 if let Err(e) = monetize_embed::signing::verify_snapshot(snap, ctx.key) {
293 return said(seq, Err(format!("the snapshot does not verify under this box's monetize key ({e}): nothing restated")));
294 }
295 let (mut new, mut held) = (0usize, 0usize);
296 for fact in &snap.facts {
297 match b.entitle(fact) {
298 Ok(Applied::New(_)) => new += 1,
299 Ok(Applied::Held(_)) => held += 1,
300 Err(e) => {
301 return said(
302 seq,
303 Err(format!(
304 "restated {new} before {} was refused: {e}",
305 fact.tenant.0
306 )),
307 )
308 }
309 }
310 }
311 said(seq, Ok(format!("restated {new} entitlement fact(s) ({held} already held as new or newer); nothing deleted")))
312 }
313 Order::PurgeTenant(p) => {
314 if !b.serving() {
315 return said(seq, not_serving());
316 }
317 match b.purge(p) {
318 Ok(answer) => OrderAck {
319 seq,
320 ok: true,
321 detail: serde_json::to_string(&answer).unwrap_or_default(),
322 receipt_json: String::new(),
323 },
324 Err(e) => said(seq, Err(e)),
325 }
326 }
327 Order::GrowRoom(g) => said(seq, b.want_room(g)),
328 Order::AddPrincipal(a) => {
329 if !b.serving() {
330 return said(seq, not_serving());
331 }
332 said(seq, b.add_principal(a))
333 }
334 Order::SignInAgain(s) => {
335 if !b.serving() {
336 return said(seq, not_serving());
337 }
338 said(seq, b.sign_in_again(s))
339 }
340 Order::Refeed(r) => said(seq, refeed(b, r)),
343 Order::GrowGoAhead(g) => grow(b, ctx, seq, g, ask),
344 Order::RechargeFromTwin(r) => {
345 let fenced = match r.fence.as_deref().map(str::trim) {
351 Some(old) if old == r.main.trim() => {
352 return said(seq, Err(format!("order {seq} names the main {} as the primary to fence: a recharge never fences the box it fills", r.main)));
353 }
354 Some(old) => match b.fence(old, r.epoch) {
355 Ok(_) => Some(old.to_string()),
356 Err(e) => return said(seq, Err(format!("order {seq} fences {old} first and that fence did not hold ({e}): nothing was recharged"))),
357 },
358 None => None,
359 };
360 let prefix = match (&fenced, r.fenced.as_deref()) {
361 (Some(f), _) => format!("fenced {f}; "),
362 (None, Some(p)) => format!("cloud-fenced ({p}); "),
363 (None, None) => String::new(),
364 };
365 said(
366 seq,
367 b.recharge(r)
368 .map(|w| format!("{prefix}{w}"))
369 .map_err(|e| format!("{prefix}{e}")),
370 )
371 }
372 Order::FencePrimary { primary, epoch } => said(seq, b.fence(primary, *epoch)),
373 Order::AdoptTwin { twin, principal } => said(seq, b.adopt(twin, principal)),
374 Order::BecomeTwin { primary } => said(seq, b.become_twin(primary)),
375 Order::Product { kind, body } => {
376 if !b.serving() {
377 return said(seq, not_serving());
378 }
379 let file = match &body.fetch {
380 None => None,
381 Some(f) => match ask.fetch(f) {
382 Ok(bytes) => Some(bytes),
383 Err(e) => {
384 return said(
385 seq,
386 Err(format!(
387 "order {seq} ({kind}): its file {} was not pulled: {e}",
388 f.param
389 )),
390 )
391 }
392 },
393 };
394 let by = OrderedBy {
395 seq,
396 signer: crate::hex(&ctx.key.to_bytes()),
397 };
398 said(
399 seq,
400 b.product_order(&by, kind, &body.params, file.as_deref()),
401 )
402 }
403 Order::ReviewTwin => match b.review() {
404 Ok(v) => OrderAck {
405 seq,
406 ok: true,
407 detail: v
408 .get("verdict")
409 .and_then(|w| w.as_str())
410 .unwrap_or("reviewed")
411 .to_string(),
412 receipt_json: v.to_string(),
413 },
414 Err(e) => said(seq, Err(e)),
415 },
416 }
417}
418
419fn grow<B: HolgerBox + ?Sized>(
423 b: &B,
424 ctx: &Ctx,
425 seq: u64,
426 g: &GrowGoAhead,
427 ask: &mut dyn GrowSet,
428) -> OrderAck {
429 let fail = |w: String| said(seq, Err(w));
430 let step = match monetize_embed::signing::verify_estate_growth(g.go_ahead.as_bytes(), ctx.key) {
431 Ok(s) => s,
432 Err(e) => {
433 return fail(format!(
434 "the paid step does not verify under this box's monetize key ({e}): nothing moved"
435 ))
436 }
437 };
438 if step.estate != ctx.me.instance {
439 return fail(format!(
440 "the paid step is estate {}'s; this box pulls as {}: nothing moved",
441 step.estate, ctx.me.instance
442 ));
443 }
444 if step.until_unix_ms <= ctx.now_unix_ms as i64 {
445 return fail(format!(
446 "the paid step was good until {} (unix ms): its period is over, nothing moved",
447 step.until_unix_ms
448 ));
449 }
450 let twin = ctx.me.role == "twin";
451 let main_gib = step.main_data_gib_per_member;
452 if !twin && g.data_gib != main_gib {
453 return fail(format!("the order sizes main's disks to {} GiB, the paid step to {main_gib} GiB: nothing moved", g.data_gib));
454 }
455 if twin && g.data_gib < main_gib {
456 return fail(format!("the order sizes the twin's disk to {} GiB, under main's {main_gib} GiB a disk: nothing moved", g.data_gib));
457 }
458 match b.grow_carried(g.data_gib) {
459 Ok(true) => {
460 return said(
461 seq,
462 Ok(format!(
463 "already {} GiB a data disk: nothing moved",
464 g.data_gib
465 )),
466 )
467 }
468 Ok(false) => {}
469 Err(e) => return fail(e),
470 }
471 if !twin {
472 if let Err(e) = b.twin_holds(g.data_gib) {
473 return fail(e);
474 }
475 }
476 match b.grow_start(g.data_gib, &format!("pull-{seq}-{}", step.reference)) {
477 Ok(GrowState::Done(d)) => return said(seq, Ok(d)),
478 Ok(GrowState::Failed(f)) => return fail(f),
479 Ok(_) => {}
480 Err(e) => return fail(e),
481 }
482 match b.grow_settle(false) {
483 GrowState::WaitingMembers(_) => {}
484 GrowState::Done(d) => return said(seq, Ok(d)),
485 GrowState::Failed(f) | GrowState::Running(f) => return fail(f),
486 }
487 let asked = ask.grow_set(g.data_gib);
488 if let Err(e) = b.grow_look_again(asked.is_err()) {
489 return fail(format!("the grow machine would not look again: {e}"));
490 }
491 let end = b.grow_settle(true);
492 match (asked, end) {
493 (Ok(words), GrowState::Done(d)) => said(seq, Ok(format!("{d} (monetize: {words})"))),
494 (
495 Ok(words),
496 GrowState::Failed(f) | GrowState::Running(f) | GrowState::WaitingMembers(f),
497 ) => fail(format!("{f} (monetize: {words})")),
498 (
499 Err(e),
500 GrowState::Done(s)
501 | GrowState::Failed(s)
502 | GrowState::Running(s)
503 | GrowState::WaitingMembers(s),
504 ) => fail(format!(
505 "monetize did not size this box's disks ({e}); /data: {s}"
506 )),
507 }
508}
509
510#[cfg(test)]
511pub(crate) mod tests {
512 use super::*;
513 use crate::order::tests::{me, monetize_key, MONETIZE_SEED};
514 use ed25519_dalek::Signer as _;
515 use monetize_product::{State, TenantId};
516 use std::cell::RefCell;
517
518 #[derive(Default)]
520 pub struct Fake {
521 pub twin: bool,
522 pub facts: RefCell<BTreeMap<String, EntitlementFact>>,
523 pub did: RefCell<Vec<String>>,
524 pub carried: bool,
525 pub settle: RefCell<Vec<GrowState>>,
526 }
527
528 impl HolgerBox for Fake {
529 fn serving(&self) -> bool {
530 !self.twin
531 }
532 fn entitle(&self, fact: &EntitlementFact) -> Result<Applied, String> {
533 let mut f = self.facts.borrow_mut();
534 if f.get(&fact.tenant.0)
535 .is_some_and(|h| h.issued_unix_ms >= fact.issued_unix_ms)
536 {
537 return Ok(Applied::Held(format!("{}: held", fact.tenant.0)));
538 }
539 f.insert(fact.tenant.0.clone(), fact.clone());
540 Ok(Applied::New(format!(
541 "{} is {:?}",
542 fact.tenant.0, fact.state
543 )))
544 }
545 fn purge(&self, p: &PurgeTenant) -> Result<PurgeAnswer, String> {
546 self.did.borrow_mut().push(format!("purge {}", p.tenant));
547 Ok(PurgeAnswer {
548 job: format!("purge:{}", p.tenant),
549 detail: "3 artifacts".into(),
550 ..Default::default()
551 })
552 }
553 fn want_room(&self, g: &GrowRoom) -> Result<String, String> {
554 self.did.borrow_mut().push(format!("room {}", g.need_bytes));
555 Ok("kept".into())
556 }
557 fn sign_in_again(&self, s: &crate::order::SignInAgain) -> Result<String, String> {
558 self.did.borrow_mut().push(format!(
559 "sign-in {} {} {} {}",
560 s.account, s.to, s.host, s.ttl_secs
561 ));
562 Ok(format!(
563 "a new sign-in link for {} was mailed to {}",
564 s.account, s.to
565 ))
566 }
567 fn keep_refeed(&self, r: &crate::order::Refeed) -> Result<String, String> {
568 self.did.borrow_mut().push(format!(
569 "refeed {}",
570 r.keys.keys().cloned().collect::<Vec<_>>().join(",")
571 ));
572 Ok(format!(
573 "kept {}",
574 r.keys.keys().cloned().collect::<Vec<_>>().join(", ")
575 ))
576 }
577 fn keep_setting(&self, name: &str, value: &[u8]) -> Result<String, String> {
578 let v = String::from_utf8_lossy(value).into_owned();
579 self.did.borrow_mut().push(format!("setting {name} {v}"));
580 Ok(format!("{name} is {v}"))
581 }
582 fn add_principal(&self, a: &AddPrincipal) -> Result<String, String> {
583 self.did
584 .borrow_mut()
585 .push(format!("signup {} {}", a.username, a.signup));
586 if a.username == "theirs" {
587 Err("taken: theirs is another signup's".into())
588 } else {
589 Ok(a.username.clone())
590 }
591 }
592 fn grow_carried(&self, _: u64) -> Result<bool, String> {
593 Ok(self.carried)
594 }
595 fn twin_holds(&self, gib: u64) -> Result<String, String> {
596 self.did.borrow_mut().push(format!("twin_holds {gib}"));
597 Ok("the twin holds it".into())
598 }
599 fn grow_start(&self, gib: u64, nonce: &str) -> Result<GrowState, String> {
600 self.did.borrow_mut().push(format!("start {gib} {nonce}"));
601 Ok(GrowState::Running("draining".into()))
602 }
603 fn grow_settle(&self, past: bool) -> GrowState {
604 self.did.borrow_mut().push(format!("settle {past}"));
605 let mut s = self.settle.borrow_mut();
606 if s.is_empty() {
607 if past {
608 GrowState::Done("grown".into())
609 } else {
610 GrowState::WaitingMembers("waiting-members".into())
611 }
612 } else {
613 s.remove(0)
614 }
615 }
616 fn grow_look_again(&self, give_up: bool) -> Result<(), String> {
617 self.did.borrow_mut().push(format!("look_again {give_up}"));
618 Ok(())
619 }
620 fn recharge(&self, r: &RechargeAsk) -> Result<String, String> {
621 self.did.borrow_mut().push(format!(
622 "recharge {} from {} lost={}",
623 r.main, r.twin, r.accept_lost
624 ));
625 Ok("recharged".into())
626 }
627 fn fence(&self, primary: &str, epoch: u64) -> Result<String, String> {
628 self.did
629 .borrow_mut()
630 .push(format!("fence {primary} {epoch}"));
631 if primary.starts_with("stuck") {
632 Err(format!("{primary} answers nothing"))
633 } else {
634 Ok(format!("{primary} is fenced at epoch {epoch}"))
635 }
636 }
637 fn adopt(&self, twin: &str, principal: &str) -> Result<String, String> {
638 self.did
639 .borrow_mut()
640 .push(format!("adopt {twin} {principal}"));
641 Ok("follows".into())
642 }
643 fn review(&self) -> Result<serde_json::Value, String> {
644 Ok(serde_json::json!({"verdict": "the twin is whole", "complete": true}))
645 }
646 fn become_twin(&self, primary: &str) -> Result<String, String> {
647 self.did.borrow_mut().push(format!("become_twin {primary}"));
648 Ok("elected".into())
649 }
650 fn product_order(
651 &self,
652 by: &OrderedBy,
653 kind: &str,
654 params: &BTreeMap<String, String>,
655 file: Option<&[u8]>,
656 ) -> Result<String, String> {
657 self.did.borrow_mut().push(format!(
658 "{kind} {params:?} {:?} by {}",
659 file.map(<[u8]>::len),
660 by.seq
661 ));
662 Ok(format!("{{\"ran\":\"{kind}\"}}"))
663 }
664 }
665
666 pub fn fact(repo: &str, issued: u64, signer: [u8; 32]) -> EntitlementFact {
667 let mut f = EntitlementFact {
668 tenant: TenantId(repo.into()),
669 plan: "o-1".into(),
670 state: State::Paid,
671 paid_until_unix_ms: Some(99_000_000),
672 caps: Default::default(),
673 source: "payment:stripe:o-1".into(),
674 signature: Vec::new(),
675 issued_unix_ms: (issued != 0).then_some(issued),
676 issued_signature: Vec::new(),
677 };
678 let key = ed25519_dalek::SigningKey::from_bytes(&signer);
679 f.signature = key
680 .sign(&monetize_embed::signing::fact_message(&f))
681 .to_bytes()
682 .to_vec();
683 if issued != 0 {
684 f.issued_signature = key
685 .sign(&monetize_embed::signing::fact_message_issued(&f))
686 .to_bytes()
687 .to_vec();
688 }
689 f
690 }
691
692 fn run(b: &dyn HolgerBox, role: &str, o: Order) -> OrderAck {
693 let (m, k) = (me(role), monetize_key());
694 execute(
695 b,
696 &Ctx {
697 me: &m,
698 key: &k,
699 now_unix_ms: 10_000,
700 },
701 5,
702 &o,
703 &mut NoAsk,
704 )
705 }
706
707 struct Asked(Vec<u64>, bool);
708 impl GrowSet for Asked {
709 fn grow_set(&mut self, gib: u64) -> Result<String, String> {
710 self.0.push(gib);
711 if self.1 {
712 Ok("resized".into())
713 } else {
714 Err("no answer".into())
715 }
716 }
717 }
718
719 fn growth(estate: &str, gib: u64, until: i64) -> String {
720 let mut g = monetize_embed::signing::EstateGrowth {
721 v: 1,
722 signer: "shop".into(),
723 estate: estate.into(),
724 main_data_gib_per_member: gib,
725 until_unix_ms: until,
726 reference: "o-7".into(),
727 issued_unix_ms: 1,
728 signature: String::new(),
729 };
730 let sig = ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED)
731 .sign(&monetize_embed::signing::estate_growth_message(&g));
732 g.signature = monetize_embed::signing::base64_encode(&sig.to_bytes());
733 serde_json::to_string(&g).unwrap()
734 }
735
736 #[test]
737 fn entitlement_is_applied_newer_wins_and_a_forgery_moves_nothing() {
738 let b = Fake::default();
739 let a = run(
740 &b,
741 "main",
742 Order::Entitlement(fact("libs", 5, MONETIZE_SEED)),
743 );
744 assert!(a.ok, "{a:?}");
745 assert!(b.facts.borrow().contains_key("libs"));
746 let again = run(
747 &b,
748 "main",
749 Order::Entitlement(fact("libs", 4, MONETIZE_SEED)),
750 );
751 assert!(
752 again.ok && again.detail.contains("held"),
753 "an older fact is held, not refused: {again:?}"
754 );
755 let forged = run(&b, "main", Order::Entitlement(fact("other", 6, [9u8; 32])));
756 assert!(
757 !forged.ok && forged.detail.contains("does not verify"),
758 "{forged:?}"
759 );
760 assert!(!b.facts.borrow().contains_key("other"));
761 let twin = Fake {
762 twin: true,
763 ..Default::default()
764 };
765 let refused = run(
766 &twin,
767 "twin",
768 Order::Entitlement(fact("libs", 5, MONETIZE_SEED)),
769 );
770 assert!(
771 !refused.ok && refused.detail.contains("does not serve"),
772 "{refused:?}"
773 );
774 }
775
776 #[test]
779 fn a_sign_in_again_reaches_the_serving_box_and_a_twin_mints_nothing() {
780 let s = crate::order::SignInAgain {
781 account: "acme".into(),
782 to: "buyer@mock.invalid".into(),
783 host: "acme.holger.mock.invalid".into(),
784 ttl_secs: 86_400,
785 };
786 let b = Fake::default();
787 let a = run(&b, "main", Order::SignInAgain(s.clone()));
788 assert!(
789 a.ok && a.detail.contains("mailed to buyer@mock.invalid"),
790 "{a:?}"
791 );
792 assert_eq!(
793 b.did.borrow().as_slice(),
794 ["sign-in acme buyer@mock.invalid acme.holger.mock.invalid 86400"]
795 );
796 let twin = Fake {
797 twin: true,
798 ..Default::default()
799 };
800 let refused = run(&twin, "twin", Order::SignInAgain(s.clone()));
801 assert!(
802 !refused.ok
803 && refused.detail.contains("does not serve")
804 && twin.did.borrow().is_empty(),
805 "{refused:?}"
806 );
807 struct Bare;
808 impl HolgerBox for Bare {
809 fn serving(&self) -> bool {
810 true
811 }
812 }
813 let none = run(&Bare, "main", Order::SignInAgain(s));
814 assert!(
815 !none.ok && none.detail.contains("mints no sign-in link"),
816 "{none:?}"
817 );
818 }
819
820 #[test]
825 fn a_refeeds_public_site_switch_is_kept_as_the_consoles_setting() {
826 let keys = |kv: &[(&str, &str)]| crate::order::Refeed {
827 keys: kv
828 .iter()
829 .map(|(k, v)| (k.to_string(), v.to_string()))
830 .collect(),
831 };
832 let b = Fake::default();
833 let a = run(
834 &b,
835 "main",
836 Order::Refeed(keys(&[
837 ("console_public_site", "on"),
838 ("public_repositories", "crates"),
839 ])),
840 );
841 assert!(a.ok && a.detail.contains("console.public-site"), "{a:?}");
842 assert_eq!(
843 b.did.borrow().as_slice(),
844 [
845 "refeed public_repositories",
846 "setting console.public-site on"
847 ]
848 );
849 let b = Fake::default();
850 let a = run(
851 &b,
852 "twin",
853 Order::Refeed(keys(&[("console_public_site", "off")])),
854 );
855 assert!(a.ok, "{a:?}");
856 assert_eq!(
857 b.did.borrow().as_slice(),
858 ["setting console.public-site off"],
859 "the switch alone keeps no key beside the feed"
860 );
861 struct KeepsKeysOnly;
862 impl HolgerBox for KeepsKeysOnly {
863 fn serving(&self) -> bool {
864 true
865 }
866 fn keep_refeed(&self, _: &crate::order::Refeed) -> Result<String, String> {
867 Ok("kept public_repositories".into())
868 }
869 }
870 let none = run(
871 &KeepsKeysOnly,
872 "main",
873 Order::Refeed(keys(&[
874 ("console_public_site", "on"),
875 ("public_repositories", "crates"),
876 ])),
877 );
878 assert!(
879 !none.ok
880 && none.detail.contains("keeps no settings")
881 && none
882 .detail
883 .contains("kept before it: kept public_repositories"),
884 "{none:?}"
885 );
886 }
887
888 #[test]
890 fn a_refeed_reaches_the_boxes_keep_and_a_box_that_keeps_none_refuses() {
891 let r = crate::order::Refeed {
892 keys: [
893 ("secret.smtp_url", "smtps://u:tok@relay.mock.invalid:465"),
894 ("secret.smtp_from", "holger@mock.invalid"),
895 ("public_repositories", "crates"),
896 ]
897 .iter()
898 .map(|(k, v)| (k.to_string(), v.to_string()))
899 .collect(),
900 };
901 let b = Fake::default();
902 let a = run(&b, "main", Order::Refeed(r.clone()));
903 assert!(
904 a.ok && a.detail.contains("public_repositories") && !a.detail.contains("tok@"),
905 "{a:?}"
906 );
907 assert_eq!(
908 b.did.borrow().as_slice(),
909 ["refeed public_repositories,secret.smtp_from,secret.smtp_url"]
910 );
911 struct Bare;
912 impl HolgerBox for Bare {
913 fn serving(&self) -> bool {
914 true
915 }
916 }
917 let none = run(&Bare, "main", Order::Refeed(r));
918 assert!(
919 !none.ok && none.detail.contains("keeps no estate keys"),
920 "{none:?}"
921 );
922 }
923
924 #[test]
925 fn restate_entitlements_verifies_the_snapshot_and_counts_new_and_held() {
926 let b = Fake::default();
927 run(&b, "main", Order::Entitlement(fact("a", 5, MONETIZE_SEED)));
928 let facts = vec![fact("a", 5, MONETIZE_SEED), fact("b", 5, MONETIZE_SEED)];
929 let mut snap = monetize_embed::Snapshot {
930 issued_unix_ms: 7,
931 facts,
932 signature: Vec::new(),
933 };
934 snap.signature = ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED)
935 .sign(&monetize_embed::signing::snapshot_message(
936 snap.issued_unix_ms,
937 &snap.facts,
938 ))
939 .to_bytes()
940 .to_vec();
941 let a = run(&b, "main", Order::RestateEntitlements(snap.clone()));
942 assert!(
943 a.ok && a.detail.contains("restated 1") && a.detail.contains("1 already held"),
944 "{a:?}"
945 );
946 snap.signature = vec![0; 64];
947 let bad = run(&b, "main", Order::RestateEntitlements(snap));
948 assert!(!bad.ok && bad.detail.contains("does not verify"), "{bad:?}");
949 }
950
951 #[test]
952 fn purge_tenant_answers_monetizes_purge_answer() {
953 let b = Fake::default();
954 let a = run(
955 &b,
956 "main",
957 Order::PurgeTenant(PurgeTenant {
958 tenant: "libs".into(),
959 reason: "nonpay".into(),
960 job: String::new(),
961 }),
962 );
963 assert!(a.ok, "{a:?}");
964 let answer: PurgeAnswer = serde_json::from_str(&a.detail).unwrap();
965 assert_eq!(answer.job, "purge:libs");
966 assert_eq!(b.did.borrow().as_slice(), ["purge libs"]);
967 }
968
969 #[test]
970 fn add_principal_makes_a_repository_on_a_plan_and_says_taken_by_name() {
971 let b = Fake::default();
972 let a = run(
973 &b,
974 "main",
975 Order::AddPrincipal(AddPrincipal {
976 username: "crates".into(),
977 kind: "org".into(),
978 signup: "s-1".into(),
979 }),
980 );
981 assert!(
982 a.ok && a.detail == "crates",
983 "the namespace made is the repository: {a:?}"
984 );
985 let t = run(
986 &b,
987 "main",
988 Order::AddPrincipal(AddPrincipal {
989 username: "theirs".into(),
990 kind: "org".into(),
991 signup: "s-2".into(),
992 }),
993 );
994 assert!(!t.ok && t.detail.starts_with("taken"), "{t:?}");
995 let twin = Fake {
996 twin: true,
997 ..Default::default()
998 };
999 assert!(
1000 !run(
1001 &twin,
1002 "twin",
1003 Order::AddPrincipal(AddPrincipal {
1004 username: "crates".into(),
1005 kind: "org".into(),
1006 signup: "s-1".into()
1007 })
1008 )
1009 .ok,
1010 "only the main that serves"
1011 );
1012 }
1013
1014 #[test]
1015 fn grow_room_is_kept_for_the_operator() {
1016 let b = Fake::default();
1017 let a = run(
1018 &b,
1019 "main",
1020 Order::GrowRoom(GrowRoom {
1021 need_bytes: 1 << 30,
1022 reason: "libs".into(),
1023 }),
1024 );
1025 assert!(a.ok, "{a:?}");
1026 assert_eq!(b.did.borrow().as_slice(), ["room 1073741824"]);
1027 }
1028
1029 #[test]
1030 fn grow_go_ahead_runs_twin_first_asks_monetize_and_remounts() {
1031 let b = Fake::default();
1032 let (m, k) = (me("main"), monetize_key());
1033 let mut ask = Asked(Vec::new(), true);
1034 let o = Order::GrowGoAhead(GrowGoAhead {
1035 go_ahead: growth("acme", 120, 99_000),
1036 data_gib: 120,
1037 });
1038 let a = execute(
1039 &b,
1040 &Ctx {
1041 me: &m,
1042 key: &k,
1043 now_unix_ms: 10_000,
1044 },
1045 6,
1046 &o,
1047 &mut ask,
1048 );
1049 assert!(a.ok, "{a:?}");
1050 assert_eq!(
1051 b.did.borrow().as_slice(),
1052 [
1053 "twin_holds 120",
1054 "start 120 pull-6-o-7",
1055 "settle false",
1056 "look_again false",
1057 "settle true"
1058 ]
1059 );
1060 assert_eq!(ask.0, [120]);
1061
1062 let b = Fake::default();
1064 let mut deaf = Asked(Vec::new(), false);
1065 b.settle.borrow_mut().extend([
1066 GrowState::WaitingMembers("w".into()),
1067 GrowState::Failed("mounted back".into()),
1068 ]);
1069 let a = execute(
1070 &b,
1071 &Ctx {
1072 me: &m,
1073 key: &k,
1074 now_unix_ms: 10_000,
1075 },
1076 7,
1077 &o,
1078 &mut deaf,
1079 );
1080 assert!(
1081 !a.ok && a.detail.contains("no answer") && a.detail.contains("mounted back"),
1082 "{a:?}"
1083 );
1084 assert!(b.did.borrow().contains(&"look_again true".to_string()));
1085 }
1086
1087 #[test]
1088 fn grow_go_ahead_refuses_another_estate_a_lapsed_step_and_a_forgery_and_skips_a_done_one() {
1089 let b = Fake::default();
1090 for (go, why) in [
1091 (growth("other", 120, 99_000), "estate other"),
1092 (growth("acme", 120, 5_000), "period is over"),
1093 (
1094 growth("acme", 120, 99_000).replace("\"o-7\"", "\"o-8\""),
1095 "does not verify",
1096 ),
1097 ] {
1098 let a = run(
1099 &b,
1100 "main",
1101 Order::GrowGoAhead(GrowGoAhead {
1102 go_ahead: go,
1103 data_gib: 120,
1104 }),
1105 );
1106 assert!(!a.ok && a.detail.contains(why), "{why}: {a:?}");
1107 }
1108 assert!(b.did.borrow().is_empty(), "nothing moved");
1109 let done = Fake {
1110 carried: true,
1111 ..Default::default()
1112 };
1113 let a = run(
1114 &done,
1115 "main",
1116 Order::GrowGoAhead(GrowGoAhead {
1117 go_ahead: growth("acme", 120, 99_000),
1118 data_gib: 120,
1119 }),
1120 );
1121 assert!(a.ok && a.detail.contains("nothing moved"), "{a:?}");
1122 let twin = Fake {
1123 twin: true,
1124 ..Default::default()
1125 };
1126 let a = run(
1127 &twin,
1128 "twin",
1129 Order::GrowGoAhead(GrowGoAhead {
1130 go_ahead: growth("acme", 120, 99_000),
1131 data_gib: 240,
1132 }),
1133 );
1134 assert!(a.detail.contains("no pull channel"), "{a:?}");
1135 assert!(
1136 !twin
1137 .did
1138 .borrow()
1139 .iter()
1140 .any(|d| d.starts_with("twin_holds")),
1141 "the twin grows first, asking no twin: {a:?}"
1142 );
1143 }
1144
1145 #[test]
1146 fn recharge_from_twin_runs_the_lane_on_a_cloud_fence_proof() {
1147 let b = Fake::default();
1148 let r = RechargeAsk {
1149 main: "main:1".into(),
1150 twin: "twin:1".into(),
1151 accept_lost: true,
1152 fenced: Some("upcloud: gone".into()),
1153 ..Default::default()
1154 };
1155 let a = run(&b, "twin", Order::RechargeFromTwin(r));
1156 assert!(a.ok && a.detail.starts_with("cloud-fenced"), "{a:?}");
1157 assert_eq!(
1158 b.did.borrow().as_slice(),
1159 ["recharge main:1 from twin:1 lost=true"]
1160 );
1161 }
1162
1163 #[test]
1168 fn recharge_from_twin_fences_the_old_primary_first() {
1169 let b = Fake::default();
1170 let r = RechargeAsk {
1171 main: "main:2".into(),
1172 twin: "twin:1".into(),
1173 accept_lost: false,
1174 epoch: 4,
1175 fence: Some("old:1".into()),
1176 ..Default::default()
1177 };
1178 let a = run(&b, "twin", Order::RechargeFromTwin(r.clone()));
1179 assert!(a.ok && a.detail.starts_with("fenced old:1; "), "{a:?}");
1180 assert_eq!(
1181 b.did.borrow().as_slice(),
1182 ["fence old:1 4", "recharge main:2 from twin:1 lost=false"]
1183 );
1184
1185 let stuck = Fake::default();
1186 let a = run(
1187 &stuck,
1188 "twin",
1189 Order::RechargeFromTwin(RechargeAsk {
1190 fence: Some("stuck:1".into()),
1191 ..r.clone()
1192 }),
1193 );
1194 assert!(
1195 !a.ok
1196 && a.detail.contains("stuck:1 answers nothing")
1197 && a.detail.contains("nothing was recharged"),
1198 "{a:?}"
1199 );
1200 assert_eq!(
1201 stuck.did.borrow().as_slice(),
1202 ["fence stuck:1 4"],
1203 "no recharge beside an unfenced primary"
1204 );
1205
1206 let same = Fake::default();
1207 let a = run(
1208 &same,
1209 "twin",
1210 Order::RechargeFromTwin(RechargeAsk {
1211 fence: Some("main:2".into()),
1212 ..r
1213 }),
1214 );
1215 assert!(
1216 !a.ok && a.detail.contains("never fences the box it fills"),
1217 "{a:?}"
1218 );
1219 assert!(same.did.borrow().is_empty(), "nothing ran");
1220 }
1221
1222 #[test]
1223 fn become_twin_runs_holgers_election() {
1224 let b = Fake {
1225 twin: true,
1226 ..Default::default()
1227 };
1228 assert!(
1229 run(
1230 &b,
1231 "twin",
1232 Order::BecomeTwin {
1233 primary: "main:1".into()
1234 }
1235 )
1236 .ok
1237 );
1238 assert_eq!(b.did.borrow().as_slice(), ["become_twin main:1"]);
1239 struct Bare;
1240 impl HolgerBox for Bare {
1241 fn serving(&self) -> bool {
1242 false
1243 }
1244 }
1245 let none = run(
1246 &Bare,
1247 "twin",
1248 Order::BecomeTwin {
1249 primary: "main:1".into(),
1250 },
1251 );
1252 assert!(
1253 !none.ok && none.detail.contains("cannot become"),
1254 "the default refuses by name: {none:?}"
1255 );
1256 }
1257
1258 #[test]
1259 fn fence_primary_runs_the_boxes_fence_adopt_and_review_run() {
1260 let b = Fake::default();
1261 let a = run(
1262 &b,
1263 "twin",
1264 Order::FencePrimary {
1265 primary: "old:1".into(),
1266 epoch: 3,
1267 },
1268 );
1269 assert!(
1270 a.ok && b.did.borrow().contains(&"fence old:1 3".to_string()),
1271 "{a:?}"
1272 );
1273 struct Bare;
1274 impl HolgerBox for Bare {
1275 fn serving(&self) -> bool {
1276 false
1277 }
1278 }
1279 let none = run(
1280 &Bare,
1281 "twin",
1282 Order::FencePrimary {
1283 primary: "old:1".into(),
1284 epoch: 3,
1285 },
1286 );
1287 assert!(
1288 !none.ok && none.detail.contains("cannot fence"),
1289 "the default refuses by name: {none:?}"
1290 );
1291 let a = run(
1292 &b,
1293 "main",
1294 Order::AdoptTwin {
1295 twin: "twin:1".into(),
1296 principal: "twin".into(),
1297 },
1298 );
1299 assert!(
1300 a.ok && b.did.borrow().contains(&"adopt twin:1 twin".to_string()),
1301 "{a:?}"
1302 );
1303 let a = run(&b, "twin", Order::ReviewTwin);
1304 assert!(
1305 a.ok && a.detail == "the twin is whole" && a.receipt_json.contains("complete"),
1306 "{a:?}"
1307 );
1308 }
1309
1310 #[test]
1313 fn a_product_order_runs_with_its_params_and_its_pulled_file() {
1314 use monetize_embed::fleet::{Fetch, ProductOrder};
1315 struct Shop(Result<Vec<u8>, String>);
1316 impl GrowSet for Shop {
1317 fn grow_set(&mut self, _: u64) -> Result<String, String> {
1318 Err("no".into())
1319 }
1320 fn fetch(&mut self, _: &Fetch) -> Result<Vec<u8>, String> {
1321 self.0.clone()
1322 }
1323 }
1324 let (m, k) = (me("main"), monetize_key());
1325 let ctx = Ctx {
1326 me: &m,
1327 key: &k,
1328 now_unix_ms: 10_000,
1329 };
1330 let b = Fake::default();
1331 let params: BTreeMap<String, String> = [("name".to_string(), "lab-1".to_string())].into();
1332 let enrol = Order::Product {
1333 kind: "downstream_enrol".into(),
1334 body: ProductOrder {
1335 params: params.clone(),
1336 fetch: None,
1337 },
1338 };
1339 let a = execute(&b, &ctx, 7, &enrol, &mut NoAsk);
1340 assert!(a.ok && a.detail.contains("downstream_enrol"), "{a:?}");
1341 let put = Order::Product {
1342 kind: "bundle_put".into(),
1343 body: ProductOrder {
1344 params,
1345 fetch: Some(Fetch {
1346 id: "ab".repeat(32),
1347 bytes: 3,
1348 param: "bundle".into(),
1349 }),
1350 },
1351 };
1352 assert!(execute(&b, &ctx, 8, &put, &mut Shop(Ok(b"abc".to_vec()))).ok);
1353 let failed = execute(&b, &ctx, 9, &put, &mut Shop(Err("gone".into())));
1354 assert!(
1355 !failed.ok && failed.detail.contains("was not pulled"),
1356 "{failed:?}"
1357 );
1358 assert_eq!(
1359 b.did.borrow().len(),
1360 2,
1361 "a file not pulled runs nothing: {:?}",
1362 b.did.borrow()
1363 );
1364 assert!(b.did.borrow()[1].contains("Some(3)"));
1365 assert!(
1366 b.did.borrow()[1].ends_with("by 8"),
1367 "the box is told the verified order it runs for: {:?}",
1368 b.did.borrow()
1369 );
1370 let twin = Fake {
1371 twin: true,
1372 ..Fake::default()
1373 };
1374 assert!(
1375 !execute(&twin, &ctx, 10, &enrol, &mut NoAsk).ok,
1376 "a twin runs none"
1377 );
1378 }
1379
1380 #[test]
1383 fn holgers_own_kinds_decode_and_a_bundle_needs_its_file() {
1384 use crate::order::{decode, holger_kind};
1385 for k in holger_kind::PRODUCT {
1386 if k == holger_kind::BUNDLE_PUT {
1387 continue;
1388 }
1389 assert!(
1390 matches!(
1391 decode(1, k, r#"{"params":{"name":"x"}}"#),
1392 Ok(Order::Product { .. })
1393 ),
1394 "{k}"
1395 );
1396 }
1397 assert!(
1398 decode(1, holger_kind::BUNDLE_PUT, r#"{"params":{"path":"b"}}"#)
1399 .unwrap_err()
1400 .contains("no file")
1401 );
1402 assert!(decode(1, holger_kind::DOWNSTREAM_ENROL, r#"[1]"#)
1403 .unwrap_err()
1404 .contains("does not decode"));
1405 assert!(decode(1, "downstream_destroy", "{}")
1406 .unwrap_err()
1407 .contains("does not know"));
1408 }
1409}