Implement CLI argument parsing with clap and config file loading
- Add Cli struct with clap derive macros for --config, --validate, --allow-wildcard-bind flags - Config loading: reads TOML, deserializes into StaticConfig + DynamicConfig, validates - --validate: load, validate, print success/errors, exit 0 or 1 - --allow-wildcard-bind is OR'd with config allow_wildcard_bind field - Default config path: /etc/reverse-proxy/config.toml - Version from Cargo.toml via clap - Unit tests for CLI argument parsing and config loading - Integration tests for --validate with valid/invalid config and --allow-wildcard-bind
This commit is contained in:
387
src/cli.rs
Normal file
387
src/cli.rs
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use anyhow::{Context, Result};
|
||||||
|
use clap::Parser;
|
||||||
|
|
||||||
|
use crate::config::dynamic_config::{
|
||||||
|
BodyConfig, DynamicConfig, RateLimitConfig, SerializableDynamicConfig,
|
||||||
|
};
|
||||||
|
use crate::config::static_config::StaticConfig;
|
||||||
|
use crate::config::validation::validate;
|
||||||
|
|
||||||
|
const DEFAULT_CONFIG_PATH: &str = "/etc/reverse-proxy/config.toml";
|
||||||
|
|
||||||
|
#[derive(Debug, Parser)]
|
||||||
|
#[command(name = "reverse-proxy", version, about = "Reverse proxy server")]
|
||||||
|
pub struct Cli {
|
||||||
|
#[arg(long, default_value = DEFAULT_CONFIG_PATH, help = "Path to config file")]
|
||||||
|
pub config: String,
|
||||||
|
|
||||||
|
#[arg(long, help = "Validate config and exit")]
|
||||||
|
pub validate: bool,
|
||||||
|
|
||||||
|
#[arg(
|
||||||
|
long,
|
||||||
|
help = "Permit 0.0.0.0 as a bind address (for container deployments)"
|
||||||
|
)]
|
||||||
|
pub allow_wildcard_bind: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct LoadedConfig {
|
||||||
|
pub static_config: StaticConfig,
|
||||||
|
pub dynamic_config: DynamicConfig,
|
||||||
|
pub allow_wildcard_bind: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse() -> Cli {
|
||||||
|
Cli::parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn parse_from<I, S>(args: I) -> Cli
|
||||||
|
where
|
||||||
|
I: IntoIterator<Item = S>,
|
||||||
|
S: Into<std::ffi::OsString> + Clone,
|
||||||
|
{
|
||||||
|
Cli::parse_from(args)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, serde::Deserialize)]
|
||||||
|
struct RawConfig {
|
||||||
|
#[serde(default)]
|
||||||
|
listeners: Vec<crate::config::static_config::ListenerConfig>,
|
||||||
|
#[serde(default)]
|
||||||
|
allow_wildcard_bind: bool,
|
||||||
|
#[serde(default = "crate::config::static_config::default_health_check_port")]
|
||||||
|
health_check_port: u16,
|
||||||
|
#[serde(default = "crate::config::static_config::default_admin_socket_path")]
|
||||||
|
admin_socket_path: String,
|
||||||
|
#[serde(default = "crate::config::static_config::default_shutdown_timeout_secs")]
|
||||||
|
shutdown_timeout_secs: u64,
|
||||||
|
#[serde(default)]
|
||||||
|
logging: crate::config::static_config::LoggingConfig,
|
||||||
|
rate_limit: RateLimitConfig,
|
||||||
|
body: BodyConfig,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load_config(cli: &Cli) -> Result<LoadedConfig> {
|
||||||
|
let config_path = Path::new(&cli.config);
|
||||||
|
let config_content = std::fs::read_to_string(config_path)
|
||||||
|
.with_context(|| format!("failed to read config file: {}", cli.config))?;
|
||||||
|
|
||||||
|
let raw: RawConfig = toml::from_str(&config_content)
|
||||||
|
.with_context(|| format!("failed to parse config file: {}", cli.config))?;
|
||||||
|
|
||||||
|
let static_config = StaticConfig {
|
||||||
|
listeners: raw.listeners,
|
||||||
|
allow_wildcard_bind: raw.allow_wildcard_bind,
|
||||||
|
health_check_port: raw.health_check_port,
|
||||||
|
admin_socket_path: raw.admin_socket_path,
|
||||||
|
shutdown_timeout_secs: raw.shutdown_timeout_secs,
|
||||||
|
logging: raw.logging,
|
||||||
|
};
|
||||||
|
|
||||||
|
let serializable_dynamic = SerializableDynamicConfig {
|
||||||
|
sites: collect_sites(&static_config),
|
||||||
|
rate_limit: raw.rate_limit,
|
||||||
|
body: raw.body,
|
||||||
|
};
|
||||||
|
|
||||||
|
let dynamic_config: DynamicConfig = serializable_dynamic.into();
|
||||||
|
|
||||||
|
let allow_wildcard_bind = static_config.allow_wildcard_bind || cli.allow_wildcard_bind;
|
||||||
|
|
||||||
|
validate(&static_config, &dynamic_config, cli.allow_wildcard_bind).map_err(|errors| {
|
||||||
|
anyhow::anyhow!(
|
||||||
|
"config validation failed:\n{}",
|
||||||
|
errors
|
||||||
|
.iter()
|
||||||
|
.map(|e| format!(" - {}", e))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n")
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(LoadedConfig {
|
||||||
|
static_config,
|
||||||
|
dynamic_config,
|
||||||
|
allow_wildcard_bind,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_sites(static_config: &StaticConfig) -> Vec<crate::config::dynamic_config::SiteConfig> {
|
||||||
|
let mut sites = Vec::new();
|
||||||
|
for listener in &static_config.listeners {
|
||||||
|
sites.extend(listener.sites.clone());
|
||||||
|
}
|
||||||
|
sites
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run_validate(cli: &Cli) -> Result<()> {
|
||||||
|
match load_config(cli) {
|
||||||
|
Ok(_) => {
|
||||||
|
println!("Configuration is valid.");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("{}", e);
|
||||||
|
Err(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_default_config_path() {
|
||||||
|
let cli = Cli::parse_from(["reverse-proxy"]);
|
||||||
|
assert_eq!(cli.config, DEFAULT_CONFIG_PATH);
|
||||||
|
assert!(!cli.validate);
|
||||||
|
assert!(!cli.allow_wildcard_bind);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_custom_config_path() {
|
||||||
|
let cli = Cli::parse_from(["reverse-proxy", "--config", "/tmp/test.toml"]);
|
||||||
|
assert_eq!(cli.config, "/tmp/test.toml");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_validate_flag() {
|
||||||
|
let cli = Cli::parse_from(["reverse-proxy", "--validate"]);
|
||||||
|
assert!(cli.validate);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_allow_wildcard_bind_flag() {
|
||||||
|
let cli = Cli::parse_from(["reverse-proxy", "--allow-wildcard-bind"]);
|
||||||
|
assert!(cli.allow_wildcard_bind);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_all_flags() {
|
||||||
|
let cli = Cli::parse_from([
|
||||||
|
"reverse-proxy",
|
||||||
|
"--config",
|
||||||
|
"/custom/path.toml",
|
||||||
|
"--validate",
|
||||||
|
"--allow-wildcard-bind",
|
||||||
|
]);
|
||||||
|
assert_eq!(cli.config, "/custom/path.toml");
|
||||||
|
assert!(cli.validate);
|
||||||
|
assert!(cli.allow_wildcard_bind);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cli_version_flag() {
|
||||||
|
let result = Cli::try_parse_from(["reverse-proxy", "--version"]);
|
||||||
|
assert!(result.is_err());
|
||||||
|
if let Err(err) = result {
|
||||||
|
use clap::error::ErrorKind;
|
||||||
|
assert!(matches!(err.kind(), ErrorKind::DisplayVersion));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_missing_file() {
|
||||||
|
let cli = Cli::parse_from(["reverse-proxy", "--config", "/nonexistent/config.toml"]);
|
||||||
|
let result = load_config(&cli);
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(err_msg.contains("failed to read config file") || err_msg.contains("No such file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_invalid_toml() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let path = dir.path().join("bad.toml");
|
||||||
|
std::fs::write(&path, "not valid toml {{{{").unwrap();
|
||||||
|
let cli = Cli::parse_from(["reverse-proxy", "--config", path.to_str().unwrap()]);
|
||||||
|
let result = load_config(&cli);
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(err_msg.contains("failed to parse config file"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_valid_file() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let cert_path = dir.path().join("cert.pem");
|
||||||
|
let key_path = dir.path().join("key.pem");
|
||||||
|
std::fs::write(&cert_path, "cert").unwrap();
|
||||||
|
std::fs::write(&key_path, "key").unwrap();
|
||||||
|
|
||||||
|
let toml = format!(
|
||||||
|
r#"
|
||||||
|
[rate_limit]
|
||||||
|
requests_per_second = 10
|
||||||
|
burst = 20
|
||||||
|
|
||||||
|
[body]
|
||||||
|
limit_bytes = 104857600
|
||||||
|
|
||||||
|
[[listeners]]
|
||||||
|
bind_addr = "127.0.0.1"
|
||||||
|
http_port = 80
|
||||||
|
https_port = 443
|
||||||
|
|
||||||
|
[listeners.tls]
|
||||||
|
mode = "manual"
|
||||||
|
cert_path = "{}"
|
||||||
|
key_path = "{}"
|
||||||
|
|
||||||
|
[[listeners.sites]]
|
||||||
|
host = "test.local"
|
||||||
|
upstream = "127.0.0.1:8080"
|
||||||
|
"#,
|
||||||
|
cert_path.to_str().unwrap(),
|
||||||
|
key_path.to_str().unwrap()
|
||||||
|
);
|
||||||
|
let config_path = dir.path().join("config.toml");
|
||||||
|
std::fs::write(&config_path, toml).unwrap();
|
||||||
|
|
||||||
|
let cli = Cli::parse_from(["reverse-proxy", "--config", config_path.to_str().unwrap()]);
|
||||||
|
let result = load_config(&cli);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let loaded = result.unwrap();
|
||||||
|
assert!(!loaded.allow_wildcard_bind);
|
||||||
|
assert_eq!(loaded.static_config.listeners.len(), 1);
|
||||||
|
assert_eq!(loaded.dynamic_config.sites.len(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_wildcard_bind_or_logic() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
let toml = r#"
|
||||||
|
allow_wildcard_bind = false
|
||||||
|
|
||||||
|
[rate_limit]
|
||||||
|
requests_per_second = 10
|
||||||
|
burst = 20
|
||||||
|
|
||||||
|
[body]
|
||||||
|
limit_bytes = 104857600
|
||||||
|
|
||||||
|
[[listeners]]
|
||||||
|
bind_addr = "0.0.0.0"
|
||||||
|
http_port = 80
|
||||||
|
https_port = 443
|
||||||
|
|
||||||
|
[listeners.tls]
|
||||||
|
mode = "acme"
|
||||||
|
acme_domains = ["test.local"]
|
||||||
|
acme_cache_dir = "/tmp/acme"
|
||||||
|
"#;
|
||||||
|
let config_path = dir.path().join("config.toml");
|
||||||
|
std::fs::write(&config_path, toml).unwrap();
|
||||||
|
|
||||||
|
let cli = Cli::parse_from([
|
||||||
|
"reverse-proxy",
|
||||||
|
"--config",
|
||||||
|
config_path.to_str().unwrap(),
|
||||||
|
"--allow-wildcard-bind",
|
||||||
|
]);
|
||||||
|
let result = load_config(&cli);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
let loaded = result.unwrap();
|
||||||
|
assert!(loaded.allow_wildcard_bind);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_wildcard_bind_rejected_without_flag() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
let toml = r#"
|
||||||
|
[rate_limit]
|
||||||
|
requests_per_second = 10
|
||||||
|
burst = 20
|
||||||
|
|
||||||
|
[body]
|
||||||
|
limit_bytes = 104857600
|
||||||
|
|
||||||
|
[[listeners]]
|
||||||
|
bind_addr = "0.0.0.0"
|
||||||
|
http_port = 80
|
||||||
|
https_port = 443
|
||||||
|
|
||||||
|
[listeners.tls]
|
||||||
|
mode = "acme"
|
||||||
|
acme_domains = ["test.local"]
|
||||||
|
acme_cache_dir = "/tmp/acme"
|
||||||
|
"#;
|
||||||
|
let config_path = dir.path().join("config.toml");
|
||||||
|
std::fs::write(&config_path, toml).unwrap();
|
||||||
|
|
||||||
|
let cli = Cli::parse_from(["reverse-proxy", "--config", config_path.to_str().unwrap()]);
|
||||||
|
let result = load_config(&cli);
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(err_msg.contains("config validation failed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_validation_fails_reports_errors() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
let toml = r#"
|
||||||
|
[rate_limit]
|
||||||
|
requests_per_second = 0
|
||||||
|
burst = 20
|
||||||
|
|
||||||
|
[body]
|
||||||
|
limit_bytes = 0
|
||||||
|
"#;
|
||||||
|
let config_path = dir.path().join("config.toml");
|
||||||
|
std::fs::write(&config_path, toml).unwrap();
|
||||||
|
|
||||||
|
let cli = Cli::parse_from(["reverse-proxy", "--config", config_path.to_str().unwrap()]);
|
||||||
|
let result = load_config(&cli);
|
||||||
|
assert!(result.is_err());
|
||||||
|
let err_msg = result.unwrap_err().to_string();
|
||||||
|
assert!(err_msg.contains("config validation failed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn load_config_allow_wildcard_from_config_only() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
|
let toml = r#"
|
||||||
|
allow_wildcard_bind = true
|
||||||
|
|
||||||
|
[rate_limit]
|
||||||
|
requests_per_second = 10
|
||||||
|
burst = 20
|
||||||
|
|
||||||
|
[body]
|
||||||
|
limit_bytes = 104857600
|
||||||
|
|
||||||
|
[[listeners]]
|
||||||
|
bind_addr = "0.0.0.0"
|
||||||
|
http_port = 80
|
||||||
|
https_port = 443
|
||||||
|
|
||||||
|
[listeners.tls]
|
||||||
|
mode = "acme"
|
||||||
|
acme_domains = ["test.local"]
|
||||||
|
acme_cache_dir = "/tmp/acme"
|
||||||
|
"#;
|
||||||
|
let config_path = dir.path().join("config.toml");
|
||||||
|
std::fs::write(&config_path, toml).unwrap();
|
||||||
|
|
||||||
|
let cli = Cli::parse_from(["reverse-proxy", "--config", config_path.to_str().unwrap()]);
|
||||||
|
let result = load_config(&cli);
|
||||||
|
assert!(result.is_ok());
|
||||||
|
assert!(result.unwrap().allow_wildcard_bind);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_from_vec() {
|
||||||
|
let args = vec!["reverse-proxy", "--validate", "--allow-wildcard-bind"];
|
||||||
|
let cli = parse_from(args);
|
||||||
|
assert!(cli.validate);
|
||||||
|
assert!(cli.allow_wildcard_bind);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,18 +16,15 @@ pub struct StaticConfig {
|
|||||||
pub logging: LoggingConfig,
|
pub logging: LoggingConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
pub fn default_health_check_port() -> u16 {
|
||||||
fn default_health_check_port() -> u16 {
|
|
||||||
9900
|
9900
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
pub fn default_admin_socket_path() -> String {
|
||||||
fn default_admin_socket_path() -> String {
|
|
||||||
"/run/reverse-proxy/admin.sock".to_string()
|
"/run/reverse-proxy/admin.sock".to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
pub fn default_shutdown_timeout_secs() -> u64 {
|
||||||
fn default_shutdown_timeout_secs() -> u64 {
|
|
||||||
30
|
30
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
pub mod admin;
|
pub mod admin;
|
||||||
|
pub mod cli;
|
||||||
pub mod config;
|
pub mod config;
|
||||||
pub mod health;
|
pub mod health;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
|
|||||||
19
src/main.rs
19
src/main.rs
@@ -1,3 +1,22 @@
|
|||||||
|
use reverse_proxy::cli;
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
let args = cli::parse();
|
||||||
|
|
||||||
|
if args.validate {
|
||||||
|
match cli::run_validate(&args) {
|
||||||
|
Ok(()) => std::process::exit(0),
|
||||||
|
Err(_) => std::process::exit(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match cli::load_config(&args) {
|
||||||
|
Ok(_config) => {
|
||||||
tracing::info!("reverse-proxy starting");
|
tracing::info!("reverse-proxy starting");
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("error: {e:#}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
mod helpers;
|
mod helpers;
|
||||||
|
|
||||||
|
use std::process::Command;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
@@ -248,3 +249,144 @@ async fn test_rate_limit_eviction_task() {
|
|||||||
|
|
||||||
handle.abort();
|
handle.abort();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn write_valid_config(dir: &std::path::Path) -> std::path::PathBuf {
|
||||||
|
let cert_path = dir.join("cert.pem");
|
||||||
|
let key_path = dir.join("key.pem");
|
||||||
|
std::fs::write(&cert_path, "cert").unwrap();
|
||||||
|
std::fs::write(&key_path, "key").unwrap();
|
||||||
|
|
||||||
|
let toml = format!(
|
||||||
|
r#"
|
||||||
|
[rate_limit]
|
||||||
|
requests_per_second = 10
|
||||||
|
burst = 20
|
||||||
|
|
||||||
|
[body]
|
||||||
|
limit_bytes = 104857600
|
||||||
|
|
||||||
|
[[listeners]]
|
||||||
|
bind_addr = "127.0.0.1"
|
||||||
|
http_port = 80
|
||||||
|
https_port = 443
|
||||||
|
|
||||||
|
[listeners.tls]
|
||||||
|
mode = "manual"
|
||||||
|
cert_path = "{}"
|
||||||
|
key_path = "{}"
|
||||||
|
|
||||||
|
[[listeners.sites]]
|
||||||
|
host = "test.local"
|
||||||
|
upstream = "127.0.0.1:8080"
|
||||||
|
"#,
|
||||||
|
cert_path.to_str().unwrap(),
|
||||||
|
key_path.to_str().unwrap()
|
||||||
|
);
|
||||||
|
let config_path = dir.join("valid_config.toml");
|
||||||
|
std::fs::write(&config_path, toml).unwrap();
|
||||||
|
config_path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_invalid_config(dir: &std::path::Path) -> std::path::PathBuf {
|
||||||
|
let toml = r#"
|
||||||
|
[rate_limit]
|
||||||
|
requests_per_second = 0
|
||||||
|
burst = 20
|
||||||
|
|
||||||
|
[body]
|
||||||
|
limit_bytes = 0
|
||||||
|
"#;
|
||||||
|
let config_path = dir.join("invalid_config.toml");
|
||||||
|
std::fs::write(&config_path, toml).unwrap();
|
||||||
|
config_path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn binary_path() -> std::path::PathBuf {
|
||||||
|
let bin = env!("CARGO_BIN_EXE_reverse-proxy");
|
||||||
|
std::path::PathBuf::from(bin)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_valid_config_exits_0() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let config_path = write_valid_config(dir.path());
|
||||||
|
let output = Command::new(binary_path())
|
||||||
|
.arg("--config")
|
||||||
|
.arg(config_path.to_str().unwrap())
|
||||||
|
.arg("--validate")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"expected exit 0, got {}: stderr={}",
|
||||||
|
output.status,
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
assert!(stdout.contains("valid"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_invalid_config_exits_1() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let config_path = write_invalid_config(dir.path());
|
||||||
|
let output = Command::new(binary_path())
|
||||||
|
.arg("--config")
|
||||||
|
.arg(config_path.to_str().unwrap())
|
||||||
|
.arg("--validate")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(!output.status.success(), "expected exit 1, got success");
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
assert!(stderr.contains("validation failed") || stderr.contains("error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_missing_config_file_exits_1() {
|
||||||
|
let output = Command::new(binary_path())
|
||||||
|
.arg("--config")
|
||||||
|
.arg("/nonexistent/path/config.toml")
|
||||||
|
.arg("--validate")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(!output.status.success(), "expected exit 1, got success");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_validate_wildcard_bind_via_cli_flag() {
|
||||||
|
let dir = tempfile::tempdir().unwrap();
|
||||||
|
let toml = r#"
|
||||||
|
[rate_limit]
|
||||||
|
requests_per_second = 10
|
||||||
|
burst = 20
|
||||||
|
|
||||||
|
[body]
|
||||||
|
limit_bytes = 104857600
|
||||||
|
|
||||||
|
[[listeners]]
|
||||||
|
bind_addr = "0.0.0.0"
|
||||||
|
http_port = 80
|
||||||
|
https_port = 443
|
||||||
|
|
||||||
|
[listeners.tls]
|
||||||
|
mode = "acme"
|
||||||
|
acme_domains = ["test.local"]
|
||||||
|
acme_cache_dir = "/tmp/acme"
|
||||||
|
"#;
|
||||||
|
let config_path = dir.path().join("wildcard.toml");
|
||||||
|
std::fs::write(&config_path, toml).unwrap();
|
||||||
|
|
||||||
|
let output = Command::new(binary_path())
|
||||||
|
.arg("--config")
|
||||||
|
.arg(config_path.to_str().unwrap())
|
||||||
|
.arg("--validate")
|
||||||
|
.arg("--allow-wildcard-bind")
|
||||||
|
.output()
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
output.status.success(),
|
||||||
|
"expected exit 0 with --allow-wildcard-bind, got {}: stderr={}",
|
||||||
|
output.status,
|
||||||
|
String::from_utf8_lossy(&output.stderr)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user