Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/monetize

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);
}