use std::path::Path;
use plecto_control::{
ChainOutcome, Control, ControlError, Host, HttpRequest, MemoryStore, ReloadOutcome,
ReloadSource, ResolvedArtifact, serve_reloads,
};
use plecto_host::Header;
use plecto_host::test_support::{TestSigner, bound_sbom, filter_hello_component};
use tempfile::tempdir;
fn req(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 signed_filter_hello() -> (TestSigner, ResolvedArtifact) {
let component = filter_hello_component();
let signer = TestSigner::new().unwrap();
let component_signature = signer.sign(&component).unwrap();
let sbom = bound_sbom(&component);
let sbom_signature = signer.sign(&sbom).unwrap();
(
signer,
ResolvedArtifact {
component,
component_signature,
sbom,
sbom_signature,
},
)
}
fn manifest_toml(digest: &str, chain: &[&str]) -> String {
let chain_list = chain
.iter()
.map(|c| format!("\"{c}\""))
.collect::<Vec<_>>()
.join(", ");
format!(
r#"
[[filter]]
id = "fh"
source = "fh"
digest = "{digest}"
isolation = "untrusted"
[chain]
filters = [{chain_list}]
"#
)
}
fn setup() -> (Host, MemoryStore, String) {
let (signer, artifact) = signed_filter_hello();
let mut store = MemoryStore::new();
let digest = store.insert("fh", artifact);
let host = Host::new(signer.trust_policy().unwrap()).unwrap();
(host, store, digest)
}
fn write_manifest(path: &Path, digest: &str, chain: &[&str]) {
std::fs::write(path, manifest_toml(digest, chain)).unwrap();
}
struct FakeReloadSource {
remaining: usize,
}
impl ReloadSource for FakeReloadSource {
fn recv(&mut self) -> Option<()> {
if self.remaining == 0 {
None
} else {
self.remaining -= 1;
Some(())
}
}
}
#[test]
fn reload_from_disk_swaps_on_change_and_is_idempotent() {
let dir = tempdir().unwrap();
let manifest_path = dir.path().join("plecto.toml");
let (host, store, digest) = setup();
write_manifest(&manifest_path, &digest, &["fh"]);
let control = Control::load_at(host, &manifest_path, Box::new(store)).unwrap();
assert!(
matches!(control.on_request(req(&[("x-plecto-block", "1")])), ChainOutcome::Respond(r) if r.status == 403),
"v1 chain blocks"
);
write_manifest(&manifest_path, &digest, &[]);
match control.reload_from_disk().unwrap() {
ReloadOutcome::Reloaded { hash } => assert!(hash.starts_with("sha256:")),
ReloadOutcome::Unchanged => panic!("the manifest changed; reload must swap"),
}
assert!(
matches!(
control.on_request(req(&[("x-plecto-block", "1")])),
ChainOutcome::Forward(_)
),
"after reload the chain is empty → the same request forwards"
);
assert_eq!(
control.reload_from_disk().unwrap(),
ReloadOutcome::Unchanged,
"an unchanged manifest reloads to Unchanged"
);
}
#[test]
fn serve_reloads_drives_reload_from_a_trigger() {
let dir = tempdir().unwrap();
let manifest_path = dir.path().join("plecto.toml");
let (host, store, digest) = setup();
write_manifest(&manifest_path, &digest, &["fh"]);
let control = Control::load_at(host, &manifest_path, Box::new(store)).unwrap();
write_manifest(&manifest_path, &digest, &[]);
let mut source = FakeReloadSource { remaining: 1 };
serve_reloads(&control, &mut source);
assert!(
matches!(
control.on_request(req(&[("x-plecto-block", "1")])),
ChainOutcome::Forward(_)
),
"serve_reloads must have re-read the manifest and swapped the chain"
);
}
#[test]
fn reload_rejecting_a_trust_change_is_fail_closed() {
let dir = tempdir().unwrap();
let manifest_path = dir.path().join("plecto.toml");
let (host, store, digest) = setup();
let v1 = format!(
r#"
[trust]
keys = ["a.pub"]
[[filter]]
id = "fh"
source = "fh"
digest = "{digest}"
isolation = "untrusted"
[chain]
filters = ["fh"]
"#
);
std::fs::write(&manifest_path, &v1).unwrap();
let control = Control::load_at(host, &manifest_path, Box::new(store)).unwrap();
std::fs::write(&manifest_path, v1.replace("a.pub", "b.pub")).unwrap();
match control.reload_from_disk() {
Err(ControlError::TrustChangeRequiresRestart) => {}
other => panic!("a [trust] edit must be rejected fail-closed, got {other:?}"),
}
assert!(
matches!(control.on_request(req(&[("x-plecto-block", "1")])), ChainOutcome::Respond(r) if r.status == 403),
"the live set is unchanged after a rejected trust-change reload"
);
}
#[test]
fn reload_rejecting_a_state_change_is_fail_closed() {
let dir = tempdir().unwrap();
let manifest_path = dir.path().join("plecto.toml");
let (host, store, digest) = setup();
write_manifest(&manifest_path, &digest, &["fh"]);
let control = Control::load_at(host, &manifest_path, Box::new(store)).unwrap();
let v2 = format!(
"[state]\nbackend = \"redb\"\npath = \"state.redb\"\n{}",
manifest_toml(&digest, &["fh"])
);
std::fs::write(&manifest_path, v2).unwrap();
match control.reload_from_disk() {
Err(ControlError::StateChangeRequiresRestart) => {}
other => panic!("a [state] edit must be rejected fail-closed, got {other:?}"),
}
assert!(
matches!(control.on_request(req(&[("x-plecto-block", "1")])), ChainOutcome::Respond(r) if r.status == 403),
"the live set is unchanged after a rejected state-change reload"
);
}
#[test]
fn snapshot_pins_config_across_a_reload() {
let dir = tempdir().unwrap();
let manifest_path = dir.path().join("plecto.toml");
let (host, store, digest) = setup();
write_manifest(&manifest_path, &digest, &["fh"]); let control = Control::load_at(host, &manifest_path, Box::new(store)).unwrap();
let snap = control.snapshot();
write_manifest(&manifest_path, &digest, &[]);
assert!(matches!(
control.reload_from_disk().unwrap(),
ReloadOutcome::Reloaded { .. }
));
assert!(
matches!(snap.on_request(req(&[("x-plecto-block", "1")])), ChainOutcome::Respond(r) if r.status == 403),
"the snapshot stays pinned to the pre-reload config"
);
assert!(
matches!(
control.on_request(req(&[("x-plecto-block", "1")])),
ChainOutcome::Forward(_)
),
"a fresh request through Control sees the reloaded config"
);
}
#[test]
fn reload_from_disk_without_a_path_errors() {
let (host, store, digest) = setup();
let manifest = plecto_control::Manifest::from_toml(&manifest_toml(&digest, &["fh"])).unwrap();
let control = Control::load(host, &manifest, Box::new(store)).unwrap();
match control.reload_from_disk() {
Err(ControlError::NoManifestPath) => {}
other => panic!("expected NoManifestPath, got {other:?}"),
}
}