From 6fb8a14120859ddf589d61523d7580bc5b76fffd Mon Sep 17 00:00:00 2001 From: Lann Martin Date: Tue, 3 Oct 2023 12:49:50 -0400 Subject: [PATCH] Move manifest tests from spin-loader to spin-trigger Also convert them to "ui tests" which stick their expected output in a file next to the input which can be updated with BLESS=1. Signed-off-by: Lann Martin --- Cargo.lock | 80 ++++- crates/loader/src/local/mod.rs | 3 - crates/loader/src/local/tests.rs | 301 ------------------ crates/trigger/Cargo.toml | 15 +- crates/trigger/src/locked.rs | 73 ----- crates/trigger/tests/ui.rs | 133 ++++++++ .../insecure-allow-all-with-invalid-url.lock | 41 +++ .../insecure-allow-all-with-invalid-url.toml | 0 crates/trigger/tests/ui/invalid-http-base.err | 4 + .../tests/ui}/invalid-http-base.toml | 0 .../ui/invalid-manifest-duplicate-id.err | 1 + .../ui}/invalid-manifest-duplicate-id.toml | 0 crates/trigger/tests/ui/invalid-manifest.err | 4 + .../tests/ui}/invalid-manifest.toml | 0 .../ui/invalid-url-in-allowed-http-hosts.err | 3 + .../invalid-url-in-allowed-http-hosts.toml | 0 crates/trigger/tests/ui/invalid-version.err | 4 + .../tests/ui}/invalid-version.toml | 0 crates/trigger/tests/ui/valid-manifest.lock | 95 ++++++ .../tests/ui}/valid-manifest.toml | 4 +- .../tests/ui}/valid-with-files/spin-fs.wasm | Bin .../tests/ui/valid-with-files/spin.lock | 46 +++ .../tests/ui}/valid-with-files/spin.toml | 0 .../ui}/valid-with-files/static/alphabet/a | 0 .../ui}/valid-with-files/static/alphabet/b | 0 .../ui}/valid-with-files/static/alphabet/c | 0 .../ui}/valid-with-files/static/numbers/1 | 0 .../ui}/valid-with-files/static/numbers/2 | 0 .../ui}/valid-with-files/static/numbers/3 | 0 .../tests/ui/wagi-custom-entrypoint.lock | 48 +++ .../tests/ui}/wagi-custom-entrypoint.toml | 0 crates/trigger/tests/ui/wasm/dummy.wasm | 0 crates/trigger/valid-manifest.lock | 95 ++++++ 33 files changed, 564 insertions(+), 386 deletions(-) delete mode 100644 crates/loader/src/local/tests.rs create mode 100644 crates/trigger/tests/ui.rs create mode 100644 crates/trigger/tests/ui/insecure-allow-all-with-invalid-url.lock rename crates/{loader/tests => trigger/tests/ui}/insecure-allow-all-with-invalid-url.toml (100%) create mode 100644 crates/trigger/tests/ui/invalid-http-base.err rename crates/{loader/tests => trigger/tests/ui}/invalid-http-base.toml (100%) create mode 100644 crates/trigger/tests/ui/invalid-manifest-duplicate-id.err rename crates/{loader/tests => trigger/tests/ui}/invalid-manifest-duplicate-id.toml (100%) create mode 100644 crates/trigger/tests/ui/invalid-manifest.err rename crates/{loader/tests => trigger/tests/ui}/invalid-manifest.toml (100%) create mode 100644 crates/trigger/tests/ui/invalid-url-in-allowed-http-hosts.err rename crates/{loader/tests => trigger/tests/ui}/invalid-url-in-allowed-http-hosts.toml (100%) create mode 100644 crates/trigger/tests/ui/invalid-version.err rename crates/{loader/tests => trigger/tests/ui}/invalid-version.toml (100%) create mode 100644 crates/trigger/tests/ui/valid-manifest.lock rename crates/{loader/tests => trigger/tests/ui}/valid-manifest.toml (93%) rename crates/{loader/tests => trigger/tests/ui}/valid-with-files/spin-fs.wasm (100%) create mode 100644 crates/trigger/tests/ui/valid-with-files/spin.lock rename crates/{loader/tests => trigger/tests/ui}/valid-with-files/spin.toml (100%) rename crates/{loader/tests => trigger/tests/ui}/valid-with-files/static/alphabet/a (100%) rename crates/{loader/tests => trigger/tests/ui}/valid-with-files/static/alphabet/b (100%) rename crates/{loader/tests => trigger/tests/ui}/valid-with-files/static/alphabet/c (100%) rename crates/{loader/tests => trigger/tests/ui}/valid-with-files/static/numbers/1 (100%) rename crates/{loader/tests => trigger/tests/ui}/valid-with-files/static/numbers/2 (100%) rename crates/{loader/tests => trigger/tests/ui}/valid-with-files/static/numbers/3 (100%) create mode 100644 crates/trigger/tests/ui/wagi-custom-entrypoint.lock rename crates/{loader/tests => trigger/tests/ui}/wagi-custom-entrypoint.toml (100%) create mode 100644 crates/trigger/tests/ui/wasm/dummy.wasm create mode 100644 crates/trigger/valid-manifest.lock diff --git a/Cargo.lock b/Cargo.lock index eeac57a056..bce0de8bae 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -119,7 +119,21 @@ dependencies = [ "anstyle", "anstyle-parse", "anstyle-query", - "anstyle-wincon", + "anstyle-wincon 2.1.0", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstream" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon 3.0.1", "colorchoice", "utf8parse", ] @@ -158,6 +172,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "anstyle-wincon" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" +dependencies = [ + "anstyle", + "windows-sys 0.48.0", +] + [[package]] name = "anyhow" version = "1.0.75" @@ -867,7 +891,7 @@ version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78116e32a042dd73c2901f0dc30790d20ff3447f3e3472fad359e8c3d282bcd6" dependencies = [ - "anstream", + "anstream 0.5.0", "anstyle", "clap_lex 0.5.1", "strsim", @@ -3048,6 +3072,17 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libtest-mimic" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d8de370f98a6cb8a4606618e53e802f93b094ddec0f96988eaec2c27e6e9ce7" +dependencies = [ + "clap 4.4.0", + "termcolor", + "threadpool", +] + [[package]] name = "link-cplusplus" version = "1.0.8" @@ -3565,6 +3600,12 @@ dependencies = [ "memchr", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "normalize-path" version = "0.2.0" @@ -5203,6 +5244,28 @@ dependencies = [ "version_check", ] +[[package]] +name = "snapbox" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b377c0b6e4715c116473d8e40d51e3fa5b0a2297ca9b2a931ba800667b259ed" +dependencies = [ + "anstream 0.6.4", + "anstyle", + "normalize-line-endings", + "similar", + "snapbox-macros", +] + +[[package]] +name = "snapbox-macros" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed1559baff8a696add3322b9be3e940d433e7bb4e38d79017205fd37ff28b28e" +dependencies = [ + "anstream 0.6.4", +] + [[package]] name = "socket2" version = "0.4.9" @@ -5780,6 +5843,7 @@ dependencies = [ "dirs 4.0.0", "futures", "indexmap 1.9.2", + "libtest-mimic", "outbound-http", "outbound-mysql", "outbound-pg", @@ -5787,6 +5851,7 @@ dependencies = [ "sanitize-filename", "serde", "serde_json", + "snapbox", "spin-app", "spin-common", "spin-componentize", @@ -6108,6 +6173,15 @@ dependencies = [ "once_cell", ] +[[package]] +name = "threadpool" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d050e60b33d41c19108b32cea32164033a9013fe3b46cbd4457559bfbf77afaa" +dependencies = [ + "num_cpus", +] + [[package]] name = "time" version = "0.1.45" @@ -6507,7 +6581,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" dependencies = [ "cfg-if", - "rand 0.7.3", + "rand 0.8.5", "static_assertions", ] diff --git a/crates/loader/src/local/mod.rs b/crates/loader/src/local/mod.rs index 3e698e24f8..08b555fe2b 100644 --- a/crates/loader/src/local/mod.rs +++ b/crates/loader/src/local/mod.rs @@ -7,9 +7,6 @@ pub mod assets; /// Configuration representation for a Spin application as a local spin.toml file. pub mod config; -#[cfg(test)] -mod tests; - use std::{ collections::HashMap, path::{Path, PathBuf}, diff --git a/crates/loader/src/local/tests.rs b/crates/loader/src/local/tests.rs deleted file mode 100644 index d12a23c267..0000000000 --- a/crates/loader/src/local/tests.rs +++ /dev/null @@ -1,301 +0,0 @@ -use crate::local::config::{RawDirectoryPlacement, RawFileMount, RawModuleSource}; - -use super::*; -use anyhow::Result; -use spin_manifest::{HttpConfig, HttpExecutor, HttpTriggerConfiguration}; -use std::path::PathBuf; - -fn raw_manifest_from_str(toml: &str) -> Result { - raw_manifest_from_slice(toml.as_bytes()) -} - -#[tokio::test] -async fn test_from_local_source() -> Result<()> { - const MANIFEST: &str = "tests/valid-with-files/spin.toml"; - - let temp_dir = tempfile::tempdir()?; - let dir = temp_dir.path(); - let app = from_file(MANIFEST, Some(dir)).await?; - - assert_eq!(app.info.name, "spin-local-source-test"); - assert_eq!(app.info.version, "1.0.0"); - assert_eq!(app.info.spin_version, SpinVersion::V1); - assert_eq!( - app.info.authors[0], - "Fermyon Engineering " - ); - - let http: HttpTriggerConfiguration = app.info.trigger.try_into()?; - assert_eq!(http.base, "/".to_string()); - - let component = &app.components[0]; - assert_eq!(component.wasm.mounts.len(), 1); - - let http: HttpConfig = app - .component_triggers - .get(&component.id) - .cloned() - .unwrap() - .try_into()?; - assert_eq!(http.executor.unwrap(), HttpExecutor::Spin); - assert_eq!(http.route, "/...".to_string()); - - let expected_path = - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/valid-with-files/spin.toml"); - assert_eq!(app.info.origin, ApplicationOrigin::File(expected_path)); - - Ok(()) -} - -#[test] -fn test_manifest_v1_signatures() -> Result<()> { - const MANIFEST: &str = include_str!("../../tests/valid-manifest.toml"); - let ageless = MANIFEST - .replace("spin_version", "temp") - .replace("spin_manifest_version", "temp"); - let v1_old = ageless.replace("temp", "spin_version"); - let v1_new = ageless.replace("temp", "spin_manifest_version"); - into_v1_manifest(&v1_old, "chain-of-command")?; - into_v1_manifest(&v1_new, "chain-of-command")?; - Ok(()) -} - -fn into_v1_manifest(spin_versioned_manifest: &str, app_name: &str) -> Result<()> { - use config::RawAppManifestAnyVersionImpl; - let raw: RawAppManifestAnyVersionImpl = - toml::from_slice(spin_versioned_manifest.as_bytes())?; - let manifest = raw.into_v1(); - assert_eq!(manifest.info.name, app_name); - Ok(()) -} - -#[test] -fn test_manifest() -> Result<()> { - const MANIFEST: &str = include_str!("../../tests/valid-manifest.toml"); - - let cfg_any: RawAppManifestAnyVersion = raw_manifest_from_str(MANIFEST)?; - let cfg = cfg_any.into_v1(); - - assert_eq!(cfg.info.name, "chain-of-command"); - assert_eq!(cfg.info.version, "6.11.2"); - assert_eq!( - cfg.info.description, - Some("A simple application that returns the number of lights".to_string()) - ); - - let http: HttpTriggerConfiguration = cfg.info.trigger.try_into()?; - assert_eq!(http.base, "/".to_string()); - - assert_eq!(cfg.info.authors.unwrap().len(), 3); - assert_eq!(cfg.components[0].id, "four-lights".to_string()); - - let http: HttpConfig = cfg.components[0].trigger.clone().try_into()?; - assert_eq!(http.executor.unwrap(), HttpExecutor::Spin); - assert_eq!(http.route, "/lights".to_string()); - - let test_component = &cfg.components[0]; - let test_env = &test_component.wasm.environment.as_ref().unwrap(); - assert_eq!(test_env.len(), 2); - assert_eq!(test_env.get("env1").unwrap(), "first"); - assert_eq!(test_env.get("env2").unwrap(), "second"); - - let test_files = &test_component.wasm.files.as_ref().unwrap(); - assert_eq!(test_files.len(), 3); - assert_eq!(test_files[0], RawFileMount::Pattern("file.txt".to_owned())); - assert_eq!( - test_files[1], - RawFileMount::Placement(RawDirectoryPlacement { - source: PathBuf::from("valid-with-files"), - destination: PathBuf::from("/vwf"), - }) - ); - assert_eq!( - test_files[2], - RawFileMount::Pattern("subdir/another.txt".to_owned()) - ); - - let u = match cfg.components[2].source.clone() { - RawModuleSource::Url(u) => u, - RawModuleSource::FileReference(_) => panic!("expected URL source"), - }; - - assert_eq!(u.url, "https://example.com/wasm.wasm.wasm".to_string()); - assert_eq!(u.digest, "sha256:12345".to_string()); - - Ok(()) -} - -#[tokio::test] -async fn can_parse_url_sources() -> Result<()> { - let fcs = FileComponentUrlSource { - url: "https://example.com/wasm.wasm.wasm".to_owned(), - digest: "sha256:12345".to_owned(), - }; - let us = UrlSource::new(&fcs)?; - assert_eq!("https", us.url().scheme()); - assert_eq!("/wasm.wasm.wasm", us.url().path()); - assert_eq!(PathBuf::from("wasm.wasm.wasm"), us.url_relative_path()); - Ok(()) -} - -#[tokio::test] -async fn url_sources_are_validated() -> Result<()> { - let fcs1 = FileComponentUrlSource { - url: "ftp://example.com/wasm.wasm.wasm".to_owned(), - digest: "sha256:12345".to_owned(), - }; - UrlSource::new(&fcs1).expect_err("fcs1 should fail on scheme"); - - let fcs2 = FileComponentUrlSource { - url: "SNORKBONGLY".to_owned(), - digest: "sha256:12345".to_owned(), - }; - UrlSource::new(&fcs2).expect_err("fcs2 should fail because not a URL"); - - let fcs3 = FileComponentUrlSource { - url: "https://example.com/wasm.wasm.wasm".to_owned(), - digest: "sha123:12345".to_owned(), - }; - UrlSource::new(&fcs3).expect_err("fcs3 should fail on digest fmt"); - - let fcs4 = FileComponentUrlSource { - url: "https://example.com/wasm.wasm.wasm".to_owned(), - digest: "sha256:".to_owned(), - }; - UrlSource::new(&fcs4).expect_err("fcs4 should fail on empty digest"); - - Ok(()) -} - -#[tokio::test] -async fn test_invalid_manifest() -> Result<()> { - const MANIFEST: &str = "tests/invalid-manifest.toml"; - - let temp_dir = tempfile::tempdir()?; - let dir = temp_dir.path(); - let app = from_file(MANIFEST, Some(dir)).await; - - let e = app.unwrap_err().to_string(); - assert!( - e.contains("invalid-manifest.toml"), - "Expected error to contain the manifest name" - ); - - Ok(()) -} - -#[test] -fn test_unknown_version_is_rejected() { - const MANIFEST: &str = include_str!("../../tests/invalid-version.toml"); - - let cfg = raw_manifest_from_str(MANIFEST); - assert!( - cfg.is_err(), - "Expected version to be validated but it wasn't" - ); - - let e = cfg.unwrap_err().to_string(); - assert!( - e.contains("spin_version"), - "Expected error to mention `spin_version`" - ); -} - -#[tokio::test] -async fn gives_correct_error_for_invalid_app_trigger_field() -> Result<()> { - const MANIFEST: &str = "tests/invalid-http-base.toml"; - - let app = raw_manifest_from_file(&PathBuf::from(MANIFEST)).await; - - let e = format!("{:#}", app.unwrap_err()); - assert!( - e.contains("expected a string for key `base`"), - "Expected error to contain trigger field information but was '{e}'" - ); - - Ok(()) -} - -#[test] -fn test_wagi_executor_with_custom_entrypoint() -> Result<()> { - const MANIFEST: &str = include_str!("../../tests/wagi-custom-entrypoint.toml"); - - const EXPECTED_CUSTOM_ENTRYPOINT: &str = "custom-entrypoint"; - const EXPECTED_DEFAULT_ARGV: &str = "${SCRIPT_NAME} ${ARGS}"; - - let cfg_any: RawAppManifestAnyVersion = raw_manifest_from_str(MANIFEST)?; - let cfg = cfg_any.into_v1(); - - let http_config: HttpConfig = cfg.components[0].trigger.clone().try_into()?; - - match http_config.executor.as_ref().unwrap() { - HttpExecutor::Spin => panic!("expected wagi http executor"), - HttpExecutor::Wagi(spin_manifest::WagiConfig { entrypoint, argv }) => { - assert_eq!(entrypoint, EXPECTED_CUSTOM_ENTRYPOINT); - assert_eq!(argv, EXPECTED_DEFAULT_ARGV); - } - }; - - Ok(()) -} - -#[tokio::test] -async fn test_duplicate_component_id_is_rejected() -> Result<()> { - const MANIFEST: &str = "tests/invalid-manifest-duplicate-id.toml"; - - let temp_dir = tempfile::tempdir()?; - let dir = temp_dir.path(); - let app = from_file(MANIFEST, Some(dir)).await; - - assert!( - app.is_err(), - "Expected component IDs to be unique, but there were duplicates" - ); - - let e = app.unwrap_err().to_string(); - assert!( - e.contains("hello"), - "Expected error to contain duplicate component ID `hello`" - ); - - Ok(()) -} - -#[tokio::test] -async fn test_insecure_allow_all_with_invalid_url() -> Result<()> { - const MANIFEST: &str = "tests/insecure-allow-all-with-invalid-url.toml"; - - let temp_dir = tempfile::tempdir()?; - let dir = temp_dir.path(); - let app = from_file(MANIFEST, Some(dir)).await; - - assert!( - app.is_ok(), - "Expected insecure:allow-all can skip url validation" - ); - - Ok(()) -} - -#[tokio::test] -async fn test_invalid_url_in_allowed_http_hosts_is_rejected() -> Result<()> { - const MANIFEST: &str = "tests/invalid-url-in-allowed-http-hosts.toml"; - - let temp_dir = tempfile::tempdir()?; - let dir = temp_dir.path(); - let app = from_file(MANIFEST, Some(dir)).await; - - assert!(app.is_err(), "Expected allowed_http_hosts parsing error"); - - let e = app.unwrap_err().to_string(); - assert!( - e.contains("ftp://random-data-api.fermyon.app"), - "Expected allowed_http_hosts parse error to contain `ftp://random-data-api.fermyon.app`" - ); - assert!( - e.contains("example.com/wib/wob"), - "Expected allowed_http_hosts parse error to contain `example.com/wib/wob`" - ); - - Ok(()) -} diff --git a/crates/trigger/Cargo.toml b/crates/trigger/Cargo.toml index b2b3132f4a..39da32c37d 100644 --- a/crates/trigger/Cargo.toml +++ b/crates/trigger/Cargo.toml @@ -4,6 +4,11 @@ version = { workspace = true } authors = { workspace = true } edition = { workspace = true } +[features] +llm = ["spin-llm-local"] +llm-metal = ["llm", "spin-llm-local/metal"] +llm-cublas = ["llm", "spin-llm-local/cublas"] + [dependencies] anyhow = "1.0" async-trait = "0.1" @@ -45,11 +50,13 @@ wasmtime = { workspace = true } spin-componentize = { workspace = true } [dev-dependencies] +libtest-mimic = "0.6.1" +snapbox = "0.4.12" tempfile = "3.3.0" toml = "0.5" tokio = { version = "1.23", features = ["rt", "macros"] } -[features] -llm = ["spin-llm-local"] -llm-metal = ["llm", "spin-llm-local/metal"] -llm-cublas = ["llm", "spin-llm-local/cublas"] +[[test]] +name = "ui" +path = "tests/ui.rs" +harness = false \ No newline at end of file diff --git a/crates/trigger/src/locked.rs b/crates/trigger/src/locked.rs index fa5a4b37f4..7b4274dd63 100644 --- a/crates/trigger/src/locked.rs +++ b/crates/trigger/src/locked.rs @@ -267,77 +267,4 @@ mod tests { let mount_path = url::Url::try_from(mount).unwrap().to_file_path().unwrap(); assert!(mount_path.is_dir(), "{mount:?} is not a dir"); } - - #[tokio::test] - async fn lock_preserves_built_in_trigger_settings() { - let temp_dir = tempfile::tempdir().unwrap(); - let dir = temp_dir.path(); - - let base_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/triggers"); - let app = spin_loader::from_file(base_dir.join("http.toml"), Some(dir)) - .await - .unwrap(); - let locked = build_locked_app(app, dir).unwrap(); - - assert_eq!("http", locked.metadata["trigger"]["type"]); - assert_eq!("/test", locked.metadata["trigger"]["base"]); - - let tspin = locked - .triggers - .iter() - .find(|t| t.id == "trigger--http-spin") - .unwrap(); - assert_eq!("http", tspin.trigger_type); - assert_eq!("http-spin", tspin.trigger_config["component"]); - assert_eq!("/hello/...", tspin.trigger_config["route"]); - - let twagi = locked - .triggers - .iter() - .find(|t| t.id == "trigger--http-wagi") - .unwrap(); - assert_eq!("http", twagi.trigger_type); - assert_eq!("http-wagi", twagi.trigger_config["component"]); - assert_eq!("/waggy/...", twagi.trigger_config["route"]); - } - - #[tokio::test] - async fn lock_preserves_unknown_trigger_settings() { - let temp_dir = tempfile::tempdir().unwrap(); - let dir = temp_dir.path(); - - let base_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/triggers"); - let app = spin_loader::from_file(base_dir.join("pounce.toml"), Some(dir)) - .await - .unwrap(); - let locked = build_locked_app(app, dir).unwrap(); - - assert_eq!("pounce", locked.metadata["trigger"]["type"]); - assert_eq!("hobbes", locked.metadata["trigger"]["attacker"]); - assert_eq!(1, locked.metadata["trigger"]["attackers-age"]); - - // Distinct settings make it across okay - let t1 = locked - .triggers - .iter() - .find(|t| t.id == "trigger--conf1") - .unwrap(); - assert_eq!("pounce", t1.trigger_type); - assert_eq!("conf1", t1.trigger_config["component"]); - assert_eq!("MY KNEES", t1.trigger_config["on"]); - assert_eq!(7, t1.trigger_config["sharpness"]); - - // Settings that could be mistaken for built-in make is across okay - let t2 = locked - .triggers - .iter() - .find(|t| t.id == "trigger--conf2") - .unwrap(); - assert_eq!("pounce", t2.trigger_type); - assert_eq!("conf2", t2.trigger_config["component"]); - assert_eq!( - "over the cat tree and out of the sun", - t2.trigger_config["route"] - ); - } } diff --git a/crates/trigger/tests/ui.rs b/crates/trigger/tests/ui.rs new file mode 100644 index 0000000000..d58e24da39 --- /dev/null +++ b/crates/trigger/tests/ui.rs @@ -0,0 +1,133 @@ +use std::{ffi::OsStr, path::Path}; + +use anyhow::Context; +use futures::Future; +use libtest_mimic::{Arguments, Failed, Trial}; +use snapbox::{Action, Assert, Data, Normalize}; +use spin_loader::cache::Cache; + +fn main() -> anyhow::Result<()> { + let args = Arguments::from_args(); + + // Insert dummy wasm into cache to avoid network traffic + block_on(async { + let cache = Cache::new(None).await.expect("Cache::new to work"); + cache + .write_wasm(b"", "0000") + .await + .expect("write_wasms to work"); + }); + + let mut tests = vec![]; + let dir = Path::new("tests/ui").canonicalize()?; + for entry in std::fs::read_dir(dir)? { + let mut path = entry?.path(); + let test_name = path + .file_stem() + .context("file_stem")? + .to_string_lossy() + .to_string(); + + // */spin.toml are tests too! + if path.is_dir() { + path = path.join("spin.toml"); + if !path.exists() { + continue; + } + } else if path.extension() != Some(OsStr::new("toml")) { + continue; + } + let snapshot_path = path.with_extension("lock"); + + tests.push(Trial::test(format!("ui::{test_name}"), move || { + run_test(&path, &snapshot_path) + })); + } + + let conclusion = libtest_mimic::run(&args, tests); + if conclusion.has_failed() { + eprintln!("Snapshot files can be automatically updated by re-running with BLESS=1"); + } + conclusion.exit(); +} + +fn run_test(manifest_path: &Path, snapshot_path: &Path) -> Result<(), Failed> { + let snapshot_path = snapshot_path.to_path_buf(); + let temp_dir = tempfile::tempdir()?; + let temp_path = temp_dir.path().canonicalize()?; + + let result = block_on(async { + let app = spin_loader::from_file(manifest_path, Some(&temp_path)).await?; + let locked = spin_trigger::locked::build_locked_app(app, &temp_path)?; + Ok(serde_json::to_string_pretty(&locked)?) + }) + .map_err(|err: anyhow::Error| format!("{err:?}")); + + let normalize = NormalizeContentPaths::new(manifest_path, &temp_path)?; + assert_snapshot(result, &snapshot_path, normalize)?; + Ok(()) +} + +fn assert_snapshot( + result: Result, String>, + snapshot_path: &Path, + normalize: impl Normalize, +) -> Result<(), String> { + // If BLESS env is set (non-empty), overwrite snapshot files + let bless = !std::env::var_os("BLESS").unwrap_or_default().is_empty(); + let assert = Assert::new().action(if bless { + Action::Overwrite + } else { + Action::Verify + }); + + let mut snapshot_path = snapshot_path.to_path_buf(); + let contents = match result { + Ok(data) => data.into(), + Err(err) => { + snapshot_path = snapshot_path.with_extension("err"); + if !bless && !snapshot_path.exists() { + return Err(err); + } + err.into() + } + } + .normalize(normalize); + assert.eq_path(snapshot_path, contents); + Ok(()) +} + +struct NormalizeContentPaths { + root_dir: String, + temp_dir: String, +} + +impl NormalizeContentPaths { + fn new(manifest_path: &Path, temp_dir: &Path) -> anyhow::Result { + let root_dir = manifest_path + .parent() + .context("manifest_path parent")? + .to_str() + .context("root_dir to_str")? + .to_string(); + let temp_dir = temp_dir.to_str().context("temp_dir to_str")?.to_string(); + Ok(Self { root_dir, temp_dir }) + } +} + +impl Normalize for NormalizeContentPaths { + fn normalize(&self, data: Data) -> Data { + data.to_string() + .replace(&self.root_dir, "") + .replace(&self.temp_dir, "") + .into() + } +} + +fn block_on(fut: impl Future) -> T { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .expect("tokio runtime builder should work") + .block_on(fut) +} diff --git a/crates/trigger/tests/ui/insecure-allow-all-with-invalid-url.lock b/crates/trigger/tests/ui/insecure-allow-all-with-invalid-url.lock new file mode 100644 index 0000000000..783b41d341 --- /dev/null +++ b/crates/trigger/tests/ui/insecure-allow-all-with-invalid-url.lock @@ -0,0 +1,41 @@ +{ + "spin_lock_version": 0, + "metadata": { + "name": "spin-hello-world-duplicate", + "origin": "file:///insecure-allow-all-with-invalid-url.toml", + "trigger": { + "base": "/", + "type": "http" + }, + "version": "1.0.0" + }, + "triggers": [ + { + "id": "trigger--hello", + "trigger_type": "http", + "trigger_config": { + "component": "hello", + "executor": null, + "route": "/hello" + } + } + ], + "components": [ + { + "id": "hello", + "metadata": { + "ai_models": [], + "allowed_http_hosts": [ + "insecure:allow-all", + "random-data-api.fermyon.app" + ], + "databases": [], + "key_value_stores": [] + }, + "source": { + "content_type": "application/wasm", + "source": "file:///path/to/wasm/file.wasm" + } + } + ] +} \ No newline at end of file diff --git a/crates/loader/tests/insecure-allow-all-with-invalid-url.toml b/crates/trigger/tests/ui/insecure-allow-all-with-invalid-url.toml similarity index 100% rename from crates/loader/tests/insecure-allow-all-with-invalid-url.toml rename to crates/trigger/tests/ui/insecure-allow-all-with-invalid-url.toml diff --git a/crates/trigger/tests/ui/invalid-http-base.err b/crates/trigger/tests/ui/invalid-http-base.err new file mode 100644 index 0000000000..2dc8bf5928 --- /dev/null +++ b/crates/trigger/tests/ui/invalid-http-base.err @@ -0,0 +1,4 @@ +Cannot read spin.toml manifest from "/invalid-http-base.toml" + +Caused by: + could not load application trigger parameters: invalid type: integer `7`, expected a string for key `base` at line 11 column 1 \ No newline at end of file diff --git a/crates/loader/tests/invalid-http-base.toml b/crates/trigger/tests/ui/invalid-http-base.toml similarity index 100% rename from crates/loader/tests/invalid-http-base.toml rename to crates/trigger/tests/ui/invalid-http-base.toml diff --git a/crates/trigger/tests/ui/invalid-manifest-duplicate-id.err b/crates/trigger/tests/ui/invalid-manifest-duplicate-id.err new file mode 100644 index 0000000000..4f492a06d1 --- /dev/null +++ b/crates/trigger/tests/ui/invalid-manifest-duplicate-id.err @@ -0,0 +1 @@ +cannot have duplicate component IDs: hello \ No newline at end of file diff --git a/crates/loader/tests/invalid-manifest-duplicate-id.toml b/crates/trigger/tests/ui/invalid-manifest-duplicate-id.toml similarity index 100% rename from crates/loader/tests/invalid-manifest-duplicate-id.toml rename to crates/trigger/tests/ui/invalid-manifest-duplicate-id.toml diff --git a/crates/trigger/tests/ui/invalid-manifest.err b/crates/trigger/tests/ui/invalid-manifest.err new file mode 100644 index 0000000000..b6d2958cde --- /dev/null +++ b/crates/trigger/tests/ui/invalid-manifest.err @@ -0,0 +1,4 @@ +Cannot read spin.toml manifest from "/invalid-manifest.toml" + +Caused by: + expected an equals, found a newline at line 6 column 4 \ No newline at end of file diff --git a/crates/loader/tests/invalid-manifest.toml b/crates/trigger/tests/ui/invalid-manifest.toml similarity index 100% rename from crates/loader/tests/invalid-manifest.toml rename to crates/trigger/tests/ui/invalid-manifest.toml diff --git a/crates/trigger/tests/ui/invalid-url-in-allowed-http-hosts.err b/crates/trigger/tests/ui/invalid-url-in-allowed-http-hosts.err new file mode 100644 index 0000000000..6ade5dfb73 --- /dev/null +++ b/crates/trigger/tests/ui/invalid-url-in-allowed-http-hosts.err @@ -0,0 +1,3 @@ +One or more allowed_http_hosts entries was invalid: +ftp://random-data-api.fermyon.app isn't a valid host or host:port string +example.com/wib/wob contains a path, should be host and optional port only \ No newline at end of file diff --git a/crates/loader/tests/invalid-url-in-allowed-http-hosts.toml b/crates/trigger/tests/ui/invalid-url-in-allowed-http-hosts.toml similarity index 100% rename from crates/loader/tests/invalid-url-in-allowed-http-hosts.toml rename to crates/trigger/tests/ui/invalid-url-in-allowed-http-hosts.toml diff --git a/crates/trigger/tests/ui/invalid-version.err b/crates/trigger/tests/ui/invalid-version.err new file mode 100644 index 0000000000..d1f3488217 --- /dev/null +++ b/crates/trigger/tests/ui/invalid-version.err @@ -0,0 +1,4 @@ +Cannot read spin.toml manifest from "/invalid-version.toml" + +Caused by: + invalid version: invalid digit found in string for key `spin_version` at line 11 column 1 \ No newline at end of file diff --git a/crates/loader/tests/invalid-version.toml b/crates/trigger/tests/ui/invalid-version.toml similarity index 100% rename from crates/loader/tests/invalid-version.toml rename to crates/trigger/tests/ui/invalid-version.toml diff --git a/crates/trigger/tests/ui/valid-manifest.lock b/crates/trigger/tests/ui/valid-manifest.lock new file mode 100644 index 0000000000..d0903413d2 --- /dev/null +++ b/crates/trigger/tests/ui/valid-manifest.lock @@ -0,0 +1,95 @@ +{ + "spin_lock_version": 0, + "metadata": { + "description": "A simple application that returns the number of lights", + "name": "chain-of-command", + "origin": "file:///valid-manifest.toml", + "trigger": { + "base": "/", + "type": "http" + }, + "version": "6.11.2" + }, + "triggers": [ + { + "id": "trigger--four-lights", + "trigger_type": "http", + "trigger_config": { + "component": "four-lights", + "executor": { + "type": "spin" + }, + "route": "/lights" + } + }, + { + "id": "trigger--this used to test Bindle but is no longer used; kept around because some tests index into the component array", + "trigger_type": "http", + "trigger_config": { + "component": "this used to test Bindle but is no longer used; kept around because some tests index into the component array", + "executor": null, + "route": "/test" + } + }, + { + "id": "trigger--web", + "trigger_type": "http", + "trigger_config": { + "component": "web", + "executor": null, + "route": "/dont/test" + } + } + ], + "components": [ + { + "id": "four-lights", + "metadata": { + "ai_models": [], + "allowed_http_hosts": [], + "databases": [], + "key_value_stores": [] + }, + "source": { + "content_type": "application/wasm", + "source": "file:///wasm/dummy.wasm" + }, + "env": { + "env1": "first", + "env2": "second" + }, + "files": [ + { + "source": "file:///assets/four-lights_7318449cb86f5233dcc7c8f0f8b5812f3aec6de0bd26d6ee4b1edadb817da337", + "path": "/" + } + ] + }, + { + "id": "this used to test Bindle but is no longer used; kept around because some tests index into the component array", + "metadata": { + "ai_models": [], + "allowed_http_hosts": [], + "databases": [], + "key_value_stores": [] + }, + "source": { + "content_type": "application/wasm", + "source": "file:///nope/nope/nope" + } + }, + { + "id": "web", + "metadata": { + "ai_models": [], + "allowed_http_hosts": [], + "databases": [], + "key_value_stores": [] + }, + "source": { + "content_type": "application/wasm", + "source": "file:///web.wasm" + } + } + ] +} \ No newline at end of file diff --git a/crates/loader/tests/valid-manifest.toml b/crates/trigger/tests/ui/valid-manifest.toml similarity index 93% rename from crates/loader/tests/valid-manifest.toml rename to crates/trigger/tests/ui/valid-manifest.toml index 98016e2f3a..75e508af30 100644 --- a/crates/loader/tests/valid-manifest.toml +++ b/crates/trigger/tests/ui/valid-manifest.toml @@ -8,7 +8,7 @@ version = "6.11.2" [[component]] files = ["file.txt", { source = "valid-with-files", destination = "/vwf" }, "subdir/another.txt"] id = "four-lights" -source = "path/to/wasm/file.wasm" +source = "wasm/dummy.wasm" [component.trigger] executor = {type = "spin"} route = "/lights" @@ -26,6 +26,6 @@ route = "/test" id = "web" [component.source] url = "https://example.com/wasm.wasm.wasm" -digest = "sha256:12345" +digest = "sha256:0000" [component.trigger] route = "/dont/test" diff --git a/crates/loader/tests/valid-with-files/spin-fs.wasm b/crates/trigger/tests/ui/valid-with-files/spin-fs.wasm similarity index 100% rename from crates/loader/tests/valid-with-files/spin-fs.wasm rename to crates/trigger/tests/ui/valid-with-files/spin-fs.wasm diff --git a/crates/trigger/tests/ui/valid-with-files/spin.lock b/crates/trigger/tests/ui/valid-with-files/spin.lock new file mode 100644 index 0000000000..08c058eda0 --- /dev/null +++ b/crates/trigger/tests/ui/valid-with-files/spin.lock @@ -0,0 +1,46 @@ +{ + "spin_lock_version": 0, + "metadata": { + "name": "spin-local-source-test", + "origin": "file:///spin.toml", + "trigger": { + "base": "/", + "type": "http" + }, + "version": "1.0.0" + }, + "triggers": [ + { + "id": "trigger--fs", + "trigger_type": "http", + "trigger_config": { + "component": "fs", + "executor": { + "type": "spin" + }, + "route": "/..." + } + } + ], + "components": [ + { + "id": "fs", + "metadata": { + "ai_models": [], + "allowed_http_hosts": [], + "databases": [], + "key_value_stores": [] + }, + "source": { + "content_type": "application/wasm", + "source": "file:///spin-fs.wasm" + }, + "files": [ + { + "source": "file:///assets/fs_dce7cce055566bed799f788cd0048e209a27a473c0f48b956fa1f1780e80d2c1", + "path": "/" + } + ] + } + ] +} \ No newline at end of file diff --git a/crates/loader/tests/valid-with-files/spin.toml b/crates/trigger/tests/ui/valid-with-files/spin.toml similarity index 100% rename from crates/loader/tests/valid-with-files/spin.toml rename to crates/trigger/tests/ui/valid-with-files/spin.toml diff --git a/crates/loader/tests/valid-with-files/static/alphabet/a b/crates/trigger/tests/ui/valid-with-files/static/alphabet/a similarity index 100% rename from crates/loader/tests/valid-with-files/static/alphabet/a rename to crates/trigger/tests/ui/valid-with-files/static/alphabet/a diff --git a/crates/loader/tests/valid-with-files/static/alphabet/b b/crates/trigger/tests/ui/valid-with-files/static/alphabet/b similarity index 100% rename from crates/loader/tests/valid-with-files/static/alphabet/b rename to crates/trigger/tests/ui/valid-with-files/static/alphabet/b diff --git a/crates/loader/tests/valid-with-files/static/alphabet/c b/crates/trigger/tests/ui/valid-with-files/static/alphabet/c similarity index 100% rename from crates/loader/tests/valid-with-files/static/alphabet/c rename to crates/trigger/tests/ui/valid-with-files/static/alphabet/c diff --git a/crates/loader/tests/valid-with-files/static/numbers/1 b/crates/trigger/tests/ui/valid-with-files/static/numbers/1 similarity index 100% rename from crates/loader/tests/valid-with-files/static/numbers/1 rename to crates/trigger/tests/ui/valid-with-files/static/numbers/1 diff --git a/crates/loader/tests/valid-with-files/static/numbers/2 b/crates/trigger/tests/ui/valid-with-files/static/numbers/2 similarity index 100% rename from crates/loader/tests/valid-with-files/static/numbers/2 rename to crates/trigger/tests/ui/valid-with-files/static/numbers/2 diff --git a/crates/loader/tests/valid-with-files/static/numbers/3 b/crates/trigger/tests/ui/valid-with-files/static/numbers/3 similarity index 100% rename from crates/loader/tests/valid-with-files/static/numbers/3 rename to crates/trigger/tests/ui/valid-with-files/static/numbers/3 diff --git a/crates/trigger/tests/ui/wagi-custom-entrypoint.lock b/crates/trigger/tests/ui/wagi-custom-entrypoint.lock new file mode 100644 index 0000000000..0ba0e8cea7 --- /dev/null +++ b/crates/trigger/tests/ui/wagi-custom-entrypoint.lock @@ -0,0 +1,48 @@ +{ + "spin_lock_version": 0, + "metadata": { + "name": "spin-wagi-custom-entrypoint", + "origin": "file:///wagi-custom-entrypoint.toml", + "trigger": { + "base": "/", + "type": "http" + }, + "version": "1.0.0" + }, + "triggers": [ + { + "id": "trigger--fs", + "trigger_type": "http", + "trigger_config": { + "component": "fs", + "executor": { + "argv": "${SCRIPT_NAME} ${ARGS}", + "entrypoint": "custom-entrypoint", + "type": "wagi" + }, + "route": "/hello" + } + } + ], + "components": [ + { + "id": "fs", + "metadata": { + "ai_models": [], + "allowed_http_hosts": [], + "databases": [], + "key_value_stores": [] + }, + "source": { + "content_type": "application/wasm", + "source": "file:///spin-fs.wasm" + }, + "files": [ + { + "source": "file:///assets/fs_dce7cce055566bed799f788cd0048e209a27a473c0f48b956fa1f1780e80d2c1", + "path": "/" + } + ] + } + ] +} \ No newline at end of file diff --git a/crates/loader/tests/wagi-custom-entrypoint.toml b/crates/trigger/tests/ui/wagi-custom-entrypoint.toml similarity index 100% rename from crates/loader/tests/wagi-custom-entrypoint.toml rename to crates/trigger/tests/ui/wagi-custom-entrypoint.toml diff --git a/crates/trigger/tests/ui/wasm/dummy.wasm b/crates/trigger/tests/ui/wasm/dummy.wasm new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/trigger/valid-manifest.lock b/crates/trigger/valid-manifest.lock new file mode 100644 index 0000000000..ad29d131c7 --- /dev/null +++ b/crates/trigger/valid-manifest.lock @@ -0,0 +1,95 @@ +{ + "spin_lock_version": 0, + "metadata": { + "description": "A simple application that returns the number of lights", + "name": "chain-of-command", + "origin": "file:///valid-manifest.toml", + "trigger": { + "base": "/", + "type": "http" + }, + "version": "6.11.2" + }, + "triggers": [ + { + "id": "trigger--four-lights", + "trigger_type": "http", + "trigger_config": { + "component": "four-lights", + "executor": { + "type": "spin" + }, + "route": "/lights" + } + }, + { + "id": "trigger--this used to test Bindle but is no longer used; kept around because some tests index into the component array", + "trigger_type": "http", + "trigger_config": { + "component": "this used to test Bindle but is no longer used; kept around because some tests index into the component array", + "executor": null, + "route": "/test" + } + }, + { + "id": "trigger--web", + "trigger_type": "http", + "trigger_config": { + "component": "web", + "executor": null, + "route": "/dont/test" + } + } + ], + "components": [ + { + "id": "four-lights", + "metadata": { + "ai_models": [], + "allowed_http_hosts": [], + "databases": [], + "key_value_stores": [] + }, + "source": { + "content_type": "application/wasm", + "source": "file:///wasm/dummy.wasm" + }, + "env": { + "env1": "first", + "env2": "second" + }, + "files": [ + { + "source": "file:///assets/four-lights_7318449cb86f5233dcc7c8f0f8b5812f3aec6de0bd26d6ee4b1edadb817da337", + "path": "/" + } + ] + }, + { + "id": "this used to test Bindle but is no longer used; kept around because some tests index into the component array", + "metadata": { + "ai_models": [], + "allowed_http_hosts": [], + "databases": [], + "key_value_stores": [] + }, + "source": { + "content_type": "application/wasm", + "source": "file:///nope/nope/nope" + } + }, + { + "id": "web", + "metadata": { + "ai_models": [], + "allowed_http_hosts": [], + "databases": [], + "key_value_stores": [] + }, + "source": { + "content_type": "application/wasm", + "source": "file:///web.wasm" + } + } + ] +} \ No newline at end of file