use std::path::Path;
use bugwarden::audit::{select_sinks, SinkSelection};
use bugwarden::config::Cli;
use bugwarden::otel::{self, OtelEnv};
use clap::error::ErrorKind;
use clap::Parser as _;
const VARS: [&str; 11] = [
"MCP_ALLOWED_HOSTS",
"BUGZILLA_USE_AUTH_HEADER",
"MCP_READ_ONLY",
"BUGWARDEN_AUDIT_CONFIG",
"OTEL_EXPORTER_OTLP_ENDPOINT",
"OTEL_EXPORTER_OTLP_HEADERS",
"OTEL_EXPORTER_OTLP_PROTOCOL",
"OTEL_SERVICE_NAME",
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
"OTEL_EXPORTER_OTLP_LOGS_HEADERS",
"OTEL_EXPORTER_OTLP_LOGS_PROTOCOL",
];
fn clear() {
for var in VARS {
std::env::remove_var(var);
}
}
fn parse(args: &[&str]) -> Result<Cli, clap::Error> {
let mut argv = vec![
"bugwarden",
"--bugzilla-server",
"https://bugzilla.example.com",
];
argv.extend_from_slice(args);
Cli::try_parse_from(argv)
}
fn outcome(var: &str, value: &str, read: fn(&Cli) -> bool) -> Result<bool, ErrorKind> {
clear();
std::env::set_var(var, value);
let outcome = parse(&[]).map(|cli| read(&cli)).map_err(|e| e.kind());
clear();
outcome
}
#[test]
fn every_flag_is_settable_from_the_environment() {
clear();
std::env::set_var("MCP_ALLOWED_HOSTS", "a.example:8000, b.example");
let cli = parse(&[]).expect("a host list parses");
assert_eq!(
cli.resolved_allowed_hosts(),
["a.example:8000", "b.example"],
"MCP_ALLOWED_HOSTS must reach --allowed-hosts as separate entries"
);
std::env::set_var("MCP_ALLOWED_HOSTS", "a b.example");
let cli = parse(&[]).expect("a space-containing value parses");
assert_eq!(
cli.resolved_allowed_hosts(),
["a b.example"],
"MCP_ALLOWED_HOSTS must not split a single entry on whitespace"
);
assert!(
cli.checked_allowed_hosts().is_err(),
"a space-containing typo is a startup error, not two authorities"
);
std::env::set_var("MCP_ALLOWED_HOSTS", "");
let cli = parse(&[]).expect("an empty value parses");
assert!(
cli.resolved_allowed_hosts().is_empty(),
"MCP_ALLOWED_HOSTS= must read as unset, leaving Host validation off"
);
assert!(
cli.checked_allowed_hosts()
.expect("empty is unset, not an error")
.is_none(),
"MCP_ALLOWED_HOSTS= must leave Host validation off"
);
std::env::set_var("MCP_ALLOWED_HOSTS", "env.example");
let cli = parse(&["--allowed-hosts", "cli.example"]).expect("the flag parses");
assert_eq!(
cli.resolved_allowed_hosts(),
["cli.example"],
"--allowed-hosts must win over MCP_ALLOWED_HOSTS"
);
assert_eq!(
outcome("BUGZILLA_USE_AUTH_HEADER", "true", |cli| cli
.use_auth_header),
Ok(true),
"BUGZILLA_USE_AUTH_HEADER=true must select the Authorization header"
);
assert_eq!(
outcome("BUGZILLA_USE_AUTH_HEADER", "false", |cli| cli
.use_auth_header),
Ok(false)
);
for value in [
"true", "false", "TRUE", "True", "1", "0", "yes", "no", "on", "off", "",
] {
assert_eq!(
outcome("BUGZILLA_USE_AUTH_HEADER", value, |cli| cli.use_auth_header),
outcome("MCP_READ_ONLY", value, |cli| cli.read_only),
"BUGZILLA_USE_AUTH_HEADER={value:?} must be read exactly like MCP_READ_ONLY"
);
}
clear();
assert!(
otel::resolve(&OtelEnv::from_env())
.expect("an environment with no endpoint resolves")
.is_none(),
"with no OTEL_EXPORTER_OTLP_ENDPOINT the export must be off"
);
std::env::set_var(
"OTEL_EXPORTER_OTLP_ENDPOINT",
"http://collector.example:4318",
);
std::env::set_var("OTEL_SERVICE_NAME", "bugwarden-edge");
let cfg = otel::resolve(&OtelEnv::from_env())
.expect("an endpoint resolves")
.expect("an endpoint means export is on");
assert_eq!(
cfg.service_name(),
"bugwarden-edge",
"OTEL_SERVICE_NAME must reach the exported resource"
);
std::env::set_var("OTEL_EXPORTER_OTLP_ENDPOINT", "");
assert!(
otel::resolve(&OtelEnv::from_env())
.expect("an emptied endpoint resolves")
.is_none(),
"OTEL_EXPORTER_OTLP_ENDPOINT= must read as unset, leaving export off"
);
std::env::set_var("OTEL_EXPORTER_OTLP_PROTOCOL", "grpc");
assert!(
otel::resolve(&OtelEnv::from_env())
.expect("no endpoint decides everything")
.is_none(),
"with export off the protocol is not consulted"
);
std::env::set_var(
"OTEL_EXPORTER_OTLP_ENDPOINT",
"http://collector.example:4318",
);
let err = otel::resolve(&OtelEnv::from_env())
.err()
.expect("grpc must be refused");
assert!(
format!("{err}").contains("OTEL_EXPORTER_OTLP_PROTOCOL"),
"the refusal must name the variable: {err}"
);
clear();
std::env::set_var("BUGWARDEN_AUDIT_CONFIG", "/etc/bugwarden/audit.toml");
let cli = parse(&[]).expect("a config path parses");
assert_eq!(
select_sinks(cli.audit_config.as_deref(), false).expect("file only"),
SinkSelection::FileOnly,
"BUGWARDEN_AUDIT_CONFIG must reach the sink selection"
);
assert_eq!(
select_sinks(cli.audit_config.as_deref(), true).expect("both sinks"),
SinkSelection::Both
);
std::env::set_var("BUGWARDEN_AUDIT_CONFIG", "none");
let cli = parse(&[]).expect("the sentinel parses");
assert_eq!(
select_sinks(cli.audit_config.as_deref(), true).expect("otlp only"),
SinkSelection::OtlpOnly,
"BUGWARDEN_AUDIT_CONFIG=none must select the fileless sink"
);
assert!(
select_sinks(cli.audit_config.as_deref(), false).is_err(),
"`none` with no OTLP endpoint must refuse startup, never run sinkless"
);
let cli = parse(&["--audit-config", "/from/cli.toml"]).expect("the flag parses");
assert_eq!(
cli.audit_config.as_deref(),
Some(Path::new("/from/cli.toml"))
);
clear();
let cli = parse(&[]).expect("no variable parses");
assert!(
select_sinks(cli.audit_config.as_deref(), true).is_err(),
"an OTLP endpoint without a file decision must refuse startup"
);
assert_eq!(
select_sinks(cli.audit_config.as_deref(), false).expect("no audit"),
SinkSelection::NoAudit
);
clear();
std::env::set_var(
"OTEL_EXPORTER_OTLP_LOGS_ENDPOINT",
"http://collector.example:4318/otlp/v1/logs",
);
let cfg = otel::resolve(&OtelEnv::from_env())
.expect("a logs endpoint resolves")
.expect("a logs endpoint alone means export is on");
assert_eq!(cfg.service_name(), "bugwarden");
clear();
}