use standard_plugin_cli::check::{Report, check};
use standard_plugin_cli::{pack, world};
use wit_parser::{LiftLowerAbi, ManglingAndAbi, Resolve};
fn component(extra: &str, world: &str) -> Vec<u8> {
let mut resolve = Resolve::default();
let package = resolve
.push_source("plugin.wit", &format!("{}\n{extra}", world::WIT))
.unwrap();
let world = resolve.select_world(&[package], Some(world)).unwrap();
let mut module =
wit_component::dummy_module(&resolve, world, ManglingAndAbi::Legacy(LiftLowerAbi::Sync));
wit_component::embed_component_metadata(
&mut module,
&resolve,
world,
wit_component::StringEncoding::UTF8,
)
.unwrap();
wit_component::ComponentEncoder::default()
.validate(true)
.module(&module)
.unwrap()
.encode()
.unwrap()
}
fn ui_manifest(grants: &str) -> String {
format!(
r#"{{"apiVersion":2,"kind":"ui","id":"card","version":"0.1.0","module":"card.wasm",
"surfaces":[{{"id":"card","anchor":"sidebar.card","model":"cells","height":2}}],
"grants":{{{grants}}}}}"#
)
}
fn has(messages: &[String], needle: &str) -> bool {
messages.iter().any(|message| message.contains(needle))
}
#[test]
fn every_grant_needs_a_reason_of_at_most_140_characters() {
let long = "x".repeat(141);
let report = check(
ui_manifest(&format!(
r#""account.read":" ","secret:TOKEN":"{long}","values.read:git.*":"Shows the branch""#
))
.as_bytes(),
None,
);
assert!(!report.is_ok());
assert!(
has(&report.errors, "`account.read` has no reason"),
"{report}"
);
assert!(
has(
&report.errors,
"`secret:TOKEN`: the reason is 141 characters"
),
"{report}"
);
assert_eq!(report.errors.len(), 2, "{report}");
let missing = check(ui_manifest(r#""account.read":null"#).as_bytes(), None);
assert!(has(&missing.errors, "has no reason"), "{missing}");
}
#[test]
fn unknown_malformed_and_out_of_world_grants_are_rejected() {
let report = check(
ui_manifest(
r#""account.write":"Changes things","values.read":"No prefix","process.exec:git":"Runs git""#,
)
.as_bytes(),
None,
);
assert!(
has(&report.errors, "unknown grant \"account.write\""),
"{report}"
);
assert!(has(&report.errors, "needs a key prefix"), "{report}");
assert!(
has(&report.errors, "not available to ui plugins"),
"{report}"
);
}
#[test]
fn a_component_importing_beyond_its_world_is_rejected() {
let sneaky = component(
"interface sneaky { steal: func() -> string; }\n\
world ui-sneaky { include ui-plugin; import sneaky; }",
"ui-sneaky",
);
let grants =
r#""account.read":"Shows panes","secret:T":"Signs in","call:card":"Asks the companion""#;
let report = check(ui_manifest(grants).as_bytes(), Some(&sneaky));
assert!(
has(
&report.errors,
"imports `standard:plugin/sneaky@2.0.0`, which the ui-plugin world does not offer"
),
"{report}"
);
assert_eq!(report.errors.len(), 1, "{report}");
let clean = check(
ui_manifest(grants).as_bytes(),
Some(&component("", "ui-plugin")),
);
assert!(clean.is_ok(), "{clean}");
let daemon = component("", "daemon-plugin");
let report = check(ui_manifest("").as_bytes(), Some(&daemon));
assert!(has(&report.errors, "does not export `frame`"), "{report}");
assert!(
has(&report.errors, "daemon-process@2.0.0`, which the ui-plugin"),
"{report}"
);
let core = wat::parse_str("(module)").unwrap();
let report = check(ui_manifest("").as_bytes(), Some(&core));
assert!(has(&report.errors, "not a component"), "{report}");
}
#[test]
fn interfaces_no_grant_covers_and_grants_nothing_uses_are_flagged() {
let everything = component("", "ui-plugin");
let report = check(ui_manifest("").as_bytes(), Some(&everything));
assert!(report.is_ok(), "{report}");
for interface in ["account", "secrets", "calls", "url"] {
assert!(
has(
&report.warnings,
&format!("uses the `{interface}` interface but no grant")
),
"{interface}: {report}"
);
}
let manifest = r#"{"apiVersion":2,"kind":"daemon","id":"d","version":"1","module":"d.wasm",
"grants":{"panes.write":"Opens a pane for you"}}"#;
let minimal = component(
"world bare {
use event-types.{event};
export activate: func(config: string);
export event: func(e: event);
export daemon-events;
export companion;
export drive: func(now-ms: u64) -> option<u64>;
export deactivate: func();
}",
"bare",
);
let report: Report = check(manifest.as_bytes(), Some(&minimal));
assert!(report.is_ok(), "{report}");
assert!(
has(&report.warnings, "grant `panes.write` is never used"),
"{report}"
);
}
#[test]
fn machine_full_covers_the_process_and_watch_interfaces() {
let daemon = |grants: &str| {
format!(
r#"{{"apiVersion":2,"kind":"daemon","id":"d","version":"1","module":"d.wasm",
"grants":{{{grants}}}}}"#
)
};
let everything = component("", "daemon-plugin");
let narrow = check(daemon("").as_bytes(), Some(&everything));
for interface in ["daemon-process", "daemon-watch"] {
assert!(
has(
&narrow.warnings,
&format!("uses the `{interface}` interface but no grant")
),
"{interface}: {narrow}"
);
}
let full = check(
daemon(r#""machine.full":"Runs your build tools""#).as_bytes(),
Some(&everything),
);
assert!(full.is_ok(), "{full}");
for interface in ["daemon-process", "daemon-watch"] {
assert!(
!has(
&full.warnings,
&format!("uses the `{interface}` interface but no grant")
),
"{interface}: {full}"
);
}
}
#[test]
fn the_double_play_pattern_is_flagged() {
let report = check(
ui_manifest(
r#""events.on:global.*":"Follows builds","values.write:builds.*":"Records the result""#,
)
.as_bytes(),
None,
);
assert!(report.is_ok(), "{report}");
assert!(has(&report.warnings, "double play"), "{report}");
for grants in [
r#""events.on:global.*":"Follows builds""#,
r#""values.write:builds.*":"Records the result""#,
] {
let report = check(ui_manifest(grants).as_bytes(), None);
assert!(!has(&report.warnings, "double play"), "{report}");
}
}
#[test]
fn packing_a_bundle_twice_gives_the_same_account_package() {
let temp = tempfile::tempdir().unwrap();
let bundle = temp.path();
std::fs::write(bundle.join("standard-plugin.json"), ui_manifest("")).unwrap();
std::fs::write(bundle.join("card.wasm"), component("", "ui-plugin")).unwrap();
std::fs::create_dir_all(bundle.join("browser/interfaces")).unwrap();
std::fs::write(bundle.join("browser/card.js"), "export {}").unwrap();
std::fs::write(bundle.join("browser/interfaces/a.d.ts"), "").unwrap();
let first = pack::package(bundle).unwrap();
std::thread::sleep(std::time::Duration::from_millis(20));
std::fs::write(bundle.join("browser/card.js"), "export {}").unwrap();
let second = pack::package(bundle).unwrap();
assert_eq!(
pack::content_hash(&first.bytes),
pack::content_hash(&second.bytes)
);
assert_eq!(pack::content_hash(&first.bytes).len(), 64);
assert_eq!(first.manifest.ui.unwrap().entry, "ui/card.wasm");
assert_eq!(
pack::entry_names(&first.bytes).unwrap(),
[
"manifest.json",
"ui/standard-plugin.json",
"ui/card.wasm",
"ui/browser/card.js",
"ui/browser/interfaces/a.d.ts"
]
);
}
#[test]
fn a_config_schema_is_checked_against_the_subset_hosts_render() {
let manifest = |config: &str| {
format!(
r#"{{"apiVersion":2,"kind":"ui","id":"card","version":"0.1.0","module":"card.wasm",
"surfaces":[{{"id":"card","anchor":"sidebar.card","model":"cells","height":2}}],
"config":{config}}}"#
)
};
let good = check(
manifest(
r#"{"type":"object","properties":{"refreshSeconds":{"type":"integer",
"minimum":300,"maximum":1800,"default":300,"title":"Refresh every"}}}"#,
)
.as_bytes(),
None,
);
assert!(good.is_ok(), "{good}");
assert!(good.manifest.unwrap().config.is_some());
let typo = check(
manifest(r#"{"type":"object","properties":{"refresh":{"type":"integer","defualt":3}}}"#)
.as_bytes(),
None,
);
assert!(has(&typo.errors, "unknown keyword \"defualt\""), "{typo}");
let out_of_range = check(
manifest(
r#"{"type":"object","properties":{"refresh":{"type":"integer","minimum":300,"default":5}}}"#,
)
.as_bytes(),
None,
);
assert!(
has(
&out_of_range.errors,
"config.refresh.default is below the minimum"
),
"{out_of_range}"
);
}