crates/monetize-server/tests/plugins_from_the_release_bucket.rs
//! **The ONE box's plugins, updated from the release bucket** (owner 2026-10-17) — against
//! the REAL toy guests (`cargo xtask build-guests`) and a bucket held in memory:
//!
//! * the release's list is verified (its own `.p7s`) and each plugin's bytes against the
//! list's digests and its own `.p7s` against its block's roots BEFORE a byte is written;
//! * a look writes nothing; an update writes atomically where the block names the module,
//! and the guest watcher swaps it in live; a second update says `unchanged`;
//! * a list nobody trusted signed, bytes that are not the list's, a module signed by a
//! stranger, a guest no block runs: refused by name, nothing written;
//! * the work is a QUEUED jera job: `submit` answers at once, `status` reads it to its end.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex};
use guest_host::verify::sign::{self, dev, Ed25519Signer};
use monetize_server::config::Config;
use monetize_server::guests::look;
use monetize_server::plugins::{self, Fetch, LIST_KEY};
use monetize_server::registry::Registry;
use monetize_server::state::AppState;
fn built(stem: &str) -> PathBuf {
let target = std::env::var("CARGO_TARGET_DIR").map(PathBuf::from).unwrap_or_else(|_| Path::new(env!("CARGO_MANIFEST_DIR")).join("../../target"));
let p = target.join(format!("wasm32-unknown-unknown/release/{stem}.wasm"));
assert!(p.is_file(), "{} has not been built. This test loads the REAL guest; it does not skip.\nBuild it with:\n cargo xtask build-guests", p.display());
p
}
fn scratch(name: &str) -> PathBuf {
let p = std::env::temp_dir().join(format!("monetize-plugins-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&p);
std::fs::create_dir_all(&p).unwrap();
p
}
/// A guest's bytes with a custom section appended: the same guest, other bytes.
fn guest_bytes(stem: &str, tag: &str) -> Vec<u8> {
let mut bytes = std::fs::read(built(stem)).unwrap();
if !tag.is_empty() {
let mut content = vec![tag.len() as u8];
content.extend_from_slice(tag.as_bytes());
bytes.push(0);
bytes.push(content.len() as u8);
bytes.extend_from_slice(&content);
}
bytes
}
fn sha256(b: &[u8]) -> String {
use sha2::Digest as _;
sha2::Sha256::digest(b).iter().map(|x| format!("{x:02x}")).collect()
}
/// The release bucket, in memory.
#[derive(Default, Clone)]
struct MapBucket(Arc<Mutex<BTreeMap<String, Vec<u8>>>>);
impl Fetch for MapBucket {
fn get(&self, object: &str, cap: usize) -> Result<Vec<u8>, String> {
let b = self.0.lock().unwrap().get(object).cloned().ok_or_else(|| format!("the bucket answered 404 Not Found for {object}"))?;
if b.len() > cap {
return Err(format!("larger than {cap} bytes"));
}
Ok(b)
}
}
impl MapBucket {
fn put(&self, k: &str, v: Vec<u8>) {
self.0.lock().unwrap().insert(k.to_string(), v);
}
}
struct Release {
rows: Vec<(String, &'static str, Vec<u8>)>,
}
impl Release {
/// Publish it as the release build does: each module and its `.p7s` under its blake3,
/// then the list and its `.p7s` (signed by `list_by`).
fn publish(&self, bucket: &MapBucket, module_by: &Ed25519Signer, list_by: &Ed25519Signer) {
self.publish_at(bucket, LIST_KEY, module_by, list_by)
}
/// [`Release::publish`] with its list at `list_key` (a box's own, OXG-5).
fn publish_at(&self, bucket: &MapBucket, list_key: &str, module_by: &Ed25519Signer, list_by: &Ed25519Signer) {
let mut list = String::from("# ONE release: every artefact by sha256 and blake3.\n");
for (name, role, bytes) in &self.rows {
let b3 = guest_host::digest_hex(bytes);
let key = plugins::object_key(&b3, name);
bucket.put(&key, bytes.clone());
bucket.put(&format!("{key}.p7s"), sign::sign_module(bytes, module_by).unwrap());
list.push_str(&format!("\n[[artefact]]\nname = {name:?}\nrole = {role:?}\nsha256 = {:?}\nblake3 = {b3:?}\nbytes = {}\n", sha256(bytes), bytes.len()));
}
// A CLI row: not a plugin, not this verb's.
list.push_str("\n[[artefact]]\nname = \"gunnar\"\nrole = \"cli\"\nsha256 = \"00\"\nblake3 = \"00\"\nbytes = 1\n");
bucket.put(list_key, list.clone().into_bytes());
bucket.put(&format!("{list_key}.p7s"), sign::sign_module(list.as_bytes(), list_by).unwrap());
}
}
struct Box_ {
dir: PathBuf,
config: PathBuf,
module: PathBuf,
signer: Ed25519Signer,
stranger: Ed25519Signer,
}
/// A box that runs the toy product as a SIGNED guest under its root.
fn a_box(name: &str) -> Box_ {
let dir = scratch(name);
let (ca_key, ca_der) = dev::mint_ca("plugin-root").unwrap();
let root = dir.join("korp.der");
std::fs::write(&root, &ca_der).unwrap();
let signer = dev::new_ed25519_signer(&ca_key, &ca_der, "plugin-signing").unwrap();
let (s_key, s_der) = dev::mint_ca("stranger-ca").unwrap();
let stranger = dev::new_ed25519_signer(&s_key, &s_der, "stranger").unwrap();
std::fs::create_dir_all(dir.join("guests")).unwrap();
let module = dir.join("guests/toy.wasm");
std::fs::write(&module, guest_bytes("monetize_guest_toy", "")).unwrap();
sign::sign_module_file(&module, &signer).unwrap();
let config = dir.join("monetize.toml");
std::fs::write(
&config,
format!(
"data_dir = {:?}\n[products.toy]\nendpoint = \"127.0.0.1:1\"\nguest = {:?}\nguest_trust_roots = [{:?}]\ngrants = [\"endpoint\"]\n",
dir.join("data").display().to_string(),
module.display().to_string(),
root.display().to_string()
),
)
.unwrap();
Box_ { dir, config, module, signer, stranger }
}
#[test]
fn a_look_writes_nothing_an_update_replaces_the_module_the_watcher_swaps_it_and_again_is_unchanged() {
let b = a_box("update");
let cfg = Config::load(&b.config).unwrap();
let state = AppState::in_memory(Registry::from_config(&cfg).unwrap());
let served = |s: &AppState| s.registry.guest("toy").map(|g| g.digest_hex().to_string());
let v1 = std::fs::read(&b.module).unwrap();
assert_eq!(served(&state), Some(guest_host::digest_hex(&v1)));
let mut last = (Vec::new(), Some(cfg.clone()));
let _ = look(&state, &b.config, &mut last);
let bucket = MapBucket::default();
let v2 = guest_bytes("monetize_guest_toy", "v2");
Release { rows: vec![("toy_guest.wasm".into(), "plugin", v2.clone())] }.publish(&bucket, &b.signer, &b.signer);
let stage = b.dir.join("stage");
// A LOOK: what would change, nothing written.
let rows = plugins::update(&cfg, &bucket, false, &stage).unwrap();
assert_eq!(rows.len(), 1, "the CLI row is not a plugin: {rows:?}");
assert_eq!((rows[0].name.as_str(), rows[0].role.as_str(), rows[0].outcome.as_str()), ("toy_guest.wasm", "plugin", "would-replace"), "{rows:?}");
assert!(rows[0].detail.contains("[products.toy]"), "{rows:?}");
assert_eq!(std::fs::read(&b.module).unwrap(), v1, "a look wrote the module");
// THE UPDATE: written where the block names it, its signature beside it.
let rows = plugins::update(&cfg, &bucket, true, &stage).unwrap();
assert_eq!(rows[0].outcome, "replaced", "{rows:?}");
assert!(rows[0].detail.contains("[products.toy]") && rows[0].detail.contains("signed by plugin-signing"), "{rows:?}");
assert_eq!(std::fs::read(&b.module).unwrap(), v2);
let leftovers: Vec<_> = std::fs::read_dir(b.module.parent().unwrap()).unwrap().flatten().map(|e| e.file_name().to_string_lossy().to_string()).filter(|n| n.starts_with('.')).collect();
assert!(leftovers.is_empty(), "a staged file was left beside the module: {leftovers:?}");
// The watcher takes it in, live, through every gate a start passes.
std::thread::sleep(std::time::Duration::from_millis(20));
let lines = look(&state, &b.config, &mut last);
assert!(lines.iter().any(|l| l.starts_with("guest REPLACED [products.toy]") && l.contains("signed by plugin-signing")), "{lines:#?}");
assert_eq!(served(&state), Some(guest_host::digest_hex(&v2)));
// Again: unchanged.
let rows = plugins::update(&cfg, &bucket, true, &stage).unwrap();
assert_eq!(rows[0].outcome, "unchanged", "{rows:?}");
let _ = std::fs::remove_dir_all(&b.dir);
}
#[test]
fn a_list_or_a_module_that_does_not_verify_is_refused_by_name_and_nothing_is_written() {
let b = a_box("refused");
let cfg = Config::load(&b.config).unwrap();
let v1 = std::fs::read(&b.module).unwrap();
let v1_sig = std::fs::read(guest_host::ModuleTrust::signature_path(&b.module)).unwrap();
let stage = b.dir.join("stage");
let untouched = |why: &str| {
assert_eq!(std::fs::read(&b.module).unwrap(), v1, "{why}: the module was written");
assert_eq!(std::fs::read(guest_host::ModuleTrust::signature_path(&b.module)).unwrap(), v1_sig, "{why}: the signature was written");
};
let v2 = guest_bytes("monetize_guest_toy", "v2");
// No list at all.
let e = plugins::update(&cfg, &MapBucket::default(), true, &stage).unwrap_err();
assert!(e.contains("[plugins-list-missing]"), "{e}");
// The list signed by a stranger: the whole job refused.
let bucket = MapBucket::default();
Release { rows: vec![("toy_guest.wasm".into(), "plugin", v2.clone())] }.publish(&bucket, &b.signer, &b.stranger);
let e = plugins::update(&cfg, &bucket, true, &stage).unwrap_err();
assert!(e.contains("[plugins-list-unsigned]"), "{e}");
untouched("a list nobody trusted");
// The bucket holds other bytes than the signed list names.
let bucket = MapBucket::default();
Release { rows: vec![("toy_guest.wasm".into(), "plugin", v2.clone())] }.publish(&bucket, &b.signer, &b.signer);
let key = plugins::object_key(&guest_host::digest_hex(&v2), "toy_guest.wasm");
let mut forged = v2.clone();
let n = forged.len();
forged[n - 1] ^= 1;
bucket.put(&key, forged);
let rows = plugins::update(&cfg, &bucket, true, &stage).unwrap();
assert_eq!(rows[0].outcome, "refused", "{rows:?}");
assert!(rows[0].detail.contains("[plugin-digest]"), "{rows:?}");
untouched("bytes the list does not name");
// The module signed by a stranger, the list by the release.
let bucket = MapBucket::default();
Release { rows: vec![("toy_guest.wasm".into(), "plugin", v2.clone())] }.publish(&bucket, &b.stranger, &b.signer);
let rows = plugins::update(&cfg, &bucket, true, &stage).unwrap();
assert_eq!(rows[0].outcome, "refused", "{rows:?}");
assert!(rows[0].detail.contains("[products.toy]") && rows[0].detail.contains("[plugin-unsigned]"), "{rows:?}");
untouched("a module a stranger signed");
// A guest no block on this box runs: refused by name, the others go on.
let bucket = MapBucket::default();
Release { rows: vec![("upcloud.wasm".into(), "cloud", guest_bytes("monetize_guest_toy_cloud", "")), ("toy_guest.wasm".into(), "plugin", v2.clone())] }.publish(&bucket, &b.signer, &b.signer);
let rows = plugins::update(&cfg, &bucket, true, &stage).unwrap();
let by: BTreeMap<&str, &plugins::Row> = rows.iter().map(|r| (r.name.as_str(), r)).collect();
assert_eq!(by["upcloud.wasm"].outcome, "refused", "{rows:?}");
assert!(by["upcloud.wasm"].detail.contains("[plugin-no-block]") && by["upcloud.wasm"].detail.contains("toycloud"), "{rows:?}");
assert_eq!(by["toy_guest.wasm"].outcome, "replaced", "{rows:?}");
assert_eq!(std::fs::read(&b.module).unwrap(), v2);
let _ = std::fs::remove_dir_all(&b.dir);
}
/// ★ **A queued jera job** (owner rule: background work is a jera job; a UI ask never blocks):
/// `submit` answers at once with the job, the same ask while it is in flight is that job,
/// and `status` reads it to its end with one row per plugin.
#[test]
fn the_update_is_a_queued_job_that_answers_at_once_and_is_read_to_its_end() {
let b = a_box("job");
let cfg = Config::load(&b.config).unwrap();
let state = AppState::in_memory(Registry::from_config(&cfg).unwrap());
*state.config.write().unwrap() = cfg.clone();
let state = Arc::new(state);
let bucket = MapBucket::default();
let v2 = guest_bytes("monetize_guest_toy", "v2");
Release { rows: vec![("toy_guest.wasm".into(), "plugin", v2.clone())] }.publish(&bucket, &b.signer, &b.signer);
let _ = state.plugin_jobs.fetch.set(Arc::new(bucket));
assert!(state.plugin_jobs.status("").is_none(), "no job yet");
let rt = tokio::runtime::Builder::new_multi_thread().worker_threads(2).enable_all().build().unwrap();
let job = rt.block_on(async { state.plugin_jobs.submit(&state, true) }).unwrap();
assert!(!job.job.is_empty() && job.apply, "{job:?}");
assert!(["queued", "running", "done"].contains(&job.state.as_str()), "{job:?}");
let mut seen = job.clone();
for _ in 0..600 {
seen = state.plugin_jobs.status(&job.job).expect("the job is readable by its id");
if seen.state == "done" || seen.state == "failed" {
break;
}
std::thread::sleep(std::time::Duration::from_millis(50));
}
assert_eq!(seen.state, "done", "{seen:?}");
assert_eq!(seen.rows.len(), 1, "{seen:?}");
assert_eq!(seen.rows[0].outcome, "replaced", "{seen:?}");
assert_eq!(state.plugin_jobs.status("").map(|v| v.job), Some(job.job.clone()), "the newest job");
assert_eq!(std::fs::read(&b.module).unwrap(), v2);
drop(rt);
let _ = std::fs::remove_dir_all(&b.dir);
}
/// ★ **OXG-5: a box named by its feed reads its OWN list, never the shared one.** One shared
/// `release/plugins/release-manifest.toml` served test2 and prod: `box plugins --apply` on
/// test2 put prod's offered guests back, and a test2 publish changed prod. A box whose feed names
/// it (`plugins_box`) reads `release/plugins/box/<box>/…`; a missing one is refused by name and
/// the shared list is never taken in its place. A config that names no box (an old feed) reads
/// the shared list, as before.
#[test]
fn a_box_named_by_its_feed_reads_its_own_list_never_the_shared_one() {
let b = a_box("per-box");
let bucket = MapBucket::default();
let stage = b.dir.join("stage");
let v1 = std::fs::read(&b.module).unwrap();
// Prod's release: the shared list names prod's guest.
let prod = guest_bytes("monetize_guest_toy", "prod");
Release { rows: vec![("toy_guest.wasm".into(), "plugin", prod.clone())] }.publish(&bucket, &b.signer, &b.signer);
let text = std::fs::read_to_string(&b.config).unwrap();
std::fs::write(&b.config, format!("plugins_box = \"test2\"\n{text}")).unwrap();
let cfg = Config::load(&b.config).unwrap();
assert_eq!(plugins::list_key_of(&cfg).unwrap(), plugins::box_list_key("test2"));
// test2's own list is not published: refused by name, prod's never taken.
let e = plugins::update(&cfg, &bucket, true, &stage).unwrap_err();
assert!(e.contains("[plugins-list-missing]") && e.contains("release/plugins/box/test2/release-manifest.toml"), "{e}");
assert_eq!(std::fs::read(&b.module).unwrap(), v1, "prod's guest was put on test2");
// test2's own list: taken.
let t2 = guest_bytes("monetize_guest_toy", "test2");
Release { rows: vec![("toy_guest.wasm".into(), "plugin", t2.clone())] }.publish_at(&bucket, &plugins::box_list_key("test2"), &b.signer, &b.signer);
let rows = plugins::update(&cfg, &bucket, true, &stage).unwrap();
assert_eq!(rows[0].outcome, "replaced", "{rows:?}");
assert_eq!(std::fs::read(&b.module).unwrap(), t2);
// An old feed names no box: the shared list.
assert_eq!(plugins::list_key_of(&Config::default()).unwrap(), LIST_KEY);
let bad = Config { plugins_box: Some("../prod".into()), ..Config::default() };
assert!(plugins::list_key_of(&bad).unwrap_err().contains("[plugins-box]"), "a box name is a plain label");
let _ = std::fs::remove_dir_all(&b.dir);
}