//! Integration tests for wasmbox CLI. //! //! These tests stand up a mock registry (wiremock), run the wasmbox binary //! against it, and verify the install → run → verify → update → remove flow. use std::path::{Path, PathBuf}; use std::process::{Command, Output}; use tempfile::TempDir; use wiremock::matchers::{method, path}; use wiremock::{Mock, MockServer, ResponseTemplate}; // --- Helpers --- fn wasmbox_bin() -> PathBuf { PathBuf::from(env!("CARGO_BIN_EXE_wasmbox")) } fn fixture_path(name: &str) -> PathBuf { // tests/fixtures/ is at workspace root Path::new(env!("CARGO_MANIFEST_DIR")) .join("../../tests/fixtures") .join(name) } fn fixture_bytes(name: &str) -> Vec { std::fs::read(fixture_path(name)).expect("read fixture") } fn fixture_hash(name: &str) -> String { wasmbox_verify::hash_file(&fixture_path(name)).expect("hash fixture") } /// Run wasmbox with --home pointing at a temp dir. Returns (Output, home_dir). fn run_wasmbox(home: &Path, args: &[&str]) -> Output { Command::new(wasmbox_bin()) .arg("--home") .arg(home) .args(args) .env("NO_COLOR", "1") .output() .expect("failed to execute wasmbox") } /// Build a wasmbox.toml manifest string for a test tool. fn build_manifest(name: &str, version: &str, wasm_file: &str, hash: &str) -> String { format!( r#"[tool] name = "{name}" version = "{version}" description = "A test tool" author = "Test Author" license = "MIT" [binary] wasm = "{wasm_file}" hash = "{hash}" [capabilities] stdout = true [ui] type = "cli" "#, ) } /// Build a registry index.json response. fn build_index(entries: &[(&str, &str, &str, u64)]) -> String { let tools: Vec = entries .iter() .map(|(name, version, hash, size)| { format!( r#" {{ "name": "{name}", "version": "{version}", "description": "A test tool", "hash": "{hash}", "size": {size}, "categories": ["test"] }}"#, ) }) .collect(); format!( r#"{{ "registry": "test-registry", "tools": [ {} ] }}"#, tools.join(",\n"), ) } /// Set up a mock registry serving a single tool. async fn setup_mock_registry( server: &MockServer, tool_name: &str, version: &str, wasm_fixture: &str, ) -> String { let wasm_bytes = fixture_bytes(wasm_fixture); let hash = fixture_hash(wasm_fixture); let manifest = build_manifest(tool_name, version, &format!("{tool_name}.wasm"), &hash); let index = build_index(&[(tool_name, version, &hash, wasm_bytes.len() as u64)]); Mock::given(method("GET")) .and(path("/index.json")) .respond_with(ResponseTemplate::new(200).set_body_string(&index)) .mount(server) .await; Mock::given(method("GET")) .and(path(format!("/tools/{tool_name}.json"))) .respond_with(ResponseTemplate::new(200).set_body_string(&manifest)) .mount(server) .await; Mock::given(method("GET")) .and(path(format!("/tools/{tool_name}.wasm"))) .respond_with(ResponseTemplate::new(200).set_body_bytes(wasm_bytes)) .mount(server) .await; hash } /// Configure a registry URL in the wasmbox home. fn configure_registry(home: &Path, url: &str) { std::fs::create_dir_all(home).expect("create home"); let config = format!("[[registries]]\nurl = \"{url}\"\n"); std::fs::write(home.join("config.toml"), config).expect("write config"); } /// Install a tool and return the Output. fn install_tool(home: &Path, name: &str, registry_url: &str) -> Output { run_wasmbox(home, &["install", name, "--registry", registry_url]) } // --- Tests --- #[tokio::test] async fn install_from_registry() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); let output = install_tool(home_path, "hello-tool", &server.uri()); assert!( output.status.success(), "install failed: {}", String::from_utf8_lossy(&output.stderr) ); // Verify files on disk let tool_dir = home_path.join("cache/hello-tool/0.1.0"); assert!(tool_dir.exists(), "tool directory not created"); assert!( tool_dir.join("wasmbox.toml").exists(), "manifest not written" ); assert!( tool_dir.join("hello-tool.wasm").exists(), "wasm not written" ); } #[tokio::test] async fn list_shows_installed_tool() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); let output = run_wasmbox(home_path, &["list", "--json"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let tools: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON"); let tools = tools.as_array().expect("should be array"); assert_eq!(tools.len(), 1); assert_eq!(tools[0]["name"], "hello-tool"); assert_eq!(tools[0]["version"], "0.1.0"); } #[tokio::test] async fn run_installed_tool() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Run with --allow stdout (tool outputs "Hello from WasmBox!") let output = run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); assert!( output.status.success(), "run failed: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("Hello from WasmBox!"), "expected hello output, got: {stdout}" ); } #[tokio::test] async fn run_with_sandbox_zero_capabilities() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Run with --sandbox: tool gets zero capabilities, no stdout let output = run_wasmbox(home_path, &["run", "hello-tool", "--sandbox"]); assert!( output.status.success(), "sandbox run failed: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); assert!( !stdout.contains("Hello from WasmBox!"), "sandbox should suppress stdout, got: {stdout}" ); } #[tokio::test] async fn verify_installed_tool_passes() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); let output = run_wasmbox(home_path, &["verify", "hello-tool"]); assert!( output.status.success(), "verify failed: {}", String::from_utf8_lossy(&output.stderr) ); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("verified"), "expected 'verified' in output"); } #[tokio::test] async fn verify_tampered_binary_fails() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Tamper with the wasm binary let wasm_path = home_path.join("cache/hello-tool/0.1.0/hello-tool.wasm"); std::fs::write(&wasm_path, b"tampered content").unwrap(); let output = run_wasmbox(home_path, &["verify", "hello-tool"]); assert!( !output.status.success(), "verify should fail on tampered binary" ); assert_eq!( output.status.code(), Some(3), "exit code should be 3 for verification failure" ); } #[tokio::test] async fn run_tampered_binary_fails() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Tamper with the wasm binary let wasm_path = home_path.join("cache/hello-tool/0.1.0/hello-tool.wasm"); std::fs::write(&wasm_path, b"tampered content").unwrap(); let output = run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); assert!( !output.status.success(), "run should fail on tampered binary" ); assert_eq!( output.status.code(), Some(3), "exit code should be 3 for verification failure" ); } #[tokio::test] async fn install_with_allow_all_stores_permissions() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); let output = run_wasmbox( home_path, &[ "install", "hello-tool", "--registry", &server.uri(), "--allow-all", ], ); assert!( output.status.success(), "install --allow-all failed: {}", String::from_utf8_lossy(&output.stderr) ); // Check permissions file exists and has the tool let perm_path = home_path.join("permissions.toml"); assert!(perm_path.exists(), "permissions.toml not created"); let output = run_wasmbox(home_path, &["permissions", "hello-tool", "show", "--json"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let perms: serde_json::Value = serde_json::from_str(&stdout).expect("parse perms JSON"); assert_eq!(perms["stdout"], true, "stdout should be granted"); } #[tokio::test] async fn run_without_tty_no_permissions_fails() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Run without --allow, --sandbox, or --allow-all. // Since tool requests stdout and there's no TTY for prompting, should fail. let output = run_wasmbox(home_path, &["run", "hello-tool"]); assert!( !output.status.success(), "should fail without TTY or --allow" ); assert_eq!( output.status.code(), Some(2), "exit code should be 2 for permission denied" ); } #[tokio::test] async fn info_shows_tool_metadata() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); let output = run_wasmbox(home_path, &["info", "hello-tool", "--json"]); assert!( output.status.success(), "info failed: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); let info: serde_json::Value = serde_json::from_str(&stdout).expect("parse info JSON"); assert_eq!(info["name"], "hello-tool"); assert_eq!(info["version"], "0.1.0"); assert_eq!(info["author"], "Test Author"); } #[tokio::test] async fn search_finds_tool() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); let output = run_wasmbox(home_path, &["search", "hello", "--json"]); assert!( output.status.success(), "search failed: {}", String::from_utf8_lossy(&output.stderr) ); let stdout = String::from_utf8_lossy(&output.stdout); let results: serde_json::Value = serde_json::from_str(&stdout).expect("parse search JSON"); let results = results.as_array().expect("should be array"); assert!(!results.is_empty(), "search should find hello-tool"); assert_eq!(results[0]["name"], "hello-tool"); } #[tokio::test] async fn search_no_results() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); let output = run_wasmbox(home_path, &["search", "nonexistent", "--json"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let results: serde_json::Value = serde_json::from_str(&stdout).expect("parse search JSON"); let results = results.as_array().expect("should be array"); assert!( results.is_empty(), "search should return empty for nonexistent" ); } #[tokio::test] async fn remove_tool() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Verify installed assert!(home_path.join("cache/hello-tool/0.1.0").exists()); let output = run_wasmbox(home_path, &["remove", "hello-tool"]); assert!( output.status.success(), "remove failed: {}", String::from_utf8_lossy(&output.stderr) ); // Verify removed assert!( !home_path.join("cache/hello-tool").exists(), "tool directory should be removed" ); } #[tokio::test] async fn remove_nonexistent_tool_fails() { let home = TempDir::new().unwrap(); let home_path = home.path(); std::fs::create_dir_all(home_path.join("cache")).unwrap(); let output = run_wasmbox(home_path, &["remove", "nonexistent"]); assert!( !output.status.success(), "remove of nonexistent should fail" ); } #[tokio::test] async fn update_to_newer_version() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); // Install v0.1.0 with hello.wasm setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Reset mocks and set up v0.2.0 with echo.wasm (different binary + hash) server.reset().await; setup_mock_registry(&server, "hello-tool", "0.2.0", "echo.wasm").await; let output = run_wasmbox(home_path, &["update", "hello-tool"]); assert!( output.status.success(), "update failed: {}", String::from_utf8_lossy(&output.stderr) ); // Both versions should exist (old kept for rollback) assert!( home_path.join("cache/hello-tool/0.1.0").exists(), "old version should be kept for rollback" ); assert!( home_path.join("cache/hello-tool/0.2.0").exists(), "new version should be installed" ); } #[tokio::test] async fn update_already_latest() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Update when already at latest let output = run_wasmbox(home_path, &["update", "hello-tool"]); assert!(output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("up to date"), "should say already up to date, got: {stderr}" ); } #[tokio::test] async fn revoke_capability() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); // Install with pre-approved permissions run_wasmbox( home_path, &[ "install", "hello-tool", "--registry", &server.uri(), "--allow-all", ], ); // Revoke stdout let output = run_wasmbox(home_path, &["revoke", "hello-tool", "stdout"]); assert!( output.status.success(), "revoke failed: {}", String::from_utf8_lossy(&output.stderr) ); // Verify stdout is revoked let output = run_wasmbox(home_path, &["permissions", "hello-tool", "show", "--json"]); let stdout = String::from_utf8_lossy(&output.stdout); let perms: serde_json::Value = serde_json::from_str(&stdout).expect("parse perms JSON"); assert_eq!(perms["stdout"], false, "stdout should be revoked"); } #[tokio::test] async fn audit_shows_all_permissions() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); run_wasmbox( home_path, &[ "install", "hello-tool", "--registry", &server.uri(), "--allow-all", ], ); let output = run_wasmbox(home_path, &["audit", "--json"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let audit: serde_json::Value = serde_json::from_str(&stdout).expect("parse audit JSON"); assert!( audit.get("hello-tool").is_some(), "audit should show hello-tool permissions" ); } #[tokio::test] async fn hash_command() { let wasm_path = fixture_path("hello.wasm"); let home = TempDir::new().unwrap(); let output = run_wasmbox(home.path(), &["hash", wasm_path.to_str().unwrap()]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); assert!( stdout.starts_with("sha256:"), "hash should start with sha256:" ); assert_eq!( stdout.len(), 7 + 64, "hash should be sha256: + 64 hex chars" ); } #[tokio::test] async fn install_nonexistent_tool_fails() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); // Empty registry Mock::given(method("GET")) .and(path("/index.json")) .respond_with( ResponseTemplate::new(200).set_body_string(r#"{"registry":"test","tools":[]}"#), ) .mount(&server) .await; configure_registry(home_path, &server.uri()); let output = install_tool(home_path, "nonexistent", &server.uri()); assert!( !output.status.success(), "install of nonexistent should fail" ); } #[tokio::test] async fn registry_add_list_remove() { let home = TempDir::new().unwrap(); let home_path = home.path(); // Add registry let output = run_wasmbox(home_path, &["registry", "add", "https://example.com"]); assert!(output.status.success()); // List registries let output = run_wasmbox(home_path, &["registry", "list"]); assert!(output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("example.com"), "should list registry"); // Remove registry let output = run_wasmbox(home_path, &["registry", "remove", "https://example.com"]); assert!(output.status.success()); // Verify removed let output = run_wasmbox(home_path, &["registry", "list"]); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("no registries"), "should have no registries" ); } #[tokio::test] async fn run_local_file_with_sandbox() { let home = TempDir::new().unwrap(); let wasm_path = fixture_path("hello.wasm"); let output = run_wasmbox( home.path(), &["run", "--file", wasm_path.to_str().unwrap(), "--sandbox"], ); assert!( output.status.success(), "run --file failed: {}", String::from_utf8_lossy(&output.stderr) ); } #[tokio::test] async fn run_local_file_with_manifest_verifies_hash() { let home = TempDir::new().unwrap(); let home_path = home.path(); let wasm_path = fixture_path("hello.wasm"); let hash = fixture_hash("hello.wasm"); // Write a manifest that matches the wasm file let manifest = build_manifest("hello", "0.1.0", "hello.wasm", &hash); let manifest_path = home_path.join("test-manifest.toml"); std::fs::write(&manifest_path, manifest).unwrap(); let output = run_wasmbox( home_path, &[ "run", "--file", wasm_path.to_str().unwrap(), "--manifest", manifest_path.to_str().unwrap(), "--allow", "stdout", ], ); assert!( output.status.success(), "run with manifest failed: {}", String::from_utf8_lossy(&output.stderr) ); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("verified"), "should verify hash"); } #[tokio::test] async fn run_local_file_with_wrong_manifest_hash_fails() { let home = TempDir::new().unwrap(); let home_path = home.path(); let wasm_path = fixture_path("hello.wasm"); // Write a manifest with wrong hash let wrong_hash = "sha256:0000000000000000000000000000000000000000000000000000000000000000"; let manifest = build_manifest("hello", "0.1.0", "hello.wasm", wrong_hash); let manifest_path = home_path.join("test-manifest.toml"); std::fs::write(&manifest_path, manifest).unwrap(); let output = run_wasmbox( home_path, &[ "run", "--file", wasm_path.to_str().unwrap(), "--manifest", manifest_path.to_str().unwrap(), "--allow", "stdout", ], ); assert!(!output.status.success(), "should fail with wrong hash"); assert_eq!(output.status.code(), Some(3), "exit code should be 3"); } #[tokio::test] async fn install_verifies_hash_from_registry() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); let wasm_bytes = fixture_bytes("hello.wasm"); let wrong_hash = "sha256:0000000000000000000000000000000000000000000000000000000000000000"; // Registry says the hash is wrong — install should fail verification let manifest = build_manifest("hello-tool", "0.1.0", "hello-tool.wasm", wrong_hash); let index = build_index(&[("hello-tool", "0.1.0", wrong_hash, wasm_bytes.len() as u64)]); Mock::given(method("GET")) .and(path("/index.json")) .respond_with(ResponseTemplate::new(200).set_body_string(&index)) .mount(&server) .await; Mock::given(method("GET")) .and(path("/tools/hello-tool.json")) .respond_with(ResponseTemplate::new(200).set_body_string(&manifest)) .mount(&server) .await; Mock::given(method("GET")) .and(path("/tools/hello-tool.wasm")) .respond_with(ResponseTemplate::new(200).set_body_bytes(wasm_bytes)) .mount(&server) .await; configure_registry(home_path, &server.uri()); let output = install_tool(home_path, "hello-tool", &server.uri()); assert!( !output.status.success(), "install with bad hash should fail" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("verification failed"), "should mention verification failure, got: {stderr}" ); } #[tokio::test] async fn run_installed_tool_at_specific_version() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); // Install v0.1.0 with hello.wasm setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Install v0.2.0 with echo.wasm by doing an update server.reset().await; setup_mock_registry(&server, "hello-tool", "0.2.0", "echo.wasm").await; run_wasmbox(home_path, &["update", "hello-tool"]); // Run specific version (v0.1.0 should output "Hello from WasmBox!") let output = run_wasmbox(home_path, &["run", "hello-tool@0.1.0", "--allow", "stdout"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); assert!( stdout.contains("Hello from WasmBox!"), "v0.1.0 should output hello, got: {stdout}" ); } #[tokio::test] async fn permissions_revoke_all() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); run_wasmbox( home_path, &[ "install", "hello-tool", "--registry", &server.uri(), "--allow-all", ], ); // Revoke all let output = run_wasmbox(home_path, &["permissions", "hello-tool", "revoke"]); assert!(output.status.success()); // Verify permissions are gone let output = run_wasmbox(home_path, &["permissions", "hello-tool", "show"]); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("no permissions"), "should say no permissions, got: {stderr}" ); } // ============================================================================= // Edge Cases // ============================================================================= #[tokio::test] async fn run_corrupted_wasm_binary() { // A file that exists but isn't valid wasm should fail gracefully let home = TempDir::new().unwrap(); let home_path = home.path(); let bad_wasm = home_path.join("bad.wasm"); std::fs::write(&bad_wasm, b"this is not valid wasm").unwrap(); let output = run_wasmbox( home_path, &["run", "--file", bad_wasm.to_str().unwrap(), "--sandbox"], ); assert!(!output.status.success(), "corrupt wasm should fail"); assert_eq!( output.status.code(), Some(1), "should be general error, not verification" ); } #[tokio::test] async fn run_empty_wasm_file() { let home = TempDir::new().unwrap(); let home_path = home.path(); let empty_wasm = home_path.join("empty.wasm"); std::fs::write(&empty_wasm, b"").unwrap(); let output = run_wasmbox( home_path, &["run", "--file", empty_wasm.to_str().unwrap(), "--sandbox"], ); assert!(!output.status.success(), "empty wasm should fail"); } #[tokio::test] async fn run_missing_wasm_file() { let home = TempDir::new().unwrap(); let output = run_wasmbox( home.path(), &["run", "--file", "/nonexistent/path.wasm", "--sandbox"], ); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("not found"), "should report file not found, got: {stderr}" ); } #[tokio::test] async fn run_tool_with_missing_cache_wasm() { // Install a tool, then delete the .wasm but leave the manifest let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Delete the wasm file let wasm_path = home_path.join("cache/hello-tool/0.1.0/hello-tool.wasm"); std::fs::remove_file(&wasm_path).unwrap(); let output = run_wasmbox(home_path, &["run", "hello-tool", "--sandbox"]); assert!(!output.status.success(), "should fail with missing wasm"); } #[tokio::test] async fn run_tool_with_missing_manifest() { // Install a tool, then delete the manifest but leave the .wasm let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Delete the manifest let manifest_path = home_path.join("cache/hello-tool/0.1.0/wasmbox.toml"); std::fs::remove_file(&manifest_path).unwrap(); let output = run_wasmbox(home_path, &["run", "hello-tool", "--sandbox"]); assert!( !output.status.success(), "should fail with missing manifest" ); } #[tokio::test] async fn corrupt_config_toml() { let home = TempDir::new().unwrap(); let home_path = home.path(); std::fs::create_dir_all(home_path).unwrap(); // Write garbage config std::fs::write(home_path.join("config.toml"), "{{{{ not valid toml").unwrap(); // Commands that read config should fail gracefully let output = run_wasmbox(home_path, &["search", "anything"]); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.to_lowercase().contains("error"), "should report error for corrupt config, got: {stderr}" ); } #[tokio::test] async fn corrupt_permissions_toml() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Write garbage permissions std::fs::write(home_path.join("permissions.toml"), "not valid {{{{").unwrap(); // Audit should fail gracefully let output = run_wasmbox(home_path, &["audit"]); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.to_lowercase().contains("error"), "should report error, got: {stderr}" ); } #[tokio::test] async fn corrupt_manifest_in_cache() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Corrupt the cached manifest let manifest_path = home_path.join("cache/hello-tool/0.1.0/wasmbox.toml"); std::fs::write(&manifest_path, "garbage {{{{ not toml").unwrap(); let output = run_wasmbox(home_path, &["run", "hello-tool", "--sandbox"]); assert!( !output.status.success(), "corrupt manifest should cause failure" ); } #[tokio::test] async fn run_nonexistent_tool() { let home = TempDir::new().unwrap(); let home_path = home.path(); std::fs::create_dir_all(home_path.join("cache")).unwrap(); let output = run_wasmbox(home_path, &["run", "nonexistent-tool", "--sandbox"]); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("not found"), "should say tool not found, got: {stderr}" ); } #[tokio::test] async fn verify_nonexistent_tool() { let home = TempDir::new().unwrap(); let home_path = home.path(); std::fs::create_dir_all(home_path.join("cache")).unwrap(); let output = run_wasmbox(home_path, &["verify", "nonexistent-tool"]); assert!(!output.status.success()); } #[tokio::test] async fn info_nonexistent_tool() { let home = TempDir::new().unwrap(); let home_path = home.path(); std::fs::create_dir_all(home_path.join("cache")).unwrap(); let output = run_wasmbox(home_path, &["info", "nonexistent-tool"]); assert!(!output.status.success()); } #[tokio::test] async fn list_with_empty_cache() { let home = TempDir::new().unwrap(); let output = run_wasmbox(home.path(), &["list", "--json"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let tools: serde_json::Value = serde_json::from_str(&stdout).expect("parse JSON"); assert_eq!(tools.as_array().unwrap().len(), 0); } #[tokio::test] async fn list_with_no_cache_dir() { let home = TempDir::new().unwrap(); // Don't create any dirs — home exists from TempDir but no cache subdir let output = run_wasmbox(home.path(), &["list"]); assert!(output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("no tools"), "should say no tools installed, got: {stderr}" ); } #[tokio::test] async fn audit_with_no_permissions_file() { let home = TempDir::new().unwrap(); let output = run_wasmbox(home.path(), &["audit"]); assert!(output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("no permissions"), "should report no permissions, got: {stderr}" ); } #[tokio::test] async fn registry_server_returns_500() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); Mock::given(method("GET")) .and(path("/index.json")) .respond_with(ResponseTemplate::new(500)) .mount(&server) .await; configure_registry(home_path, &server.uri()); let output = install_tool(home_path, "any-tool", &server.uri()); assert!(!output.status.success()); } #[tokio::test] async fn registry_returns_invalid_json() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); Mock::given(method("GET")) .and(path("/index.json")) .respond_with(ResponseTemplate::new(200).set_body_string("not json at all")) .mount(&server) .await; configure_registry(home_path, &server.uri()); let output = install_tool(home_path, "any-tool", &server.uri()); assert!(!output.status.success()); } #[tokio::test] async fn registry_returns_empty_index() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); Mock::given(method("GET")) .and(path("/index.json")) .respond_with( ResponseTemplate::new(200).set_body_string(r#"{"registry":"test","tools":[]}"#), ) .mount(&server) .await; configure_registry(home_path, &server.uri()); let output = run_wasmbox(home_path, &["search", "anything", "--json"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let results: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert!(results.as_array().unwrap().is_empty()); } #[tokio::test] async fn install_when_registry_wasm_returns_404() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); let hash = fixture_hash("hello.wasm"); let index = build_index(&[("hello-tool", "0.1.0", &hash, 1000)]); Mock::given(method("GET")) .and(path("/index.json")) .respond_with(ResponseTemplate::new(200).set_body_string(&index)) .mount(&server) .await; // Wasm endpoint returns 404 Mock::given(method("GET")) .and(path("/tools/hello-tool.wasm")) .respond_with(ResponseTemplate::new(404)) .mount(&server) .await; configure_registry(home_path, &server.uri()); let output = install_tool(home_path, "hello-tool", &server.uri()); assert!( !output.status.success(), "install with 404 wasm should fail" ); } #[tokio::test] async fn install_when_registry_manifest_returns_404() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); let wasm_bytes = fixture_bytes("hello.wasm"); let hash = fixture_hash("hello.wasm"); let index = build_index(&[("hello-tool", "0.1.0", &hash, wasm_bytes.len() as u64)]); Mock::given(method("GET")) .and(path("/index.json")) .respond_with(ResponseTemplate::new(200).set_body_string(&index)) .mount(&server) .await; Mock::given(method("GET")) .and(path("/tools/hello-tool.wasm")) .respond_with(ResponseTemplate::new(200).set_body_bytes(wasm_bytes)) .mount(&server) .await; // Manifest endpoint returns 404 Mock::given(method("GET")) .and(path("/tools/hello-tool.json")) .respond_with(ResponseTemplate::new(404)) .mount(&server) .await; configure_registry(home_path, &server.uri()); let output = install_tool(home_path, "hello-tool", &server.uri()); assert!( !output.status.success(), "install with 404 manifest should fail" ); } #[tokio::test] async fn no_registries_configured() { let home = TempDir::new().unwrap(); let home_path = home.path(); std::fs::create_dir_all(home_path).unwrap(); // Write empty config (no registries) std::fs::write(home_path.join("config.toml"), "").unwrap(); let output = run_wasmbox(home_path, &["search", "anything"]); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("no registries"), "should report no registries, got: {stderr}" ); } #[tokio::test] async fn install_same_tool_twice() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); // First install let output = install_tool(home_path, "hello-tool", &server.uri()); assert!(output.status.success()); // Second install of same version — should succeed (overwrite) let output = install_tool(home_path, "hello-tool", &server.uri()); assert!(output.status.success()); // Tool should still work let output = run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); assert!(output.status.success()); } #[tokio::test] async fn remove_specific_version() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); // Install v0.1.0 setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Install v0.2.0 server.reset().await; setup_mock_registry(&server, "hello-tool", "0.2.0", "echo.wasm").await; run_wasmbox(home_path, &["update", "hello-tool"]); // Remove only v0.1.0 let output = run_wasmbox(home_path, &["remove", "hello-tool@0.1.0"]); assert!(output.status.success()); // v0.2.0 should still exist assert!(home_path.join("cache/hello-tool/0.2.0").exists()); assert!(!home_path.join("cache/hello-tool/0.1.0").exists()); } #[tokio::test] async fn hash_nonexistent_file() { let home = TempDir::new().unwrap(); let output = run_wasmbox(home.path(), &["hash", "/nonexistent/file.wasm"]); assert!(!output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("not found"), "should report not found, got: {stderr}" ); } #[tokio::test] async fn update_nonexistent_tool() { let home = TempDir::new().unwrap(); let home_path = home.path(); std::fs::create_dir_all(home_path.join("cache")).unwrap(); let output = run_wasmbox(home_path, &["update", "nonexistent-tool"]); assert!(!output.status.success()); } #[tokio::test] async fn run_with_no_args() { let home = TempDir::new().unwrap(); let output = run_wasmbox(home.path(), &["run"]); assert!(!output.status.success(), "run with no args should fail"); } #[tokio::test] async fn revoke_capability_on_tool_with_no_permissions() { let home = TempDir::new().unwrap(); let home_path = home.path(); std::fs::create_dir_all(home_path).unwrap(); let output = run_wasmbox(home_path, &["revoke", "nonexistent", "stdout"]); // Should succeed but report nothing to revoke let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("no ") || output.status.success(), "should handle missing permissions gracefully, got: {stderr}" ); } #[tokio::test] async fn registry_duplicate_add() { let home = TempDir::new().unwrap(); let home_path = home.path(); run_wasmbox(home_path, &["registry", "add", "https://example.com"]); let output = run_wasmbox(home_path, &["registry", "add", "https://example.com"]); assert!(output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("already"), "should report already configured, got: {stderr}" ); } #[tokio::test] async fn registry_remove_nonexistent() { let home = TempDir::new().unwrap(); let home_path = home.path(); let output = run_wasmbox(home_path, &["registry", "remove", "https://not-there.com"]); assert!(output.status.success()); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("not found"), "should report not found, got: {stderr}" ); } // ============================================================================= // Compliance: Run Log // ============================================================================= #[tokio::test] async fn run_creates_log_entry() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Run the tool run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); // Check run.log exists and has entries let log_path = home_path.join("run.log"); assert!(log_path.exists(), "run.log should be created"); let content = std::fs::read_to_string(&log_path).unwrap(); let lines: Vec<&str> = content.lines().collect(); // Should have 2 entries: pre-execution + post-execution assert!( lines.len() >= 2, "run.log should have at least 2 entries (pre+post), got {}", lines.len(), ); // Parse the last entry (post-execution) let last: serde_json::Value = serde_json::from_str(lines.last().unwrap()).unwrap(); assert_eq!(last["tool"], "hello-tool"); assert_eq!(last["version"], "0.1.0"); assert_eq!(last["hash_verified"], true); assert_eq!(last["exit_code"], 0); assert!(last["duration_ms"].as_u64().is_some()); assert_eq!(last["policy"], "no_policy"); } #[tokio::test] async fn run_logs_hash_failure() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Tamper with the binary let wasm_path = home_path.join("cache/hello-tool/0.1.0/hello-tool.wasm"); std::fs::write(&wasm_path, b"tampered").unwrap(); // Run — should fail run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); // Check run.log has the hash failure entry let log_path = home_path.join("run.log"); assert!( log_path.exists(), "run.log should be created even on hash failure" ); let content = std::fs::read_to_string(&log_path).unwrap(); let entry: serde_json::Value = serde_json::from_str(content.lines().next().unwrap()).unwrap(); assert_eq!(entry["hash_verified"], false); assert!(entry["exit_code"].is_null()); } #[tokio::test] async fn log_command_shows_entries() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Generate some log entries run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); let output = run_wasmbox(home_path, &["log", "--json"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let entries: serde_json::Value = serde_json::from_str(&stdout).expect("parse log JSON"); let entries = entries.as_array().expect("should be array"); assert!(!entries.is_empty(), "log should have entries"); } #[tokio::test] async fn log_filter_by_tool() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); let output = run_wasmbox(home_path, &["log", "--tool", "hello-tool", "--json"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let entries: Vec = serde_json::from_str(&stdout).unwrap(); assert!(entries.iter().all(|e| e["tool"] == "hello-tool")); } #[tokio::test] async fn log_last_n() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Run tool 3 times to generate multiple log entries run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); let output = run_wasmbox(home_path, &["log", "--last", "2", "--json"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let entries: Vec = serde_json::from_str(&stdout).unwrap(); assert_eq!(entries.len(), 2, "should only return last 2 entries"); } #[tokio::test] async fn log_stats() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); let output = run_wasmbox(home_path, &["log", "--stats", "--json"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let stats: serde_json::Value = serde_json::from_str(&stdout).expect("parse stats JSON"); assert!(stats["total_runs"].as_u64().unwrap() >= 2); // pre + post assert_eq!(stats["unique_tools"], 1); assert_eq!(stats["blocked"], 0); } #[tokio::test] async fn log_rotate() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); assert!(home_path.join("run.log").exists()); let output = run_wasmbox(home_path, &["log", "--rotate"]); assert!(output.status.success()); assert!( !home_path.join("run.log").exists(), "run.log should be rotated" ); assert!( home_path.join("run.log.1").exists(), "archived log should exist" ); } #[tokio::test] async fn log_export() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); let export_path = home_path.join("exported.jsonl"); let output = run_wasmbox( home_path, &["log", "--export", export_path.to_str().unwrap()], ); assert!(output.status.success()); assert!(export_path.exists(), "exported file should exist"); } #[tokio::test] async fn log_empty() { let home = TempDir::new().unwrap(); let home_path = home.path(); std::fs::create_dir_all(home_path).unwrap(); let output = run_wasmbox(home_path, &["log", "--json"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let entries: Vec = serde_json::from_str(&stdout).unwrap(); assert!(entries.is_empty()); } // ============================================================================= // Compliance: Policy // ============================================================================= #[tokio::test] async fn policy_init_creates_file() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); let output = run_wasmbox( home_path, &[ "policy", "init", "--name", "Test Policy", "--approved-by", "test@example.com", "--enforcement", "warn", ], ); assert!( output.status.success(), "policy init failed: {}", String::from_utf8_lossy(&output.stderr), ); assert!( home_path.join("policy.toml").exists(), "policy.toml should be created" ); // Verify content let content = std::fs::read_to_string(home_path.join("policy.toml")).unwrap(); assert!(content.contains("Test Policy")); assert!(content.contains("test@example.com")); assert!(content.contains("hello-tool")); } #[tokio::test] async fn policy_show() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); run_wasmbox( home_path, &[ "policy", "init", "--name", "Test", "--approved-by", "test@example.com", ], ); let output = run_wasmbox(home_path, &["policy", "show", "--json"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let policy: serde_json::Value = serde_json::from_str(&stdout).expect("parse policy JSON"); assert_eq!(policy["policy"]["name"], "Test"); assert!(policy["tools"]["hello-tool"].is_object()); } #[tokio::test] async fn policy_check() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); run_wasmbox( home_path, &[ "policy", "init", "--name", "Test", "--approved-by", "test@example.com", ], ); let output = run_wasmbox(home_path, &["policy", "check", "--json"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let results: Vec = serde_json::from_str(&stdout).unwrap(); assert_eq!(results.len(), 1); assert_eq!(results[0]["name"], "hello-tool"); assert_eq!(results[0]["status"], "approved"); } #[tokio::test] async fn policy_enforce_blocks_unapproved_tool() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); // Install hello-tool setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Create policy with hello-tool but set to enforce run_wasmbox( home_path, &[ "policy", "init", "--name", "Strict", "--approved-by", "test@example.com", "--enforcement", "enforce", ], ); // Remove hello-tool from policy run_wasmbox(home_path, &["policy", "remove", "hello-tool"]); // Running hello-tool should be blocked let output = run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); assert!(!output.status.success(), "should be blocked by policy"); assert_eq!(output.status.code(), Some(2), "exit code should be 2"); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("BLOCKED") || stderr.contains("blocked"), "should mention blocked, got: {stderr}", ); // Check run.log has the blocked entry let log_path = home_path.join("run.log"); let content = std::fs::read_to_string(&log_path).unwrap(); let has_blocked = content.lines().any(|line| { let v: serde_json::Value = serde_json::from_str(line).unwrap_or_default(); v["policy"] == "blocked" }); assert!(has_blocked, "run.log should contain a blocked entry"); } #[tokio::test] async fn policy_warn_allows_unapproved_tool() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Create policy and remove tool, but in warn mode run_wasmbox( home_path, &[ "policy", "init", "--name", "Lenient", "--approved-by", "test@example.com", "--enforcement", "warn", ], ); run_wasmbox(home_path, &["policy", "remove", "hello-tool"]); // Running should succeed (with warning) let output = run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); assert!(output.status.success(), "warn mode should allow execution"); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("WARNING"), "should show warning, got: {stderr}", ); } #[tokio::test] async fn policy_add_and_remove_tool() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); run_wasmbox( home_path, &[ "policy", "init", "--name", "Test", "--approved-by", "test@example.com", ], ); // Remove tool from policy let output = run_wasmbox(home_path, &["policy", "remove", "hello-tool"]); assert!(output.status.success()); // Re-add tool let output = run_wasmbox(home_path, &["policy", "add", "hello-tool"]); assert!(output.status.success()); // Check it's back let output = run_wasmbox(home_path, &["policy", "check", "--json"]); let stdout = String::from_utf8_lossy(&output.stdout); let results: Vec = serde_json::from_str(&stdout).unwrap(); assert_eq!(results[0]["status"], "approved"); } #[tokio::test] async fn policy_enforcement_modes() { let home = TempDir::new().unwrap(); let home_path = home.path(); std::fs::create_dir_all(home_path).unwrap(); run_wasmbox( home_path, &[ "policy", "init", "--name", "Test", "--approved-by", "test@example.com", ], ); // Set to enforce let output = run_wasmbox(home_path, &["policy", "enforce"]); assert!(output.status.success()); let content = std::fs::read_to_string(home_path.join("policy.toml")).unwrap(); assert!(content.contains(r#"enforcement = "enforce""#)); // Set to warn let output = run_wasmbox(home_path, &["policy", "warn"]); assert!(output.status.success()); let content = std::fs::read_to_string(home_path.join("policy.toml")).unwrap(); assert!(content.contains(r#"enforcement = "warn""#)); // Set to disabled let output = run_wasmbox(home_path, &["policy", "disable"]); assert!(output.status.success()); let content = std::fs::read_to_string(home_path.join("policy.toml")).unwrap(); assert!(content.contains(r#"enforcement = "disabled""#)); } #[tokio::test] async fn policy_diff_detects_changes() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); run_wasmbox( home_path, &[ "policy", "init", "--name", "Test", "--approved-by", "test@example.com", ], ); // Remove the tool from disk (but keep in policy) std::fs::remove_dir_all(home_path.join("cache/hello-tool")).unwrap(); let output = run_wasmbox(home_path, &["policy", "diff", "--json"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let changes: Vec = serde_json::from_str(&stdout).unwrap(); assert!(!changes.is_empty(), "diff should detect removed tool"); assert!(changes[0].contains("REMOVED")); } #[tokio::test] async fn policy_export() { let home = TempDir::new().unwrap(); let home_path = home.path(); std::fs::create_dir_all(home_path).unwrap(); run_wasmbox( home_path, &[ "policy", "init", "--name", "Export Test", "--approved-by", "test@example.com", ], ); let output = run_wasmbox(home_path, &["policy", "export"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("Export Test")); assert!(stdout.contains("test@example.com")); } // ============================================================================= // Compliance: Signed Audit Export // ============================================================================= #[tokio::test] async fn audit_export_json() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Run tool to generate log entries run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); let output = run_wasmbox(home_path, &["audit", "--export"]); assert!( output.status.success(), "audit export failed: {}", String::from_utf8_lossy(&output.stderr), ); let stdout = String::from_utf8_lossy(&output.stdout); let report: serde_json::Value = serde_json::from_str(&stdout).expect("parse audit JSON"); assert_eq!(report["report"]["type"], "wasmbox_compliance_audit"); assert!(report["tools"].as_array().is_some()); assert!(report["run_summary"]["total_executions"].as_u64().unwrap() >= 1); assert_eq!( report["compliance_checks"]["compliance_status"], "INCOMPLETE" ); } #[tokio::test] async fn audit_export_with_policy_passes() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Create policy run_wasmbox( home_path, &[ "policy", "init", "--name", "Test", "--approved-by", "test@example.com", "--enforcement", "enforce", ], ); // Run tool run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); let output = run_wasmbox(home_path, &["audit", "--export"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let report: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert_eq!(report["compliance_checks"]["compliance_status"], "PASS"); assert_eq!( report["compliance_checks"]["policy_enforcement_active"], true ); assert_eq!(report["policy"]["name"], "Test"); } #[tokio::test] async fn audit_export_markdown() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); let output = run_wasmbox(home_path, &["audit", "--export", "--format", "md"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("# WasmBox Compliance Audit Report")); } #[tokio::test] async fn audit_export_csv() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); let output = run_wasmbox(home_path, &["audit", "--export", "--format", "csv"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); assert!(stdout.contains("name,version,hash")); assert!(stdout.contains("hello-tool")); } #[tokio::test] async fn audit_init_key_and_sign() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Generate signing key let output = run_wasmbox(home_path, &["audit", "--init-key"]); assert!( output.status.success(), "init-key failed: {}", String::from_utf8_lossy(&output.stderr), ); assert!(home_path.join("audit_key.pem").exists()); assert!(home_path.join("audit_key.pub").exists()); // Export with signature let output = run_wasmbox(home_path, &["audit", "--export", "--sign"]); assert!( output.status.success(), "signed export failed: {}", String::from_utf8_lossy(&output.stderr), ); let stdout = String::from_utf8_lossy(&output.stdout); let report: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert!(report["signature"].is_object(), "should have signature"); assert_eq!(report["signature"]["algorithm"], "ed25519"); // Write report to file and verify let report_path = home_path.join("audit-report.json"); std::fs::write(&report_path, stdout.as_bytes()).unwrap(); let output = run_wasmbox( home_path, &["audit", "--verify", report_path.to_str().unwrap()], ); assert!( output.status.success(), "verify failed: {}", String::from_utf8_lossy(&output.stderr), ); let stderr = String::from_utf8_lossy(&output.stderr); assert!(stderr.contains("VALID"), "should say VALID, got: {stderr}"); } #[tokio::test] async fn audit_verify_tampered_report_fails() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // Generate key and signed report run_wasmbox(home_path, &["audit", "--init-key"]); let output = run_wasmbox(home_path, &["audit", "--export", "--sign"]); let stdout = String::from_utf8_lossy(&output.stdout); // Tamper with the report let tampered = stdout.replace("INCOMPLETE", "PASS"); let report_path = home_path.join("tampered.json"); std::fs::write(&report_path, tampered).unwrap(); let output = run_wasmbox( home_path, &["audit", "--verify", report_path.to_str().unwrap()], ); assert!( !output.status.success(), "tampered report should fail verification" ); let stderr = String::from_utf8_lossy(&output.stderr); assert!( stderr.contains("INVALID") || stderr.contains("invalid"), "should say invalid, got: {stderr}", ); } #[tokio::test] async fn audit_backwards_compatible() { // The default audit command (no --export) should still work let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); run_wasmbox( home_path, &[ "install", "hello-tool", "--registry", &server.uri(), "--allow-all", ], ); let output = run_wasmbox(home_path, &["audit", "--json"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let audit: serde_json::Value = serde_json::from_str(&stdout).expect("parse audit JSON"); assert!( audit.get("hello-tool").is_some(), "backwards-compatible audit should show hello-tool", ); } // ============================================================================= // Compliance: End-to-End Flow // ============================================================================= #[tokio::test] async fn full_compliance_flow() { let server = MockServer::start().await; let home = TempDir::new().unwrap(); let home_path = home.path(); // 1. Install tool setup_mock_registry(&server, "hello-tool", "0.1.0", "hello.wasm").await; configure_registry(home_path, &server.uri()); install_tool(home_path, "hello-tool", &server.uri()); // 2. Create policy let output = run_wasmbox( home_path, &[ "policy", "init", "--name", "Production Policy", "--approved-by", "compliance@company.com", "--enforcement", "enforce", ], ); assert!(output.status.success()); // 3. Run tool (should be approved since it was in policy from init) let output = run_wasmbox(home_path, &["run", "hello-tool", "--allow", "stdout"]); assert!(output.status.success()); // 4. Check log has approved entry let output = run_wasmbox(home_path, &["log", "--json"]); let stdout = String::from_utf8_lossy(&output.stdout); let entries: Vec = serde_json::from_str(&stdout).unwrap(); let approved = entries.iter().any(|e| e["policy"] == "approved"); assert!(approved, "should have an approved log entry"); // 5. Generate key and signed audit run_wasmbox(home_path, &["audit", "--init-key"]); let output = run_wasmbox(home_path, &["audit", "--export", "--sign"]); assert!(output.status.success()); let stdout = String::from_utf8_lossy(&output.stdout); let report: serde_json::Value = serde_json::from_str(&stdout).unwrap(); assert_eq!(report["compliance_checks"]["compliance_status"], "PASS"); assert_eq!(report["policy"]["name"], "Production Policy"); assert!(report["signature"].is_object()); // 6. Verify signed report let report_path = home_path.join("report.json"); std::fs::write(&report_path, stdout.as_bytes()).unwrap(); let output = run_wasmbox( home_path, &["audit", "--verify", report_path.to_str().unwrap()], ); assert!(output.status.success()); } // --- Session Recorder: via --- #[tokio::test] async fn test_via_passes_through_and_logs() { let home = TempDir::new().unwrap(); let out = run_wasmbox(home.path(), &["via", "echo", "hello"]); assert!(out.status.success()); assert_eq!(String::from_utf8_lossy(&out.stdout), "hello\n"); let log = run_wasmbox(home.path(), &["log", "--json"]); let text = String::from_utf8_lossy(&log.stdout); assert!(text.contains("\"mode\": \"via\"")); assert!(text.contains("\"tool\": \"echo\"")); } #[tokio::test] async fn test_via_propagates_exit_code() { let home = TempDir::new().unwrap(); let out = run_wasmbox(home.path(), &["via", "false"]); assert_eq!(out.status.code(), Some(1)); } // --- Session Recorder: shell --- #[tokio::test] async fn test_shell_init_generates_shims() { let home = TempDir::new().unwrap(); let out = run_wasmbox(home.path(), &["shell", "init", "--include", "git,jq"]); assert!(out.status.success()); let proxy_dir = home.path().join("proxy"); assert!(proxy_dir.join("git").is_file()); assert!(proxy_dir.join("jq").is_file()); assert!(!proxy_dir.join("wasmbox").exists()); let status = run_wasmbox(home.path(), &["shell", "status", "--json"]); let text = String::from_utf8_lossy(&status.stdout); assert!(text.contains("\"shim_count\"")); } // --- Session Recorder: wrap --- #[tokio::test] async fn test_wrap_records_session() { let home = TempDir::new().unwrap(); let out = run_wasmbox(home.path(), &["wrap", "echo", "hello-agent"]); assert!(out.status.success()); let err = String::from_utf8_lossy(&out.stderr); assert!(err.contains("WasmBox Session Summary")); } #[tokio::test] async fn test_wrap_missing_agent_friendly_error() { let home = TempDir::new().unwrap(); let out = run_wasmbox(home.path(), &["wrap", "definitely-not-installed-xyz"]); assert!(!out.status.success()); let err = String::from_utf8_lossy(&out.stderr).to_lowercase(); assert!(err.contains("not found") || err.contains("not installed")); } // --- Session Recorder: log filters --- #[tokio::test] async fn test_log_filter_by_mode() { let home = TempDir::new().unwrap(); run_wasmbox(home.path(), &["via", "echo", "one"]); run_wasmbox(home.path(), &["via", "true"]); let out = run_wasmbox(home.path(), &["log", "--mode", "via", "--json"]); let text = String::from_utf8_lossy(&out.stdout); assert!(text.contains("\"tool\": \"echo\"")); assert!(text.contains("\"tool\": \"true\"")); assert!(!text.contains("via_started")); } // --- Session Recorder: audit session export --- #[tokio::test] async fn test_audit_session_export() { let home = TempDir::new().unwrap(); let sid = "test-sess-4f1e"; // Run a proxied command stamped with an explicit session ID. let via = Command::new(wasmbox_bin()) .arg("--home") .arg(home.path()) .args(["via", "echo", "hi"]) .env("NO_COLOR", "1") .env("WASMBOX_SESSION_ID", sid) .output() .expect("run via"); assert!(via.status.success()); let out = run_wasmbox(home.path(), &["audit", "--session", sid, "--export"]); assert!(out.status.success()); let report = String::from_utf8_lossy(&out.stdout); assert!(report.contains("session_audit")); assert!(report.contains(sid)); assert!(report.contains("\"tool\": \"echo\"")); } // --- Session Recorder: proxy modes --- #[tokio::test] async fn test_via_enforce_mode_blocks_via_env() { let home = TempDir::new().unwrap(); let out = Command::new(wasmbox_bin()) .arg("--home") .arg(home.path()) .args(["via", "echo", "x"]) .env("NO_COLOR", "1") .env("WASMBOX_MODE", "enforce") .output() .expect("run via"); assert_eq!(out.status.code(), Some(126)); } // --- Session Recorder: end-to-end --- #[tokio::test] async fn test_end_to_end_session_recording() { let home = TempDir::new().unwrap(); run_wasmbox(home.path(), &["shell", "init", "--include", "echo,true"]); let agent = home.path().join("fake-agent.sh"); let wasmbox_bin = wasmbox_bin(); std::fs::write( &agent, format!( "#!/bin/sh\n\ {bin} --home {home} via echo step-one\n\ {bin} --home {home} via true\n", bin = wasmbox_bin.display(), home = home.path().display(), ), ) .unwrap(); #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; let mut p = std::fs::metadata(&agent).unwrap().permissions(); p.set_mode(0o755); std::fs::set_permissions(&agent, p).unwrap(); } let out = run_wasmbox(home.path(), &["wrap", agent.to_str().unwrap()]); assert!(out.status.success()); let err = String::from_utf8_lossy(&out.stderr); assert!(err.contains("WasmBox Session Summary")); assert!(err.contains("2 passthrough")); let log = run_wasmbox(home.path(), &["log", "--mode", "via", "--json"]); let entries: serde_json::Value = serde_json::from_str(&String::from_utf8_lossy(&log.stdout)).unwrap(); let arr = entries.as_array().unwrap(); assert_eq!(arr.len(), 2); let sid = arr[0]["session_id"].as_str().unwrap(); assert!(arr.iter().all(|e| e["session_id"].as_str() == Some(sid))); } #[tokio::test] async fn test_audit_session_sign_and_verify() { let home = TempDir::new().unwrap(); let sid = "verify-sess-1"; Command::new(wasmbox_bin()) .arg("--home") .arg(home.path()) .args(["via", "echo", "hi"]) .env("NO_COLOR", "1") .env("WASMBOX_SESSION_ID", sid) .output() .expect("run via"); run_wasmbox(home.path(), &["audit", "--init-key"]); let out = run_wasmbox(home.path(), &["audit", "--session", sid, "--export", "--sign"]); assert!(out.status.success()); let report_path = home.path().join("sess.json"); std::fs::write(&report_path, &out.stdout).unwrap(); let v = run_wasmbox(home.path(), &["audit", "--verify", report_path.to_str().unwrap()]); assert!(v.status.success(), "signed session report must verify"); assert!(String::from_utf8_lossy(&v.stderr).contains("VALID")); }