use std::path::Path;
use std::process::Command;
fn run(args: &[&str], dir: &Path) -> std::process::Output {
Command::new(env!("CARGO_BIN_EXE_plecto"))
.args(args)
.current_dir(dir)
.output()
.unwrap()
}
const VALID_MANIFEST: &str = r#"
[[upstream]]
name = "app"
addresses = ["127.0.0.1:9000"]
[upstream.health]
path = "/healthz"
[[route]]
upstream = "app"
[route.match]
path_prefix = "/api"
"#;
#[test]
fn version_flag_prints_the_package_version_and_exits_zero() {
let dir = tempfile::tempdir().unwrap();
for flag in ["--version", "-V"] {
let out = run(&[flag], dir.path());
assert!(out.status.success(), "{flag} exits 0");
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains(env!("CARGO_PKG_VERSION")),
"{flag} prints the package version, got: {stdout:?}"
);
}
}
#[test]
fn version_flag_names_the_compiled_capability_profile() {
let dir = tempfile::tempdir().unwrap();
let out = run(&["--version"], dir.path());
assert!(out.status.success(), "--version exits 0");
let stdout = String::from_utf8_lossy(&out.stdout);
let expected = if cfg!(feature = "capabilities") {
"profile: capabilities (outbound-http, outbound-tcp, fat-guest)"
} else {
"profile: minimal"
};
assert!(
stdout.contains(expected),
"--version names the compiled profile, want {expected:?}, got: {stdout:?}"
);
}
#[test]
fn version_flag_lists_the_loadable_filter_contract_versions() {
let dir = tempfile::tempdir().unwrap();
let out = run(&["--version"], dir.path());
assert!(out.status.success(), "--version exits 0");
let stdout = String::from_utf8_lossy(&out.stdout);
for version in [
"plecto:filter@0.1.0",
"plecto:filter@0.2.0",
"plecto:filter@0.3.0",
"plecto:filter@0.4.0",
] {
assert!(
stdout.contains(version),
"--version lists {version} as loadable, got: {stdout:?}"
);
}
}
#[test]
fn schema_emits_a_draft07_json_schema_describing_the_manifest() {
let dir = tempfile::tempdir().unwrap();
let out = run(&["schema"], dir.path());
assert!(
out.status.success(),
"schema exits 0, stderr: {:?}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
let schema: serde_json::Value = serde_json::from_str(&stdout).expect("stdout is JSON");
assert_eq!(
schema["$schema"], "http://json-schema.org/draft-07/schema#",
"draft-07 output (the taplo-compatible level)"
);
assert_eq!(
schema["additionalProperties"],
serde_json::Value::Bool(false),
"deny_unknown_fields → additionalProperties: false"
);
let props = schema["properties"]
.as_object()
.expect("root schema has properties");
for key in [
"trust",
"state",
"filter",
"chain",
"upstream",
"route",
"tls",
"observability",
"listen",
] {
assert!(props.contains_key(key), "schema describes [{key}]");
}
}
#[test]
fn validate_accepts_a_good_manifest_and_exits_without_serving() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("plecto.toml"), VALID_MANIFEST).unwrap();
let out = run(&["validate", "plecto.toml"], dir.path());
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
out.status.success(),
"a valid manifest validates, stderr: {stderr:?}"
);
assert!(
stdout.contains("sha256:"),
"validate reports the config version, got: {stdout:?}"
);
}
#[test]
fn validate_notes_the_startup_fixed_fields_a_manifest_declares() {
let dir = tempfile::tempdir().unwrap();
let manifest = format!(
r#"
[observability]
admin_addr = "127.0.0.1:9090"
[listen]
addr = "0.0.0.0:8080"
[listen.drain]
window_ms = 5000
{VALID_MANIFEST}"#
);
std::fs::write(dir.path().join("plecto.toml"), manifest).unwrap();
let out = run(&["validate", "plecto.toml"], dir.path());
assert!(
out.status.success(),
"the manifest is valid, stderr: {:?}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
let note = stdout
.lines()
.find(|l| l.starts_with("note:"))
.unwrap_or_else(|| panic!("validate notes the startup-fixed fields, got: {stdout:?}"));
for field in ["[observability]", "[listen].addr", "[listen].drain"] {
assert!(
note.contains(field),
"the note names {field}, got: {note:?}"
);
}
assert!(
note.contains("restart"),
"the note says what to do about it, got: {note:?}"
);
assert!(
!note.contains("advertised_port"),
"the note lists only what the manifest declares, got: {note:?}"
);
}
#[test]
fn validate_warns_when_the_unauthenticated_admin_listener_is_public() {
let dir = tempfile::tempdir().unwrap();
let manifest = format!(
r#"
[observability]
admin_addr = "0.0.0.0:9090"
{VALID_MANIFEST}"#
);
std::fs::write(dir.path().join("plecto.toml"), manifest).unwrap();
let out = run(&["validate", "plecto.toml"], dir.path());
assert!(
out.status.success(),
"a public admin bind remains an explicit valid operator choice: {:?}",
String::from_utf8_lossy(&out.stderr)
);
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
stdout.contains("warning:")
&& stdout.contains("admin_addr")
&& stdout.contains("unauthenticated"),
"validate warns before exposing the admin listener, got: {stdout:?}"
);
}
#[test]
fn validate_stays_quiet_when_no_startup_fixed_field_is_declared() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(dir.path().join("plecto.toml"), VALID_MANIFEST).unwrap();
let out = run(&["validate", "plecto.toml"], dir.path());
let stdout = String::from_utf8_lossy(&out.stdout);
assert!(
!stdout.contains("note:"),
"nothing startup-fixed is declared, so there is nothing to note: {stdout:?}"
);
}
#[test]
fn validate_rejects_an_unknown_field_with_a_nonzero_exit() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("plecto.toml"),
VALID_MANIFEST.replace("path = \"/healthz\"", "path = \"/healthz\"\ntypo_field = 1"),
)
.unwrap();
let out = run(&["validate", "plecto.toml"], dir.path());
assert!(!out.status.success(), "an unknown field must fail validate");
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("typo_field"),
"the error names the offending field, got: {stderr:?}"
);
}
#[test]
fn validate_rejects_a_route_referencing_an_unknown_upstream() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("plecto.toml"),
VALID_MANIFEST.replace("upstream = \"app\"", "upstream = \"nonexistent\""),
)
.unwrap();
let out = run(&["validate", "plecto.toml"], dir.path());
assert!(
!out.status.success(),
"a dangling upstream reference must fail validate"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("nonexistent"),
"the error names the unknown upstream, got: {stderr:?}"
);
}
#[test]
fn validate_rejects_the_inert_chain_section_and_names_the_migration() {
let dir = tempfile::tempdir().unwrap();
let manifest = format!(
r#"
[[filter]]
id = "security-headers"
source = "oci/security-headers"
digest = "sha256:abc"
[chain]
filters = ["security-headers"]
{VALID_MANIFEST}"#
);
std::fs::write(dir.path().join("plecto.toml"), manifest).unwrap();
let out = run(&["validate", "plecto.toml"], dir.path());
assert!(
!out.status.success(),
"a manifest declaring [chain] must fail validate, stdout: {:?}",
String::from_utf8_lossy(&out.stdout)
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("[chain]"),
"the error names the rejected section, got: {stderr:?}"
);
assert!(
stderr.contains("[[route]]"),
"the error names where the filters belong, got: {stderr:?}"
);
}
#[test]
fn validate_never_creates_the_redb_state_file() {
let dir = tempfile::tempdir().unwrap();
let manifest = format!("[state]\nbackend = \"redb\"\npath = \"kv.redb\"\n{VALID_MANIFEST}");
std::fs::write(dir.path().join("plecto.toml"), manifest).unwrap();
let out = run(&["validate", "plecto.toml"], dir.path());
assert!(
out.status.success(),
"a coherent [state] section validates, stderr: {:?}",
String::from_utf8_lossy(&out.stderr)
);
assert!(
!dir.path().join("kv.redb").exists(),
"validate must not create the redb state file"
);
}
#[test]
fn validate_rejects_a_bad_upstream_ca_path() {
let dir = tempfile::tempdir().unwrap();
let manifest = VALID_MANIFEST.replace(
"[upstream.health]",
"[upstream.tls]\nca_path = \"missing-ca.pem\"\n[upstream.health]",
);
std::fs::write(dir.path().join("plecto.toml"), manifest).unwrap();
let out = run(&["validate", "plecto.toml"], dir.path());
assert!(
!out.status.success(),
"a missing CA file must fail validate"
);
let stderr = String::from_utf8_lossy(&out.stderr);
assert!(
stderr.contains("missing-ca.pem"),
"the error names the missing CA path, got: {stderr:?}"
);
}