115 lines
6.2 KiB
Rust
115 lines
6.2 KiB
Rust
//! First-party content: the dice plugin as a registry entry. A throwaway key (generated by
|
|
//! `cargo xtask registry-sign keygen` in a temp folder, never in the repository) signs a registry
|
|
//! tree built from `plugins/dice`; the plugin then installs offline from a seed folder of that
|
|
//! tree, is enabled, approved and rolls. Needs `plugins/dice/build.sh` to have run.
|
|
|
|
mod common;
|
|
|
|
use std::path::{Path, PathBuf};
|
|
use std::process::Command;
|
|
use std::sync::Arc;
|
|
|
|
use common::{temp_config, temp_vault};
|
|
use notescraft_engine::addons::{Ed25519, FakeTransport, INDEX_URL, Index, Kind, Response, SeedDir, Source, Transport};
|
|
use notescraft_engine::{Registry, Session, register_core};
|
|
use serde_json::{Value, json};
|
|
|
|
const KEY_ID: &str = "k1";
|
|
/// What `entry.toml` says about the plugin; the permissions must equal the manifest's.
|
|
const ENTRY_TOML: &str = "title = \"Dice\"\nsummary = \"Rolls dice: 2d6+3\"\nlicense = \"MIT\"\nmin_app_version = \"0.1.0\"\naliases = [\"dice\", \"roll\"]\npermissions = [\"editor\"]\n";
|
|
|
|
fn repo() -> PathBuf {
|
|
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..")
|
|
}
|
|
|
|
/// Runs `cargo xtask registry-sign <args>` and answers its stdout.
|
|
fn sign_tool(args: &[&str]) -> String {
|
|
let out = Command::new(env!("CARGO")).current_dir(repo()).arg("xtask").arg("registry-sign").args(args).output().expect("cargo runs");
|
|
assert!(out.status.success(), "{}", String::from_utf8_lossy(&out.stderr));
|
|
String::from_utf8(out.stdout).expect("utf-8")
|
|
}
|
|
|
|
fn run(reg: &Registry<Session>, session: &mut Session, id: &str, params: &Value) -> Value {
|
|
reg.run(id, session, params).unwrap_or_else(|e| panic!("{id}: {e}"))
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Shared(Arc<FakeTransport>);
|
|
|
|
impl Transport for Shared {
|
|
fn get(&self, url: &str, max_bytes: u64, if_none_match: Option<&str>) -> notescraft_engine::Result<Response> {
|
|
self.0.get(url, max_bytes, if_none_match)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn dice_installs_from_a_signed_seed_offline_and_rolls() {
|
|
let dice = repo().join("plugins/dice");
|
|
let (Ok(wasm), Ok(manifest)) = (std::fs::read(dice.join("dice.wasm")), std::fs::read(dice.join("manifest.json"))) else {
|
|
println!("skipped: build plugins/dice/build.sh first");
|
|
return;
|
|
};
|
|
let work = std::env::temp_dir().join(format!("notescraft-first-party-{}", std::process::id()));
|
|
let _ = std::fs::remove_dir_all(&work);
|
|
let item = work.join("registry/files/plugin/dice");
|
|
std::fs::create_dir_all(item.join("1.0.0")).expect("registry tree");
|
|
std::fs::write(item.join("entry.toml"), ENTRY_TOML).expect("entry.toml");
|
|
std::fs::write(item.join("1.0.0/manifest.json"), &manifest).expect("manifest");
|
|
std::fs::write(item.join("1.0.0/plugin.wasm"), &wasm).expect("wasm");
|
|
|
|
let key = work.join("throwaway.key");
|
|
let public = sign_tool(&["keygen", "--out", key.to_str().expect("path")]).trim().to_owned();
|
|
let registry = work.join("registry");
|
|
sign_tool(&["--dir", registry.to_str().expect("path"), "--key", key.to_str().expect("path")]);
|
|
sign_tool(&["--check", "--dir", registry.to_str().expect("path"), "--public-key", &public]);
|
|
|
|
// The seed is the signed tree itself: index.json plus files/ (entry.toml is not part of it).
|
|
let seed_root = work.join("registry-seed");
|
|
std::fs::create_dir_all(seed_root.join("files/plugin/dice/1.0.0")).expect("seed tree");
|
|
std::fs::copy(registry.join("index.json"), seed_root.join("index.json")).expect("seed index");
|
|
for name in ["manifest.json", "plugin.wasm"] {
|
|
std::fs::copy(item.join("1.0.0").join(name), seed_root.join("files/plugin/dice/1.0.0").join(name)).expect("seed file");
|
|
}
|
|
let index_bytes = std::fs::read(registry.join("index.json")).expect("index");
|
|
let index = Index::parse(&index_bytes).expect("the signed index parses");
|
|
let entry = index.find(Kind::Plugin, "dice").expect("dice is indexed");
|
|
let declared: Value = serde_json::from_slice(&manifest).expect("manifest json");
|
|
assert_eq!(declared["id"], json!(entry.id));
|
|
assert_eq!(declared["version"], json!(entry.version.to_string()));
|
|
assert_eq!(declared["permissions"], json!(entry.permissions));
|
|
|
|
let (vault, config) = (temp_vault("first-party"), temp_config("first-party"));
|
|
let mut session = Session::open(vault.path()).expect("session");
|
|
session.use_settings_dir(config.path());
|
|
let key_bytes = notescraft_engine::decode_base64(&public).expect("public key");
|
|
let key_array: [u8; 32] = key_bytes.try_into().expect("32 bytes");
|
|
session.set_addons_verifier(Box::new(Ed25519::new(vec![(KEY_ID.to_owned(), key_array)])));
|
|
// Only the index is online; every file must come from the seed.
|
|
let net = FakeTransport::new().with_response(INDEX_URL, Ok(Response { status: 200, etag: None, bytes: index_bytes }));
|
|
session.set_addons_transport(Box::new(Shared(Arc::new(net))));
|
|
session.set_addons_seed(Some(Box::new(SeedDir::new(seed_root.clone()))));
|
|
let mut reg = Registry::new();
|
|
register_core(&mut reg).expect("core commands");
|
|
|
|
run(®, &mut session, "registry.check", &json!({}));
|
|
let report = run(®, &mut session, "registry.install", &json!({ "kind": "plugin", "id": "dice" }));
|
|
assert_eq!(report["source"], json!(format!("{:?}", Source::Seed).to_lowercase()));
|
|
run(®, &mut session, "registry.enable", &json!({ "id": "dice" }));
|
|
run(®, &mut session, "plugins.approve", &json!({ "id": "dice" }));
|
|
assert_eq!(session.reload_plugin_commands(&mut reg).expect("reload"), 1);
|
|
let answer = reg.run("plugin.dice.roll", &mut session, &json!({ "args": { "expr": "2d6+3" } })).expect("dice rolls");
|
|
let total = answer["result"]["total"].as_i64().expect("a total");
|
|
assert!((5..=15).contains(&total), "{answer}");
|
|
assert!(answer["result"]["text"].as_str().is_some_and(|text| text.starts_with("2d6+3 = ")), "{answer}");
|
|
|
|
// A flipped byte in the seed is not used; offline, the install then fails and changes nothing.
|
|
run(®, &mut session, "registry.remove", &json!({ "kind": "plugin", "id": "dice" }));
|
|
let mut bad = wasm.clone();
|
|
if let Some(byte) = bad.last_mut() {
|
|
*byte ^= 1;
|
|
}
|
|
std::fs::write(seed_root.join("files/plugin/dice/1.0.0/plugin.wasm"), bad).expect("tamper");
|
|
assert!(reg.run("registry.install", &mut session, &json!({ "kind": "plugin", "id": "dice" })).is_err());
|
|
let _ = std::fs::remove_dir_all(&work);
|
|
}
|