use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use monetize_embed::fleet::{
ask, call, kind, order_message, Ask, FleetCall, OrderAck, OrdersAfter, Report, SignedOrder,
LONG_POLL_MAX_MS,
};
use monetize_embed::VerifyingKey;
use serde::{Deserialize, Serialize};
use super::key::BoxKey;
pub const STATE_REL: &str = "fleet/pulled.json";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Addressee {
pub product: String,
pub instance: String,
pub role: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Fault {
pub code: String,
pub message: String,
}
impl Fault {
pub fn new(code: impl Into<String>, message: impl Into<String>) -> Fault {
Fault {
code: code.into(),
message: message.into(),
}
}
}
impl std::fmt::Display for Fault {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.code, self.message)
}
}
#[derive(Debug, Clone)]
pub struct Signed {
pub call: FleetCall,
pub signature: Vec<u8>,
pub box_key: Vec<u8>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Asked {
pub case_id: String,
pub words: String,
pub url: String,
}
pub trait Link {
fn report(&mut self, s: Signed) -> Result<u64, Fault>;
fn orders(&mut self, s: Signed) -> Result<Vec<SignedOrder>, Fault>;
fn ask(&mut self, s: Signed) -> Result<Asked, Fault>;
fn ask_bytes(&mut self, s: Signed) -> Result<(Asked, Vec<u8>), Fault> {
self.ask(s).map(|a| (a, Vec::new()))
}
}
pub trait Asker {
fn ask(&mut self, a: &Ask) -> Result<Asked, Fault>;
fn ask_bytes(&mut self, a: &Ask) -> Result<(Asked, Vec<u8>), Fault> {
self.ask(a).map(|x| (x, Vec::new()))
}
}
pub fn grow_set(asker: &mut dyn Asker, gib: u64) -> Result<String, String> {
let a = Ask {
kind: ask::GROW_SET.into(),
body: serde_json::json!({ "members": [], "gib": gib }),
};
let Asked {
case_id: case,
words,
..
} = asker.ask(&a).map_err(|f| f.to_string())?;
Ok(if words.is_empty() { case } else { words })
}
pub fn fetch(asker: &mut dyn Asker, f: &monetize_embed::fleet::Fetch) -> Result<Vec<u8>, String> {
let a = Ask {
kind: ask::FETCH.into(),
body: serde_json::json!({ "id": f.id }),
};
let (_, bytes) = asker.ask_bytes(&a).map_err(|e| e.to_string())?;
if bytes.len() as u64 != f.bytes {
return Err(format!(
"the file pulled for {} is {} bytes, and the order names {}",
f.param,
bytes.len(),
f.bytes
));
}
Ok(bytes)
}
pub struct Ctx<'a> {
pub me: &'a Addressee,
pub key: &'a VerifyingKey,
pub now_unix_ms: u64,
}
pub trait PumpBox {
fn health(&self) -> BTreeMap<String, String> {
BTreeMap::new()
}
fn product_report(&self) -> Option<String> {
None
}
fn own_review(&self) -> Option<String> {
None
}
fn run(&self, ctx: &Ctx, order: &SignedOrder, ask: &mut dyn Asker) -> OrderAck;
}
pub use monetize_embed::fleet::FUTURE_SKEW_MS;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Checked {
Forged(String),
Refused(String),
Fresh,
}
pub fn check(
o: &SignedOrder,
me: &Addressee,
key: &VerifyingKey,
last_seq: u64,
now_unix_ms: u64,
) -> Checked {
let signed = ed25519_dalek::Signature::from_slice(&o.signature)
.map(|s| key.verify_strict(&order_message(o), &s).is_ok())
.unwrap_or(false);
if !signed {
return Checked::Forged(format!(
"order {} ({}) is not signed by this product's monetize key",
o.seq, o.kind
));
}
if o.product != me.product || o.instance != me.instance || o.role != me.role {
return Checked::Refused(format!(
"order {} is for {}/{}/{}, and this box is {}/{}/{}",
o.seq, o.product, o.instance, o.role, me.product, me.instance, me.role
));
}
if o.seq <= last_seq {
return Checked::Refused(format!(
"order {} is not newer than {last_seq}, the last one this box took: a replay",
o.seq
));
}
if o.not_after_unix_ms != 0 && now_unix_ms > o.not_after_unix_ms {
return Checked::Refused(format!(
"order {} expired {} s ago",
o.seq,
(now_unix_ms - o.not_after_unix_ms) / 1000
));
}
if o.issued_unix_ms > now_unix_ms.saturating_add(FUTURE_SKEW_MS) {
return Checked::Refused(format!(
"order {} is dated {} s in the future; whose clock is wrong?",
o.seq,
(o.issued_unix_ms - now_unix_ms) / 1000
));
}
Checked::Fresh
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Pulled {
pub last_seq: u64,
pub pending: Vec<OrderAck>,
pub twin_review: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Pull {
pub acks: Vec<OrderAck>,
pub forged: Vec<String>,
}
pub struct Puller {
pub me: Addressee,
pub build: String,
key: BoxKey,
state: Option<PathBuf>,
pub pulled: Pulled,
heard: bool,
}
impl Puller {
pub fn new(me: Addressee, key: BoxKey, build: impl Into<String>) -> Puller {
Puller {
me,
build: build.into(),
key,
state: None,
pulled: Pulled::default(),
heard: false,
}
}
pub fn at(root: &Path, me: Addressee, build: impl Into<String>) -> std::io::Result<Puller> {
let key = BoxKey::load_or_mint(root)?;
let state = root.join(STATE_REL);
let pulled = match std::fs::read(&state) {
Ok(bytes) => serde_json::from_slice(&bytes).map_err(|e| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
format!("{}: {e}", state.display()),
)
})?,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Pulled::default(),
Err(e) => return Err(e),
};
Ok(Puller {
me,
build: build.into(),
key,
state: Some(state),
pulled,
heard: false,
})
}
pub fn box_public(&self) -> [u8; 32] {
self.key.public()
}
fn save(&self) -> Result<(), Fault> {
let Some(path) = &self.state else {
return Ok(());
};
let bytes =
serde_json::to_vec(&self.pulled).map_err(|e| Fault::new("internal", e.to_string()))?;
crate::write_atomic(path, &bytes, false)
.map_err(|e| Fault::new("io", format!("{}: {e}", path.display())))
}
fn signed(&self, which: &str, body: String, now_unix_ms: u64) -> Signed {
let c = FleetCall {
product: self.me.product.clone(),
instance: self.me.instance.clone(),
role: self.me.role.clone(),
call: which.to_owned(),
body,
at_unix_ms: now_unix_ms,
};
let signature = self.key.sign(&c);
Signed {
call: c,
signature,
box_key: if self.heard {
Vec::new()
} else {
self.key.public().to_vec()
},
}
}
pub fn report(
&mut self,
link: &mut dyn Link,
health: BTreeMap<String, String>,
product: Option<String>,
own_review: Option<String>,
now_unix_ms: u64,
) -> Result<u64, Fault> {
let sent = self.pulled.pending.len();
let report = Report {
build: self.build.clone(),
health,
acked: self.pulled.pending.clone(),
twin_review: own_review.or_else(|| self.pulled.twin_review.clone()),
product,
measured_unix_ms: now_unix_ms,
};
let body =
serde_json::to_string(&report).map_err(|e| Fault::new("internal", e.to_string()))?;
let next = link.report(self.signed(call::REPORT, body, now_unix_ms))?;
self.heard = true;
self.pulled.pending.drain(..sent);
self.pulled.twin_review = None;
self.save()?;
Ok(next)
}
pub fn pull(
&mut self,
link: &mut dyn Link,
b: &dyn PumpBox,
monetize_pubkey: Option<&[u8; 32]>,
wait_ms: u64,
now_unix_ms: u64,
) -> Result<Pull, Fault> {
let Some(raw) = monetize_pubkey else {
return Err(Fault::new(
"no_key",
"this box has no monetize key yet, so no order could be verified: none is pulled",
));
};
let key = VerifyingKey::from_bytes(raw).map_err(|e| {
Fault::new(
"no_key",
format!("the monetize key is not an Ed25519 point: {e}"),
)
})?;
let body = serde_json::to_string(&OrdersAfter {
after: self.pulled.last_seq,
wait_ms: wait_ms.min(LONG_POLL_MAX_MS),
})
.map_err(|e| Fault::new("internal", e.to_string()))?;
let mut orders = link.orders(self.signed(call::ORDERS, body, now_unix_ms))?;
self.heard = true;
orders.sort_by_key(|o| o.seq);
let mut out = Pull::default();
for o in orders {
let checked = check(&o, &self.me, &key, self.pulled.last_seq, now_unix_ms);
if let Checked::Forged(why) = checked {
out.forged.push(why);
continue;
}
if o.seq <= self.pulled.last_seq {
continue;
}
self.pulled.last_seq = o.seq;
self.save()?;
let ack = match checked {
Checked::Fresh => {
let me = self.me.clone();
let ctx = Ctx {
me: &me,
key: &key,
now_unix_ms,
};
let ack = {
let mut via = AskVia {
puller: &mut *self,
link: &mut *link,
now_unix_ms,
};
b.run(&ctx, &o, &mut via)
};
if o.kind == kind::REVIEW_TWIN && ack.ok {
self.pulled.twin_review = Some(ack.receipt_json.clone());
}
ack
}
Checked::Refused(detail) => OrderAck {
seq: o.seq,
ok: false,
detail,
receipt_json: String::new(),
},
Checked::Forged(_) => unreachable!("forged orders were skipped"),
};
self.pulled.pending.push(ack.clone());
self.save()?;
out.acks.push(ack);
}
Ok(out)
}
pub fn ask(&mut self, link: &mut dyn Link, a: &Ask, now_unix_ms: u64) -> Result<Asked, Fault> {
let body = serde_json::to_string(a).map_err(|e| Fault::new("internal", e.to_string()))?;
let answer = link.ask(self.signed(call::ASK, body, now_unix_ms))?;
self.heard = true;
Ok(answer)
}
pub fn ask_bytes(
&mut self,
link: &mut dyn Link,
a: &Ask,
now_unix_ms: u64,
) -> Result<(Asked, Vec<u8>), Fault> {
let body = serde_json::to_string(a).map_err(|e| Fault::new("internal", e.to_string()))?;
let answer = link.ask_bytes(self.signed(call::ASK, body, now_unix_ms))?;
self.heard = true;
Ok(answer)
}
}
struct AskVia<'a> {
puller: &'a mut Puller,
link: &'a mut dyn Link,
now_unix_ms: u64,
}
impl Asker for AskVia<'_> {
fn ask(&mut self, a: &Ask) -> Result<Asked, Fault> {
self.puller.ask(self.link, a, self.now_unix_ms)
}
fn ask_bytes(&mut self, a: &Ask) -> Result<(Asked, Vec<u8>), Fault> {
self.puller.ask_bytes(self.link, a, self.now_unix_ms)
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
use ed25519_dalek::Signer as _;
use std::cell::RefCell;
pub const MONETIZE_SEED: [u8; 32] = [7u8; 32];
pub fn me(role: &str) -> Addressee {
Addressee {
product: "a-product".into(),
instance: "acme".into(),
role: role.into(),
}
}
pub fn monetize_key() -> VerifyingKey {
ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED).verifying_key()
}
fn mkey() -> [u8; 32] {
monetize_key().to_bytes()
}
pub fn signed(seq: u64, role: &str, kind: &str, body: &str, seed: [u8; 32]) -> SignedOrder {
let mut o = SignedOrder {
seq,
product: "a-product".into(),
instance: "acme".into(),
role: role.into(),
kind: kind.into(),
body: body.into(),
issued_unix_ms: 9_000,
not_after_unix_ms: 0,
signature: Vec::new(),
};
o.signature = ed25519_dalek::SigningKey::from_bytes(&seed)
.sign(&order_message(&o))
.to_bytes()
.to_vec();
o
}
#[derive(Default)]
pub struct Guardian {
pub queue: Vec<SignedOrder>,
pub heard: Vec<Signed>,
}
impl Link for Guardian {
fn report(&mut self, s: Signed) -> Result<u64, Fault> {
let r: Report = serde_json::from_str(&s.call.body).unwrap();
self.queue
.retain(|o| !r.acked.iter().any(|a| a.seq == o.seq));
self.heard.push(s);
Ok(30_000)
}
fn orders(&mut self, s: Signed) -> Result<Vec<SignedOrder>, Fault> {
let after: OrdersAfter = serde_json::from_str(&s.call.body).unwrap();
self.heard.push(s);
Ok(self
.queue
.iter()
.filter(|o| o.seq > after.after)
.cloned()
.collect())
}
fn ask(&mut self, s: Signed) -> Result<Asked, Fault> {
let _: Ask = serde_json::from_str(&s.call.body).unwrap();
self.heard.push(s);
Ok(Asked {
case_id: "c-1".into(),
words: "resized".into(),
url: String::new(),
})
}
}
#[derive(Default)]
pub struct Fake {
pub ran: RefCell<Vec<(u64, String)>>,
}
impl PumpBox for Fake {
fn run(&self, ctx: &Ctx, o: &SignedOrder, ask: &mut dyn Asker) -> OrderAck {
self.ran.borrow_mut().push((o.seq, o.kind.clone()));
let said = match o.kind.as_str() {
"note" => Ok(format!("noted for {}", ctx.me.role)),
"grow" => grow_set(ask, 64),
kind::REVIEW_TWIN => {
return OrderAck {
seq: o.seq,
ok: true,
detail: "reviewed".into(),
receipt_json: r#"{"complete":"yes"}"#.into(),
}
}
other => Err(format!(
"order {}: this product does not know the order kind {other:?}",
o.seq
)),
};
match said {
Ok(detail) => OrderAck {
seq: o.seq,
ok: true,
detail,
receipt_json: String::new(),
},
Err(detail) => OrderAck {
seq: o.seq,
ok: false,
detail,
receipt_json: String::new(),
},
}
}
}
#[test]
fn an_order_is_pulled_run_and_acked_in_the_next_report_and_never_twice() {
let b = Fake::default();
let mut g = Guardian::default();
g.queue.push(signed(4, "main", "note", "{}", MONETIZE_SEED));
let mut p = Puller::new(me("main"), BoxKey::from_seed([3u8; 32]), "test");
let pulled = p.pull(&mut g, &b, Some(&mkey()), 0, 10_000).unwrap();
assert_eq!(pulled.acks.len(), 1);
assert!(pulled.acks[0].ok, "{pulled:?}");
assert_eq!(pulled.acks[0].detail, "noted for main");
p.report(
&mut g,
BTreeMap::from([("serving".into(), "yes".into())]),
Some("{}".into()),
None,
10_001,
)
.unwrap();
let r: Report = serde_json::from_str(&g.heard.last().unwrap().call.body).unwrap();
assert_eq!(
r.acked.iter().map(|a| (a.seq, a.ok)).collect::<Vec<_>>(),
vec![(4, true)]
);
assert_eq!(
(r.product.as_deref(), r.build.as_str()),
(Some("{}"), "test")
);
assert!(g.queue.is_empty() && p.pulled.pending.is_empty());
assert!(
p.pull(&mut g, &b, Some(&mkey()), 0, 10_002)
.unwrap()
.acks
.is_empty(),
"nothing runs twice"
);
assert_eq!(b.ran.borrow().len(), 1);
}
#[test]
fn a_weak_key_signs_no_order_and_a_real_one_still_does() {
let mut identity = [0u8; 32];
identity[0] = 1;
let weak = VerifyingKey::from_bytes(&identity).expect("the identity point decompresses");
let mut o = signed(3, "main", "recharge_from_twin", "{}", MONETIZE_SEED);
o.signature = {
let mut s = vec![0u8; 64];
s[0] = 1;
s
};
assert!(
matches!(check(&o, &me("main"), &weak, 0, 9_500), Checked::Forged(_)),
"a weak key verifies nothing"
);
let real = signed(3, "main", "recharge_from_twin", "{}", MONETIZE_SEED);
assert_eq!(
check(&real, &me("main"), &monetize_key(), 0, 9_500),
Checked::Fresh
);
}
#[test]
fn an_order_dated_ahead_of_the_box_clock_is_refused_by_name() {
let mut ahead = signed(3, "main", "note", "{}", MONETIZE_SEED);
ahead.issued_unix_ms = 9_000 + FUTURE_SKEW_MS + 1;
ahead.signature = ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED)
.sign(&order_message(&ahead))
.to_bytes()
.to_vec();
match check(&ahead, &me("main"), &monetize_key(), 0, 9_000) {
Checked::Refused(why) => assert!(why.contains("in the future"), "{why}"),
other => panic!("{other:?}"),
}
let mut skew = ahead.clone();
skew.issued_unix_ms = 9_000 + FUTURE_SKEW_MS;
skew.signature = ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED)
.sign(&order_message(&skew))
.to_bytes()
.to_vec();
assert_eq!(
check(&skew, &me("main"), &monetize_key(), 0, 9_000),
Checked::Fresh
);
}
#[test]
fn a_forged_order_never_runs_never_acks_and_never_moves_the_sequence() {
let b = Fake::default();
let mut g = Guardian::default();
g.queue
.push(signed(u64::MAX, "main", "note", "{}", [9u8; 32]));
let mut p = Puller::new(me("main"), BoxKey::from_seed([3u8; 32]), "test");
let pulled = p.pull(&mut g, &b, Some(&mkey()), 0, 10_000).unwrap();
assert!(pulled.acks.is_empty() && pulled.forged.len() == 1);
assert_eq!(p.pulled.last_seq, 0);
assert!(b.ran.borrow().is_empty());
}
#[test]
fn another_boxs_an_expired_or_an_unknown_order_is_refused_by_name() {
let k = monetize_key();
let twin = signed(5, "twin", "note", "{}", MONETIZE_SEED);
assert!(
matches!(check(&twin, &me("main"), &k, 0, 10_000), Checked::Refused(w) if w.contains("for a-product/acme/twin"))
);
let mut late = signed(5, "main", "note", "{}", MONETIZE_SEED);
late.not_after_unix_ms = 1_000;
let late = {
let mut l = late.clone();
l.signature = signed_like(&l);
l
};
assert!(
matches!(check(&late, &me("main"), &k, 0, 61_000), Checked::Refused(w) if w.contains("expired 60 s ago"))
);
assert!(
matches!(check(&signed(5, "main", "note", "{}", MONETIZE_SEED), &me("main"), &k, 5, 1), Checked::Refused(w) if w.contains("a replay"))
);
assert_eq!(
check(
&signed(6, "main", "note", "{}", MONETIZE_SEED),
&me("main"),
&k,
5,
1
),
Checked::Fresh
);
let b = Fake::default();
let mut g = Guardian::default();
g.queue.push(twin);
g.queue
.push(signed(7, "main", "reboot_the_moon", "{}", MONETIZE_SEED));
let mut p = Puller::new(me("main"), BoxKey::from_seed([3u8; 32]), "test");
let pulled = p.pull(&mut g, &b, Some(&mkey()), 0, 10_000).unwrap();
assert_eq!(
pulled
.acks
.iter()
.map(|a| (a.seq, a.ok))
.collect::<Vec<_>>(),
vec![(5, false), (7, false)]
);
assert!(
pulled.acks[1]
.detail
.contains("does not know the order kind \"reboot_the_moon\""),
"{pulled:?}"
);
assert_eq!(
*b.ran.borrow(),
vec![(7, "reboot_the_moon".to_string())],
"only the box's own order reaches the product"
);
}
fn signed_like(o: &SignedOrder) -> Vec<u8> {
ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED)
.sign(&order_message(o))
.to_bytes()
.to_vec()
}
#[test]
fn without_a_monetize_key_nothing_is_pulled_and_monetize_is_not_even_asked() {
let mut g = Guardian::default();
let mut p = Puller::new(me("main"), BoxKey::from_seed([3u8; 32]), "test");
assert_eq!(
p.pull(&mut g, &Fake::default(), None, 0, 10_000)
.unwrap_err()
.code,
"no_key"
);
assert!(g.heard.is_empty());
}
#[test]
fn the_first_call_carries_the_box_key_and_later_ones_do_not() {
let mut g = Guardian::default();
let mut p = Puller::new(me("main"), BoxKey::from_seed([3u8; 32]), "test");
p.report(&mut g, BTreeMap::new(), None, None, 1).unwrap();
p.report(&mut g, BTreeMap::new(), None, None, 2).unwrap();
assert_eq!(g.heard[0].box_key, p.box_public().to_vec());
assert!(g.heard[1].box_key.is_empty());
}
#[test]
fn a_twins_own_review_rides_every_report_and_an_ordered_one_the_next_only() {
let mut g = Guardian::default();
let mut p = Puller::new(me("twin"), BoxKey::from_seed([4u8; 32]), "test");
g.queue
.push(signed(1, "twin", "review_twin", "{}", MONETIZE_SEED));
p.pull(&mut g, &Fake::default(), Some(&mkey()), 0, 10)
.unwrap();
p.report(&mut g, BTreeMap::new(), None, None, 11).unwrap();
p.report(&mut g, BTreeMap::new(), None, None, 12).unwrap();
p.report(
&mut g,
BTreeMap::new(),
None,
Some(r#"{"own":1}"#.into()),
13,
)
.unwrap();
let said: Vec<Option<String>> = g
.heard
.iter()
.filter(|s| s.call.call == "report")
.map(|s| {
serde_json::from_str::<Report>(&s.call.body)
.unwrap()
.twin_review
})
.collect();
assert!(
said[0].as_deref().is_some_and(|r| r.contains("complete")),
"{said:?}"
);
assert_eq!(said[1], None);
assert_eq!(said[2].as_deref(), Some(r#"{"own":1}"#));
}
#[test]
fn an_order_asks_monetize_on_its_own_link_signed_by_the_box() {
let mut g = Guardian::default();
g.queue.push(signed(2, "main", "grow", "{}", MONETIZE_SEED));
let mut p = Puller::new(me("main"), BoxKey::from_seed([3u8; 32]), "test");
let pulled = p
.pull(&mut g, &Fake::default(), Some(&mkey()), 0, 10_000)
.unwrap();
assert_eq!(pulled.acks[0].detail, "resized");
let asked = g
.heard
.iter()
.find(|s| s.call.call == "ask")
.expect("asked");
let a: Ask = serde_json::from_str(&asked.call.body).unwrap();
assert_eq!(
(a.kind.as_str(), a.body["gib"].as_u64()),
("grow_set", Some(64))
);
let vk = VerifyingKey::from_bytes(&p.box_public()).unwrap();
monetize_embed::fleet::verify_fleet_call(&asked.call, &asked.signature, &vk, 10_000)
.expect("the ask is the box's");
}
#[test]
fn the_puller_remembers_what_it_took_across_a_restart() {
let dir = tempfile::tempdir().unwrap();
let mut g = Guardian::default();
g.queue.push(signed(6, "main", "note", "{}", MONETIZE_SEED));
let mut p = Puller::at(dir.path(), me("main"), "test").unwrap();
let key = p.box_public();
p.pull(&mut g, &Fake::default(), Some(&mkey()), 0, 10_000)
.unwrap();
drop(p);
let p = Puller::at(dir.path(), me("main"), "test").unwrap();
assert_eq!(
(p.box_public(), p.pulled.last_seq, p.pulled.pending.len()),
(key, 6, 1)
);
}
#[test]
fn a_product_orders_file_is_pulled_by_a_fetch_ask_and_checked() {
struct Shop(Vec<u8>, Vec<Ask>);
impl Asker for Shop {
fn ask(&mut self, _: &Ask) -> Result<Asked, Fault> {
unreachable!("a fetch asks for its bytes")
}
fn ask_bytes(&mut self, a: &Ask) -> Result<(Asked, Vec<u8>), Fault> {
self.1.push(a.clone());
Ok((Asked::default(), self.0.clone()))
}
}
let f = monetize_embed::fleet::Fetch {
id: "ab".repeat(32),
bytes: 3,
param: "bundle".into(),
};
let mut shop = Shop(b"abc".to_vec(), Vec::new());
assert_eq!(fetch(&mut shop, &f).unwrap(), b"abc");
assert_eq!(
(shop.1[0].kind.as_str(), shop.1[0].body["id"].as_str()),
(ask::FETCH, Some(f.id.as_str()))
);
let mut short = Shop(b"ab".to_vec(), Vec::new());
assert!(fetch(&mut short, &f).unwrap_err().contains("names 3"));
}
}