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