use plecto_host::test_support::{TestSigner, bound_sbom};
use plecto_host::{
Header, Host, HttpRequest, LoadOptions, LoadedFilter, LogLine, RequestDecision, RequestTrace,
RunError, SignedArtifact,
};
fn on_req(f: &LoadedFilter, r: &HttpRequest) -> Result<(RequestDecision, Vec<LogLine>), RunError> {
f.on_request(r, &RequestTrace::root())
}
fn component_bytes() -> Vec<u8> {
std::fs::read(env!("FILTER_HELLO_COMPONENT")).expect("read filter-hello component")
}
fn signed_load(opts: LoadOptions) -> (Host, LoadedFilter) {
let bytes = component_bytes();
let signer = TestSigner::new().unwrap();
let component_signature = signer.sign(&bytes).unwrap();
let sbom = bound_sbom(&bytes);
let sbom_signature = signer.sign(&sbom).unwrap();
let host = Host::new(signer.trust_policy().unwrap()).unwrap();
let artifact = SignedArtifact {
component_bytes: &bytes,
component_signature: &component_signature,
sbom: &sbom,
sbom_signature: &sbom_signature,
};
let filter = host.load("filter-hello", &artifact, opts).unwrap();
(host, filter)
}
fn request(headers: &[(&str, &str)]) -> HttpRequest {
HttpRequest {
method: "GET".to_string(),
path: "/".to_string(),
authority: "example.test".to_string(),
scheme: "https".to_string(),
headers: headers
.iter()
.map(|(n, v)| Header {
name: (*n).to_string(),
value: v.as_bytes().to_vec(),
})
.collect(),
}
}
fn init_calls(logs: &[LogLine]) -> u64 {
logs.iter()
.find_map(|l| {
l.message
.strip_prefix("init-calls=")
.and_then(|n| n.parse().ok())
})
.expect("filter-hello logs init-calls=N every request")
}
fn local_state(logs: &[LogLine]) -> u32 {
logs.iter()
.find_map(|l| {
l.message
.strip_prefix("local-state=")
.and_then(|n| n.parse().ok())
})
.expect("filter-hello logs local-state=N every request")
}
#[test]
fn non_utf8_request_header_continues_with_native_bytes() {
let (_host, filter) = signed_load(LoadOptions::untrusted());
let raw: &[u8] = &[0xC3, 0x28];
let req = HttpRequest {
method: "GET".to_string(),
path: "/".to_string(),
authority: "example.test".to_string(),
scheme: "https".to_string(),
headers: vec![Header {
name: "x-blob".to_string(),
value: raw.to_vec(),
}],
};
let (decision, _) = on_req(&filter, &req).unwrap();
assert!(
matches!(decision, RequestDecision::Continue),
"filter-hello should continue an unblocked request"
);
}
#[test]
fn continues_when_request_is_not_blocked() {
let (_host, filter) = signed_load(LoadOptions::untrusted());
let (decision, logs) = on_req(&filter, &request(&[])).unwrap();
assert!(
matches!(decision, RequestDecision::Continue),
"an unblocked request should continue down the chain"
);
assert!(logs.iter().any(|l| l.message.contains("on-request")));
}
#[test]
fn short_circuits_when_block_header_present() {
let (_host, filter) = signed_load(LoadOptions::untrusted());
let (decision, _logs) = on_req(&filter, &request(&[("x-plecto-block", "1")])).unwrap();
match decision {
RequestDecision::ShortCircuit(resp) => {
assert_eq!(resp.status, 403, "blocked request must get 403");
assert!(
resp.headers
.iter()
.any(|h| h.name == "x-plecto" && h.value.as_slice() == b"blocked"),
"short-circuit response must carry the filter's header"
);
}
_ => panic!("expected short-circuit for a blocked request"),
}
}
#[test]
fn trusted_filter_runs_init_once_across_requests() {
let (_host, filter) = signed_load(LoadOptions::trusted());
for _ in 0..3 {
let (_decision, logs) = on_req(&filter, &request(&[])).unwrap();
assert_eq!(init_calls(&logs), 1, "trusted init must run exactly once");
}
}
#[test]
fn untrusted_filter_reinitializes_each_request() {
let (_host, filter) = signed_load(LoadOptions::untrusted());
let mut seen = Vec::new();
for _ in 0..3 {
let (_decision, logs) = on_req(&filter, &request(&[])).unwrap();
seen.push(init_calls(&logs));
}
assert_eq!(seen, vec![1, 2, 3], "untrusted init must run every request");
}
#[test]
fn rate_limit_short_circuits_after_capacity() {
let (_host, filter) = signed_load(LoadOptions::trusted().with_ratelimit_bucket(2, 1, 60_000));
let limited = request(&[("x-plecto-ratelimit", "1")]);
for n in 1..=2 {
let (decision, _logs) = on_req(&filter, &limited).unwrap();
assert!(
matches!(decision, RequestDecision::Continue),
"request {n} within capacity should continue"
);
}
let (decision, _logs) = on_req(&filter, &limited).unwrap();
match decision {
RequestDecision::ShortCircuit(resp) => {
assert_eq!(resp.status, 429, "exhausted bucket must get 429");
assert!(
resp.headers.iter().any(|h| h.name == "retry-after-ms"),
"429 must advertise a retry-after"
);
}
_ => panic!("expected short-circuit once the bucket is empty"),
}
}
#[test]
fn rate_limit_counting_is_host_native_across_fresh_untrusted_instances() {
let (_host, filter) = signed_load(LoadOptions::untrusted().with_ratelimit_bucket(2, 1, 60_000));
let limited = request(&[("x-plecto-ratelimit", "1")]);
for n in 1..=2 {
let (decision, logs) = on_req(&filter, &limited).unwrap();
assert_eq!(
init_calls(&logs),
n,
"untrusted instance must be fresh each request (init re-runs)"
);
assert!(
matches!(decision, RequestDecision::Continue),
"request {n} within capacity should continue even on a fresh instance"
);
}
let (decision, logs) = on_req(&filter, &limited).unwrap();
assert_eq!(
init_calls(&logs),
3,
"third request is still a fresh instance"
);
assert!(
matches!(decision, RequestDecision::ShortCircuit(resp) if resp.status == 429),
"host-native bucket drains across fresh instances → third request denied 429"
);
}
#[test]
fn ratelimit_without_a_host_bucket_fails_closed() {
let (_host, filter) = signed_load(LoadOptions::untrusted());
let (decision, _logs) = on_req(&filter, &request(&[("x-plecto-ratelimit", "1")])).unwrap();
assert!(
matches!(decision, RequestDecision::ShortCircuit(resp) if resp.status == 429),
"a filter with no host-configured bucket must fail closed (denied), not bypass the limiter"
);
}
#[test]
fn runaway_filter_is_interrupted_by_epoch_deadline() {
let (_host, filter) = signed_load(LoadOptions::untrusted().with_request_deadline_ms(50));
let result = on_req(&filter, &request(&[("x-plecto-spin", "1")]));
assert!(
matches!(result, Err(RunError::Deadline)),
"a runaway filter must trap as Deadline (fail-closed)"
);
}
#[test]
fn trusted_filter_recovers_after_trap() {
let (_host, filter) = signed_load(LoadOptions::trusted().with_request_deadline_ms(50));
let trapped = on_req(&filter, &request(&[("x-plecto-spin", "1")]));
assert!(
matches!(trapped, Err(RunError::Deadline)),
"the runaway request must trap the persistent instance"
);
let (decision, logs) =
on_req(&filter, &request(&[])).expect("trusted filter must self-heal after a trap");
assert!(matches!(decision, RequestDecision::Continue));
assert!(logs.iter().any(|l| l.message.contains("on-request")));
}
#[test]
fn trusted_trap_storm_opens_circuit_breaker() {
let (_host, filter) = signed_load(LoadOptions::trusted().with_request_deadline_ms(50));
let spin = request(&[("x-plecto-spin", "1")]);
for _ in 0..3 {
assert!(
matches!(on_req(&filter, &spin), Err(RunError::Deadline)),
"consecutive traps self-heal and surface as Deadline until the breaker opens"
);
}
assert!(
matches!(on_req(&filter, &request(&[])), Err(RunError::Unavailable)),
"once the breaker is open, even a benign request fails closed cheaply (no rebuild)"
);
}
#[test]
fn memory_limit_traps_runaway_allocation() {
let (_host, filter) = signed_load(LoadOptions::untrusted().with_max_memory_bytes(16 << 20));
let result = on_req(&filter, &request(&[("x-plecto-balloon", "1")]));
assert!(
matches!(result, Err(RunError::Trap(_))),
"over-allocation past the memory cap must trap (fail-closed), not OOM the host"
);
let (decision, _logs) = on_req(&filter, &request(&[])).expect("host survives the guest trap");
assert!(matches!(decision, RequestDecision::Continue));
}
#[test]
fn untrusted_guest_memory_is_fresh_each_request() {
let (_host, filter) = signed_load(LoadOptions::untrusted());
for _ in 0..3 {
let (_decision, logs) = on_req(&filter, &request(&[])).unwrap();
assert_eq!(
local_state(&logs),
1,
"untrusted guest memory must be fresh each request (no carry-over)"
);
}
}
#[test]
fn trusted_guest_memory_persists_across_requests() {
let (_host, filter) = signed_load(LoadOptions::trusted());
let mut seen = Vec::new();
for _ in 0..3 {
let (_decision, logs) = on_req(&filter, &request(&[])).unwrap();
seen.push(local_state(&logs));
}
assert_eq!(
seen,
vec![1, 2, 3],
"trusted guest memory persists across reused-instance requests"
);
}