Skip to content

Commit ed0411c

Browse files
committed
test(cli): HTTP policy e2e tests + warn on unused fs.deny
Integration tests spawn the act binary against the http-client component and exercise phase A + phase C2 end-to-end: - default deny blocks outgoing HTTP - --http-allow matching the request host succeeds against example.com - --http-allow mismatched host blocks - --http-policy open allows arbitrary hosts - info emits the declared-but-denied warning when policy is deny Tests skip if the http-client wasm is not built; set ACT_TEST_HTTP_CLIENT_WASM to override the default path (../../components/http-client/target/wasm32-wasip2/release/ component_http_client.wasm). Also: warn_missing_capabilities now flags FsConfig.deny entries that were parsed but cannot be enforced in phase C2 — preopens can't express path-level deny overlays, so users get a heads-up until the custom wasi:filesystem impl lands in phase C1.
1 parent a7a0552 commit ed0411c

2 files changed

Lines changed: 174 additions & 0 deletions

File tree

act-cli/src/runtime.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,17 @@ pub fn warn_missing_capabilities(
259259
"component declares wasi:http but policy denies all HTTP access"
260260
);
261261
}
262+
263+
// Phase C2 limitation: FsConfig.deny is parsed but not enforced —
264+
// preopens can't express path-level deny overlays. Warn so users know
265+
// their overlay is silently inactive until the custom wasi:filesystem
266+
// impl lands in phase C1.
267+
if !fs.deny.is_empty() {
268+
tracing::warn!(
269+
component = %info.std.name,
270+
"fs deny entries are ignored in this release; use narrower allow entries instead"
271+
);
272+
}
262273
}
263274

264275
/// Spawn the component actor task. Owns the Store and ActWorld.

act-cli/tests/http_policy_e2e.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
//! End-to-end HTTP policy tests using a real ACT component.
2+
//!
3+
//! Requires a built `http-client` component. Set the `ACT_TEST_HTTP_CLIENT_WASM`
4+
//! env var to its path, or rely on the default location under
5+
//! `../components/http-client/target/wasm32-wasip2/release/`. Tests skip with a
6+
//! message when the component isn't available — running the full suite requires
7+
//! a reachable `example.com` over HTTPS.
8+
//!
9+
//! Run with `cargo test -p act-cli --test http_policy_e2e -- --nocapture` after
10+
//! building the http-client component with `just build` in its directory.
11+
12+
use std::path::PathBuf;
13+
use std::process::Command;
14+
15+
const DEFAULT_PATH: &str =
16+
"../../components/http-client/target/wasm32-wasip2/release/component_http_client.wasm";
17+
18+
fn fixture_wasm() -> Option<PathBuf> {
19+
if let Ok(env_path) = std::env::var("ACT_TEST_HTTP_CLIENT_WASM") {
20+
let p = PathBuf::from(env_path);
21+
if p.exists() {
22+
return Some(p);
23+
}
24+
}
25+
let default = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(DEFAULT_PATH);
26+
if default.exists() {
27+
return Some(default);
28+
}
29+
None
30+
}
31+
32+
fn skip_if_missing() -> Option<PathBuf> {
33+
match fixture_wasm() {
34+
Some(p) => Some(p),
35+
None => {
36+
eprintln!(
37+
"skipping: set ACT_TEST_HTTP_CLIENT_WASM or build components/http-client first"
38+
);
39+
None
40+
}
41+
}
42+
}
43+
44+
fn run_call(args: &[&str]) -> (bool, String, String) {
45+
let output = Command::new(env!("CARGO_BIN_EXE_act"))
46+
.args(args)
47+
.output()
48+
.expect("failed to spawn act");
49+
(
50+
output.status.success(),
51+
String::from_utf8_lossy(&output.stdout).to_string(),
52+
String::from_utf8_lossy(&output.stderr).to_string(),
53+
)
54+
}
55+
56+
#[test]
57+
fn default_deny_blocks_http() {
58+
let Some(wasm) = skip_if_missing() else {
59+
return;
60+
};
61+
let wasm_s = wasm.to_string_lossy().to_string();
62+
let (ok, _stdout, stderr) = run_call(&[
63+
"call",
64+
&wasm_s,
65+
"fetch",
66+
"--args",
67+
r#"{"url":"https://example.com"}"#,
68+
]);
69+
assert!(!ok, "expected call to fail under default deny policy");
70+
assert!(
71+
stderr.contains("HttpRequestDenied") || stderr.contains("blocked by ACT policy"),
72+
"stderr should explain denial; got: {stderr}"
73+
);
74+
}
75+
76+
#[test]
77+
fn http_allow_matching_host_succeeds() {
78+
let Some(wasm) = skip_if_missing() else {
79+
return;
80+
};
81+
let wasm_s = wasm.to_string_lossy().to_string();
82+
let (ok, stdout, stderr) = run_call(&[
83+
"call",
84+
"--http-allow",
85+
"example.com",
86+
&wasm_s,
87+
"fetch",
88+
"--args",
89+
r#"{"url":"https://example.com"}"#,
90+
]);
91+
assert!(
92+
ok,
93+
"expected call to succeed with --http-allow example.com; stderr: {stderr}"
94+
);
95+
assert!(
96+
stdout.contains("Example Domain") || stdout.contains("example"),
97+
"expected response body; got stdout: {stdout}"
98+
);
99+
}
100+
101+
#[test]
102+
fn http_allow_mismatched_host_blocks() {
103+
let Some(wasm) = skip_if_missing() else {
104+
return;
105+
};
106+
let wasm_s = wasm.to_string_lossy().to_string();
107+
let (ok, _stdout, stderr) = run_call(&[
108+
"call",
109+
"--http-allow",
110+
"evil.example",
111+
&wasm_s,
112+
"fetch",
113+
"--args",
114+
r#"{"url":"https://example.com"}"#,
115+
]);
116+
assert!(
117+
!ok,
118+
"expected deny when allow rule's host doesn't match request host"
119+
);
120+
assert!(
121+
stderr.contains("HttpRequestDenied") || stderr.contains("blocked by ACT policy"),
122+
"stderr should explain denial; got: {stderr}"
123+
);
124+
}
125+
126+
#[test]
127+
fn http_policy_open_allows_any_host() {
128+
let Some(wasm) = skip_if_missing() else {
129+
return;
130+
};
131+
let wasm_s = wasm.to_string_lossy().to_string();
132+
let (ok, stdout, stderr) = run_call(&[
133+
"call",
134+
"--http-policy",
135+
"open",
136+
&wasm_s,
137+
"fetch",
138+
"--args",
139+
r#"{"url":"https://example.com"}"#,
140+
]);
141+
assert!(
142+
ok,
143+
"expected open policy to allow the request; stderr: {stderr}"
144+
);
145+
assert!(
146+
stdout.contains("Example Domain") || stdout.contains("example"),
147+
"expected response body; got stdout: {stdout}"
148+
);
149+
}
150+
151+
#[test]
152+
fn declaration_warning_when_policy_deny() {
153+
let Some(wasm) = skip_if_missing() else {
154+
return;
155+
};
156+
let wasm_s = wasm.to_string_lossy().to_string();
157+
// info doesn't need the network; should still emit the capability warning
158+
let (_ok, _stdout, stderr) = run_call(&["info", "--tools", &wasm_s]);
159+
assert!(
160+
stderr.contains("component declares wasi:http but policy denies"),
161+
"expected capability warning; got: {stderr}"
162+
);
163+
}

0 commit comments

Comments
 (0)