From 3f16f9260a149045a2b8362b7dc1feef81cf1d20 Mon Sep 17 00:00:00 2001 From: Erick Bourgeois Date: Wed, 14 Jan 2026 14:39:42 -0500 Subject: [PATCH] Properly process config from env vars, then config files for RNDC secret, server/port and algorithm Signed-off-by: Erick Bourgeois --- src/lib.rs | 4 +- src/main.rs | 94 +++---- src/rate_limit.rs | 53 ---- src/rate_limit_test.rs | 49 ++++ src/rndc_conf_parser.rs | 448 +--------------------------------- src/rndc_conf_parser_tests.rs | 444 +++++++++++++++++++++++++++++++++ src/rndc_conf_types.rs | 148 +---------- src/rndc_conf_types_tests.rs | 144 +++++++++++ 8 files changed, 696 insertions(+), 688 deletions(-) create mode 100644 src/rate_limit_test.rs create mode 100644 src/rndc_conf_parser_tests.rs create mode 100644 src/rndc_conf_types_tests.rs diff --git a/src/lib.rs b/src/lib.rs index 66a84b6..0589dda 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -180,13 +180,13 @@ mod metrics_test; #[cfg(test)] mod middleware_test; #[cfg(test)] +mod rate_limit_test; +#[cfg(test)] mod rndc_test; #[cfg(test)] mod rndc_parser_tests; - #[cfg(test)] mod rndc_types_tests; - #[cfg(test)] mod types_test; #[cfg(test)] diff --git a/src/main.rs b/src/main.rs index d610ef0..21d3fa2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -222,50 +222,62 @@ async fn main() -> anyhow::Result<()> { } // get rndc configuration from environment or fallback to rndc.conf - let (rndc_server, rndc_algorithm, rndc_secret) = if let Ok(secret) = - std::env::var("RNDC_SECRET") - { - // environment variables provided - let server = std::env::var("RNDC_SERVER").unwrap_or_else(|_| "127.0.0.1:953".to_string()); - let algorithm = std::env::var("RNDC_ALGORITHM").unwrap_or_else(|_| "sha256".to_string()); - info!("using rndc configuration from environment variables"); - info!("rndc server: {}", server); - info!("rndc algorithm: {}", algorithm); - (server, algorithm, secret) - } else { - // try to parse from rndc.conf files - info!("RNDC_SECRET not set, attempting to parse rndc.conf"); - - let config_paths = vec!["/etc/bind/rndc.conf", "/etc/rndc.conf"]; - let mut config = None; - - for path in &config_paths { - match bindcar::rndc::parse_rndc_conf(path) { - Ok(cfg) => { - info!("successfully parsed rndc configuration from {}", path); - config = Some(cfg); - break; - } - Err(e) => { - debug!("failed to parse {}: {}", path, e); - } - } - } - - match config { - Some(cfg) => { - info!("rndc server: {}", cfg.server); - info!("rndc algorithm: {}", cfg.algorithm); - (cfg.server, cfg.algorithm, cfg.secret) + // Each parameter is checked independently - env var takes priority, then rndc.conf + + // Try to parse rndc.conf file first to have fallback values + let config_paths = vec!["/etc/bind/rndc.conf", "/etc/rndc.conf"]; + let mut parsed_config = None; + + for path in &config_paths { + match bindcar::rndc::parse_rndc_conf(path) { + Ok(cfg) => { + info!("successfully parsed rndc configuration from {}", path); + parsed_config = Some(cfg); + break; } - None => { - error!("rndc configuration not found!"); - error!("either set RNDC_SECRET environment variable or ensure /etc/bind/rndc.conf exists"); - return Err(anyhow::anyhow!( - "rndc configuration required: set RNDC_SECRET env var or create /etc/bind/rndc.conf" - )); + Err(e) => { + debug!("failed to parse {}: {}", path, e); } } + } + + // Check each parameter independently: env var first, then rndc.conf, then hardcoded default + let rndc_server = if let Ok(server) = std::env::var("RNDC_SERVER") { + info!("using RNDC_SERVER from environment: {}", server); + server + } else if let Some(ref cfg) = parsed_config { + info!("using server from rndc.conf: {}", cfg.server); + cfg.server.clone() + } else { + let default = "127.0.0.1:953".to_string(); + warn!("using default RNDC_SERVER: {}", default); + default + }; + + let rndc_algorithm = if let Ok(algorithm) = std::env::var("RNDC_ALGORITHM") { + info!("using RNDC_ALGORITHM from environment: {}", algorithm); + algorithm + } else if let Some(ref cfg) = parsed_config { + info!("using algorithm from rndc.conf: {}", cfg.algorithm); + cfg.algorithm.clone() + } else { + let default = "sha256".to_string(); + warn!("using default RNDC_ALGORITHM: {}", default); + default + }; + + let rndc_secret = if let Ok(secret) = std::env::var("RNDC_SECRET") { + info!("using RNDC_SECRET from environment"); + secret + } else if let Some(ref cfg) = parsed_config { + info!("using secret from rndc.conf"); + cfg.secret.clone() + } else { + error!("rndc configuration not found!"); + error!("either set RNDC_SECRET environment variable or ensure /etc/bind/rndc.conf exists"); + return Err(anyhow::anyhow!( + "rndc configuration required: set RNDC_SECRET env var or create /etc/bind/rndc.conf" + )); }; // verify zone directory exists diff --git a/src/rate_limit.rs b/src/rate_limit.rs index de3246f..144f4f5 100644 --- a/src/rate_limit.rs +++ b/src/rate_limit.rs @@ -87,56 +87,3 @@ impl RateLimitConfig { Ok(()) } } - -#[cfg(test)] -mod rate_limit_test { - use super::*; - - #[test] - fn test_rate_limit_config_default() { - let config = RateLimitConfig::default(); - assert_eq!(config.requests_per_period, 100); - assert_eq!(config.period_secs, 60); - assert_eq!(config.burst_size, 10); - assert!(config.enabled); - } - - #[test] - fn test_rate_limit_config_validation() { - let config = RateLimitConfig::default(); - assert!(config.validate().is_ok()); - - let invalid_config = RateLimitConfig { - requests_per_period: 0, - ..Default::default() - }; - assert!(invalid_config.validate().is_err()); - - let invalid_config = RateLimitConfig { - period_secs: 0, - ..Default::default() - }; - assert!(invalid_config.validate().is_err()); - - let invalid_config = RateLimitConfig { - burst_size: 0, - ..Default::default() - }; - assert!(invalid_config.validate().is_err()); - } - - #[test] - fn test_rate_limit_config_from_env() { - // Test with no env vars set - should use defaults - std::env::remove_var("RATE_LIMIT_ENABLED"); - std::env::remove_var("RATE_LIMIT_REQUESTS"); - std::env::remove_var("RATE_LIMIT_PERIOD_SECS"); - std::env::remove_var("RATE_LIMIT_BURST"); - - let config = RateLimitConfig::from_env(); - assert_eq!(config.requests_per_period, 100); - assert_eq!(config.period_secs, 60); - assert_eq!(config.burst_size, 10); - assert!(config.enabled); - } -} diff --git a/src/rate_limit_test.rs b/src/rate_limit_test.rs new file mode 100644 index 0000000..c859527 --- /dev/null +++ b/src/rate_limit_test.rs @@ -0,0 +1,49 @@ +use crate::rate_limit::RateLimitConfig; + +#[test] +fn test_rate_limit_config_default() { + let config = RateLimitConfig::default(); + assert_eq!(config.requests_per_period, 100); + assert_eq!(config.period_secs, 60); + assert_eq!(config.burst_size, 10); + assert!(config.enabled); +} + +#[test] +fn test_rate_limit_config_validation() { + let config = RateLimitConfig::default(); + assert!(config.validate().is_ok()); + + let invalid_config = RateLimitConfig { + requests_per_period: 0, + ..Default::default() + }; + assert!(invalid_config.validate().is_err()); + + let invalid_config = RateLimitConfig { + period_secs: 0, + ..Default::default() + }; + assert!(invalid_config.validate().is_err()); + + let invalid_config = RateLimitConfig { + burst_size: 0, + ..Default::default() + }; + assert!(invalid_config.validate().is_err()); +} + +#[test] +fn test_rate_limit_config_from_env() { + // Test with no env vars set - should use defaults + std::env::remove_var("RATE_LIMIT_ENABLED"); + std::env::remove_var("RATE_LIMIT_REQUESTS"); + std::env::remove_var("RATE_LIMIT_PERIOD_SECS"); + std::env::remove_var("RATE_LIMIT_BURST"); + + let config = RateLimitConfig::from_env(); + assert_eq!(config.requests_per_period, 100); + assert_eq!(config.period_secs, 60); + assert_eq!(config.burst_size, 10); + assert!(config.enabled); +} diff --git a/src/rndc_conf_parser.rs b/src/rndc_conf_parser.rs index 32a12d9..ab2be21 100644 --- a/src/rndc_conf_parser.rs +++ b/src/rndc_conf_parser.rs @@ -603,449 +603,5 @@ fn parse_rndc_conf_file_recursive( } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_line_comment() { - assert!(line_comment("// comment\n").is_ok()); - assert!(line_comment("// comment").is_ok()); - } - - #[test] - fn test_hash_comment() { - assert!(hash_comment("# comment\n").is_ok()); - } - - #[test] - fn test_block_comment() { - assert!(block_comment("/* comment */").is_ok()); - assert!(block_comment("/* multi\nline */").is_ok()); - } - - #[test] - fn test_quoted_string() { - assert_eq!(quoted_string(r#""hello""#).unwrap().1, "hello"); - assert_eq!(quoted_string(r#""hello world""#).unwrap().1, "hello world"); - } - - #[test] - fn test_quoted_string_with_escapes() { - assert_eq!(quoted_string(r#""hello\"world""#).unwrap().1, "hello\"world"); - assert_eq!(quoted_string(r#""line1\nline2""#).unwrap().1, "line1\nline2"); - } - - #[test] - fn test_identifier() { - assert_eq!(identifier("hmac-sha256").unwrap().1, "hmac-sha256"); - assert_eq!(identifier("rndc-key").unwrap().1, "rndc-key"); - assert_eq!(identifier("localhost").unwrap().1, "localhost"); - } - - #[test] - fn test_ipv4_addr() { - let result = ipv4_addr("192.168.1.1").unwrap().1; - assert_eq!(result, "192.168.1.1".parse::().unwrap()); - } - - #[test] - fn test_ipv6_addr() { - let result = ipv6_addr("2001:db8::1").unwrap().1; - assert_eq!(result, "2001:db8::1".parse::().unwrap()); - } - - #[test] - fn test_port_number() { - assert_eq!(port_number("953").unwrap().1, 953); - assert_eq!(port_number("8080").unwrap().1, 8080); - } - - #[test] - fn test_parse_key_block() { - let input = r#"key "rndc-key" { - algorithm hmac-sha256; - secret "dGVzdC1zZWNyZXQ="; - };"#; - - let (_, (name, key)) = parse_key_block(input).unwrap(); - assert_eq!(name, "rndc-key"); - assert_eq!(key.algorithm, "hmac-sha256"); - assert_eq!(key.secret, "dGVzdC1zZWNyZXQ="); - } - - #[test] - fn test_parse_server_block() { - let input = r#"server localhost { - key "rndc-key"; - port 953; - };"#; - - let (_, (addr, server)) = parse_server_block(input).unwrap(); - assert_eq!(addr, "localhost"); - assert_eq!(server.key, Some("rndc-key".to_string())); - assert_eq!(server.port, Some(953)); - } - - #[test] - fn test_parse_options_block() { - let input = r#"options { - default-server localhost; - default-key "rndc-key"; - default-port 953; - };"#; - - let (_, options) = parse_options_block(input).unwrap(); - assert_eq!(options.default_server, Some("localhost".to_string())); - assert_eq!(options.default_key, Some("rndc-key".to_string())); - assert_eq!(options.default_port, Some(953)); - } - - #[test] - fn test_parse_include_stmt() { - let input = r#"include "/etc/bind/rndc.key";"#; - let (_, path) = parse_include_stmt(input).unwrap(); - assert_eq!(path, PathBuf::from("/etc/bind/rndc.key")); - } - - #[test] - fn test_parse_complete_conf() { - let input = r#" - # Example rndc.conf - include "/etc/bind/rndc.key"; - - key "rndc-key" { - algorithm hmac-sha256; - secret "dGVzdC1zZWNyZXQ="; - }; - - server localhost { - key "rndc-key"; - port 953; - }; - - options { - default-server localhost; - default-key "rndc-key"; - }; - "#; - - let conf = parse_rndc_conf_str(input).unwrap(); - assert_eq!(conf.keys.len(), 1); - assert_eq!(conf.servers.len(), 1); - assert_eq!(conf.includes.len(), 1); - assert_eq!(conf.options.default_server, Some("localhost".to_string())); - } - - #[test] - fn test_parse_with_comments() { - let input = r#" - // Line comment - # Hash comment - /* Block comment */ - key "test-key" { - algorithm hmac-sha256; // inline comment - secret "secret"; # another comment - }; - "#; - - let conf = parse_rndc_conf_str(input).unwrap(); - assert_eq!(conf.keys.len(), 1); - } - - #[test] - fn test_roundtrip() { - let input = r#" - key "rndc-key" { - algorithm hmac-sha256; - secret "dGVzdC1zZWNyZXQ="; - }; - - options { - default-server localhost; - default-key "rndc-key"; - }; - "#; - - let conf = parse_rndc_conf_str(input).unwrap(); - let serialized = conf.to_conf_file(); - let conf2 = parse_rndc_conf_str(&serialized).unwrap(); - - assert_eq!(conf.keys.len(), conf2.keys.len()); - assert_eq!(conf.options.default_server, conf2.options.default_server); - } - - // Error handling tests - #[test] - fn test_parse_empty_input() { - let input = ""; - let result = parse_rndc_conf_str(input); - assert!(result.is_ok()); - let conf = result.unwrap(); - assert_eq!(conf.keys.len(), 0); - } - - #[test] - fn test_parse_incomplete_key_block() { - // Test with no secret field - should still parse with default - let input = r#"key "test-key" { };"#; - let result = parse_rndc_conf_str(input); - assert!(result.is_ok()); - let conf = result.unwrap(); - assert!(conf.keys.contains_key("test-key")); - } - - #[test] - fn test_parse_invalid_ip_address() { - let input = "999.999.999.999"; - let result = ip_addr(input); - assert!(result.is_err()); - } - - #[test] - fn test_parse_server_with_ipv6() { - let input = r#"server 2001:db8::1 { - key "rndc-key"; - port 953; - };"#; - - let (_, (addr, server)) = parse_server_block(input).unwrap(); - assert!(addr.contains("2001:db8::1")); - assert_eq!(server.key, Some("rndc-key".to_string())); - assert_eq!(server.port, Some(953)); - } - - #[test] - fn test_parse_empty_options_block() { - let input = r#"options { };"#; - let (_, options) = parse_options_block(input).unwrap(); - assert!(options.is_empty()); - } - - #[test] - fn test_parse_key_block_without_algorithm() { - let input = r#"key "test-key" { - secret "dGVzdA=="; - };"#; - - let (_, (name, key)) = parse_key_block(input).unwrap(); - assert_eq!(name, "test-key"); - assert_eq!(key.algorithm, "hmac-sha256"); // Should default - assert_eq!(key.secret, "dGVzdA=="); - } - - #[test] - fn test_parse_server_block_full() { - // Test server block with key and port - let input = r#"server 192.168.1.1 { - key "test-key"; - port 8953; - };"#; - - let (_, (addr, server)) = parse_server_block(input).unwrap(); - assert!(addr.contains("192.168.1.1")); - assert_eq!(server.key, Some("test-key".to_string())); - assert_eq!(server.port, Some(8953)); - } - - #[test] - fn test_parse_multiple_keys() { - let input = r#" - key "key1" { - algorithm hmac-sha256; - secret "secret1"; - }; - - key "key2" { - algorithm hmac-md5; - secret "secret2"; - }; - "#; - - let conf = parse_rndc_conf_str(input).unwrap(); - assert_eq!(conf.keys.len(), 2); - assert!(conf.keys.contains_key("key1")); - assert!(conf.keys.contains_key("key2")); - } - - #[test] - fn test_parse_multiple_servers() { - let input = r#" - server 127.0.0.1 { - key "key1"; - }; - - server localhost { - key "key2"; - port 8953; - }; - "#; - - let conf = parse_rndc_conf_str(input).unwrap(); - assert_eq!(conf.servers.len(), 2); - } - - #[test] - fn test_error_display() { - let error = RndcConfParseError::MissingField("algorithm".to_string()); - assert_eq!(error.to_string(), "Missing required field: algorithm"); - - let error = RndcConfParseError::CircularInclude("/path/to/file".to_string()); - assert_eq!(error.to_string(), "Circular include detected: /path/to/file"); - } - - // File-based parsing tests - #[test] - fn test_parse_file_not_found() { - let result = parse_rndc_conf_file(Path::new("/nonexistent/path/rndc.conf")); - assert!(result.is_err()); - match result { - Err(RndcConfParseError::FileNotFound(_)) => {} - _ => panic!("Expected FileNotFound error"), - } - } - - #[test] - fn test_parse_file_with_includes() { - use std::fs; - use std::io::Write; - use tempfile::TempDir; - - // Create temporary directory - let temp_dir = TempDir::new().unwrap(); - let main_file = temp_dir.path().join("rndc.conf"); - let include_file = temp_dir.path().join("rndc.key"); - - // Write included file - let mut file = fs::File::create(&include_file).unwrap(); - writeln!( - file, - r#"key "rndc-key" {{ - algorithm hmac-sha256; - secret "dGVzdC1zZWNyZXQ="; -}};"# - ) - .unwrap(); - - // Write main file with include directive - let mut file = fs::File::create(&main_file).unwrap(); - writeln!(file, r#"include "{}";"#, include_file.display()).unwrap(); - writeln!( - file, - r#"options {{ - default-server localhost; - default-key "rndc-key"; -}};"# - ) - .unwrap(); - - // Parse the file - let conf = parse_rndc_conf_file(&main_file).unwrap(); - assert_eq!(conf.keys.len(), 1); - assert!(conf.keys.contains_key("rndc-key")); - assert_eq!(conf.options.default_server, Some("localhost".to_string())); - assert_eq!(conf.includes.len(), 1); - } - - #[test] - fn test_parse_file_circular_include() { - use std::fs; - use std::io::Write; - use tempfile::TempDir; - - // Create temporary directory - let temp_dir = TempDir::new().unwrap(); - let file1 = temp_dir.path().join("file1.conf"); - let file2 = temp_dir.path().join("file2.conf"); - - // Create circular includes: file1 -> file2 -> file1 - let mut f = fs::File::create(&file1).unwrap(); - writeln!(f, r#"include "{}";"#, file2.display()).unwrap(); - - let mut f = fs::File::create(&file2).unwrap(); - writeln!(f, r#"include "{}";"#, file1.display()).unwrap(); - - // Try to parse - let result = parse_rndc_conf_file(&file1); - assert!(result.is_err()); - match result { - Err(RndcConfParseError::CircularInclude(_)) => {} - _ => panic!("Expected CircularInclude error"), - } - } - - #[test] - fn test_parse_file_relative_include() { - use std::fs; - use std::io::Write; - use tempfile::TempDir; - - // Create temporary directory - let temp_dir = TempDir::new().unwrap(); - let subdir = temp_dir.path().join("subdir"); - fs::create_dir(&subdir).unwrap(); - - let main_file = temp_dir.path().join("rndc.conf"); - let include_file = subdir.join("rndc.key"); - - // Write included file - let mut file = fs::File::create(&include_file).unwrap(); - writeln!( - file, - r#"key "test-key" {{ - algorithm hmac-sha256; - secret "test"; -}};"# - ) - .unwrap(); - - // Write main file with relative include - let mut file = fs::File::create(&main_file).unwrap(); - writeln!(file, r#"include "subdir/rndc.key";"#).unwrap(); - - // Parse the file - let conf = parse_rndc_conf_file(&main_file).unwrap(); - assert_eq!(conf.keys.len(), 1); - assert!(conf.keys.contains_key("test-key")); - } - - #[test] - fn test_parse_file_options_merging() { - use std::fs; - use std::io::Write; - use tempfile::TempDir; - - // Create temporary directory - let temp_dir = TempDir::new().unwrap(); - let main_file = temp_dir.path().join("rndc.conf"); - let include_file = temp_dir.path().join("defaults.conf"); - - // Write included file with default options - let mut file = fs::File::create(&include_file).unwrap(); - writeln!( - file, - r#"options {{ - default-server 192.168.1.1; - default-port 8953; -}};"# - ) - .unwrap(); - - // Write main file that overrides default-server - let mut file = fs::File::create(&main_file).unwrap(); - writeln!(file, r#"include "{}";"#, include_file.display()).unwrap(); - writeln!( - file, - r#"options {{ - default-server localhost; -}};"# - ) - .unwrap(); - - // Parse the file - let conf = parse_rndc_conf_file(&main_file).unwrap(); - // Main file options take precedence - assert_eq!(conf.options.default_server, Some("localhost".to_string())); - // But included port is preserved - assert_eq!(conf.options.default_port, Some(8953)); - } -} +#[path = "rndc_conf_parser_tests.rs"] +mod tests; diff --git a/src/rndc_conf_parser_tests.rs b/src/rndc_conf_parser_tests.rs new file mode 100644 index 0000000..3d51adf --- /dev/null +++ b/src/rndc_conf_parser_tests.rs @@ -0,0 +1,444 @@ +use super::*; + +#[test] +fn test_line_comment() { + assert!(line_comment("// comment\n").is_ok()); + assert!(line_comment("// comment").is_ok()); +} + +#[test] +fn test_hash_comment() { + assert!(hash_comment("# comment\n").is_ok()); +} + +#[test] +fn test_block_comment() { + assert!(block_comment("/* comment */").is_ok()); + assert!(block_comment("/* multi\nline */").is_ok()); +} + +#[test] +fn test_quoted_string() { + assert_eq!(quoted_string(r#""hello""#).unwrap().1, "hello"); + assert_eq!(quoted_string(r#""hello world""#).unwrap().1, "hello world"); +} + +#[test] +fn test_quoted_string_with_escapes() { + assert_eq!(quoted_string(r#""hello\"world""#).unwrap().1, "hello\"world"); + assert_eq!(quoted_string(r#""line1\nline2""#).unwrap().1, "line1\nline2"); +} + +#[test] +fn test_identifier() { + assert_eq!(identifier("hmac-sha256").unwrap().1, "hmac-sha256"); + assert_eq!(identifier("rndc-key").unwrap().1, "rndc-key"); + assert_eq!(identifier("localhost").unwrap().1, "localhost"); +} + +#[test] +fn test_ipv4_addr() { + let result = ipv4_addr("192.168.1.1").unwrap().1; + assert_eq!(result, "192.168.1.1".parse::().unwrap()); +} + +#[test] +fn test_ipv6_addr() { + let result = ipv6_addr("2001:db8::1").unwrap().1; + assert_eq!(result, "2001:db8::1".parse::().unwrap()); +} + +#[test] +fn test_port_number() { + assert_eq!(port_number("953").unwrap().1, 953); + assert_eq!(port_number("8080").unwrap().1, 8080); +} + +#[test] +fn test_parse_key_block() { + let input = r#"key "rndc-key" { + algorithm hmac-sha256; + secret "dGVzdC1zZWNyZXQ="; + };"#; + + let (_, (name, key)) = parse_key_block(input).unwrap(); + assert_eq!(name, "rndc-key"); + assert_eq!(key.algorithm, "hmac-sha256"); + assert_eq!(key.secret, "dGVzdC1zZWNyZXQ="); +} + +#[test] +fn test_parse_server_block() { + let input = r#"server localhost { + key "rndc-key"; + port 953; + };"#; + + let (_, (addr, server)) = parse_server_block(input).unwrap(); + assert_eq!(addr, "localhost"); + assert_eq!(server.key, Some("rndc-key".to_string())); + assert_eq!(server.port, Some(953)); +} + +#[test] +fn test_parse_options_block() { + let input = r#"options { + default-server localhost; + default-key "rndc-key"; + default-port 953; + };"#; + + let (_, options) = parse_options_block(input).unwrap(); + assert_eq!(options.default_server, Some("localhost".to_string())); + assert_eq!(options.default_key, Some("rndc-key".to_string())); + assert_eq!(options.default_port, Some(953)); +} + +#[test] +fn test_parse_include_stmt() { + let input = r#"include "/etc/bind/rndc.key";"#; + let (_, path) = parse_include_stmt(input).unwrap(); + assert_eq!(path, PathBuf::from("/etc/bind/rndc.key")); +} + +#[test] +fn test_parse_complete_conf() { + let input = r#" + # Example rndc.conf + include "/etc/bind/rndc.key"; + + key "rndc-key" { + algorithm hmac-sha256; + secret "dGVzdC1zZWNyZXQ="; + }; + + server localhost { + key "rndc-key"; + port 953; + }; + + options { + default-server localhost; + default-key "rndc-key"; + }; + "#; + + let conf = parse_rndc_conf_str(input).unwrap(); + assert_eq!(conf.keys.len(), 1); + assert_eq!(conf.servers.len(), 1); + assert_eq!(conf.includes.len(), 1); + assert_eq!(conf.options.default_server, Some("localhost".to_string())); +} + +#[test] +fn test_parse_with_comments() { + let input = r#" + // Line comment + # Hash comment + /* Block comment */ + key "test-key" { + algorithm hmac-sha256; // inline comment + secret "secret"; # another comment + }; + "#; + + let conf = parse_rndc_conf_str(input).unwrap(); + assert_eq!(conf.keys.len(), 1); +} + +#[test] +fn test_roundtrip() { + let input = r#" + key "rndc-key" { + algorithm hmac-sha256; + secret "dGVzdC1zZWNyZXQ="; + }; + + options { + default-server localhost; + default-key "rndc-key"; + }; + "#; + + let conf = parse_rndc_conf_str(input).unwrap(); + let serialized = conf.to_conf_file(); + let conf2 = parse_rndc_conf_str(&serialized).unwrap(); + + assert_eq!(conf.keys.len(), conf2.keys.len()); + assert_eq!(conf.options.default_server, conf2.options.default_server); +} + +// Error handling tests +#[test] +fn test_parse_empty_input() { + let input = ""; + let result = parse_rndc_conf_str(input); + assert!(result.is_ok()); + let conf = result.unwrap(); + assert_eq!(conf.keys.len(), 0); +} + +#[test] +fn test_parse_incomplete_key_block() { + // Test with no secret field - should still parse with default + let input = r#"key "test-key" { };"#; + let result = parse_rndc_conf_str(input); + assert!(result.is_ok()); + let conf = result.unwrap(); + assert!(conf.keys.contains_key("test-key")); +} + +#[test] +fn test_parse_invalid_ip_address() { + let input = "999.999.999.999"; + let result = ip_addr(input); + assert!(result.is_err()); +} + +#[test] +fn test_parse_server_with_ipv6() { + let input = r#"server 2001:db8::1 { + key "rndc-key"; + port 953; + };"#; + + let (_, (addr, server)) = parse_server_block(input).unwrap(); + assert!(addr.contains("2001:db8::1")); + assert_eq!(server.key, Some("rndc-key".to_string())); + assert_eq!(server.port, Some(953)); +} + +#[test] +fn test_parse_empty_options_block() { + let input = r#"options { };"#; + let (_, options) = parse_options_block(input).unwrap(); + assert!(options.is_empty()); +} + +#[test] +fn test_parse_key_block_without_algorithm() { + let input = r#"key "test-key" { + secret "dGVzdA=="; + };"#; + + let (_, (name, key)) = parse_key_block(input).unwrap(); + assert_eq!(name, "test-key"); + assert_eq!(key.algorithm, "hmac-sha256"); // Should default + assert_eq!(key.secret, "dGVzdA=="); +} + +#[test] +fn test_parse_server_block_full() { + // Test server block with key and port + let input = r#"server 192.168.1.1 { + key "test-key"; + port 8953; + };"#; + + let (_, (addr, server)) = parse_server_block(input).unwrap(); + assert!(addr.contains("192.168.1.1")); + assert_eq!(server.key, Some("test-key".to_string())); + assert_eq!(server.port, Some(8953)); +} + +#[test] +fn test_parse_multiple_keys() { + let input = r#" + key "key1" { + algorithm hmac-sha256; + secret "secret1"; + }; + + key "key2" { + algorithm hmac-md5; + secret "secret2"; + }; + "#; + + let conf = parse_rndc_conf_str(input).unwrap(); + assert_eq!(conf.keys.len(), 2); + assert!(conf.keys.contains_key("key1")); + assert!(conf.keys.contains_key("key2")); +} + +#[test] +fn test_parse_multiple_servers() { + let input = r#" + server 127.0.0.1 { + key "key1"; + }; + + server localhost { + key "key2"; + port 8953; + }; + "#; + + let conf = parse_rndc_conf_str(input).unwrap(); + assert_eq!(conf.servers.len(), 2); +} + +#[test] +fn test_error_display() { + let error = RndcConfParseError::MissingField("algorithm".to_string()); + assert_eq!(error.to_string(), "Missing required field: algorithm"); + + let error = RndcConfParseError::CircularInclude("/path/to/file".to_string()); + assert_eq!(error.to_string(), "Circular include detected: /path/to/file"); +} + +// File-based parsing tests +#[test] +fn test_parse_file_not_found() { + let result = parse_rndc_conf_file(Path::new("/nonexistent/path/rndc.conf")); + assert!(result.is_err()); + match result { + Err(RndcConfParseError::FileNotFound(_)) => {} + _ => panic!("Expected FileNotFound error"), + } +} + +#[test] +fn test_parse_file_with_includes() { + use std::fs; + use std::io::Write; + use tempfile::TempDir; + + // Create temporary directory + let temp_dir = TempDir::new().unwrap(); + let main_file = temp_dir.path().join("rndc.conf"); + let include_file = temp_dir.path().join("rndc.key"); + + // Write included file + let mut file = fs::File::create(&include_file).unwrap(); + writeln!( + file, + r#"key "rndc-key" {{ + algorithm hmac-sha256; + secret "dGVzdC1zZWNyZXQ="; +}};"# + ) + .unwrap(); + + // Write main file with include directive + let mut file = fs::File::create(&main_file).unwrap(); + writeln!(file, r#"include "{}";"#, include_file.display()).unwrap(); + writeln!( + file, + r#"options {{ + default-server localhost; + default-key "rndc-key"; +}};"# + ) + .unwrap(); + + // Parse the file + let conf = parse_rndc_conf_file(&main_file).unwrap(); + assert_eq!(conf.keys.len(), 1); + assert!(conf.keys.contains_key("rndc-key")); + assert_eq!(conf.options.default_server, Some("localhost".to_string())); + assert_eq!(conf.includes.len(), 1); +} + +#[test] +fn test_parse_file_circular_include() { + use std::fs; + use std::io::Write; + use tempfile::TempDir; + + // Create temporary directory + let temp_dir = TempDir::new().unwrap(); + let file1 = temp_dir.path().join("file1.conf"); + let file2 = temp_dir.path().join("file2.conf"); + + // Create circular includes: file1 -> file2 -> file1 + let mut f = fs::File::create(&file1).unwrap(); + writeln!(f, r#"include "{}";"#, file2.display()).unwrap(); + + let mut f = fs::File::create(&file2).unwrap(); + writeln!(f, r#"include "{}";"#, file1.display()).unwrap(); + + // Try to parse + let result = parse_rndc_conf_file(&file1); + assert!(result.is_err()); + match result { + Err(RndcConfParseError::CircularInclude(_)) => {} + _ => panic!("Expected CircularInclude error"), + } +} + +#[test] +fn test_parse_file_relative_include() { + use std::fs; + use std::io::Write; + use tempfile::TempDir; + + // Create temporary directory + let temp_dir = TempDir::new().unwrap(); + let subdir = temp_dir.path().join("subdir"); + fs::create_dir(&subdir).unwrap(); + + let main_file = temp_dir.path().join("rndc.conf"); + let include_file = subdir.join("rndc.key"); + + // Write included file + let mut file = fs::File::create(&include_file).unwrap(); + writeln!( + file, + r#"key "test-key" {{ + algorithm hmac-sha256; + secret "test"; +}};"# + ) + .unwrap(); + + // Write main file with relative include + let mut file = fs::File::create(&main_file).unwrap(); + writeln!(file, r#"include "subdir/rndc.key";"#).unwrap(); + + // Parse the file + let conf = parse_rndc_conf_file(&main_file).unwrap(); + assert_eq!(conf.keys.len(), 1); + assert!(conf.keys.contains_key("test-key")); +} + +#[test] +fn test_parse_file_options_merging() { + use std::fs; + use std::io::Write; + use tempfile::TempDir; + + // Create temporary directory + let temp_dir = TempDir::new().unwrap(); + let main_file = temp_dir.path().join("rndc.conf"); + let include_file = temp_dir.path().join("defaults.conf"); + + // Write included file with default options + let mut file = fs::File::create(&include_file).unwrap(); + writeln!( + file, + r#"options {{ + default-server 192.168.1.1; + default-port 8953; +}};"# + ) + .unwrap(); + + // Write main file that overrides default-server + let mut file = fs::File::create(&main_file).unwrap(); + writeln!(file, r#"include "{}";"#, include_file.display()).unwrap(); + writeln!( + file, + r#"options {{ + default-server localhost; +}};"# + ) + .unwrap(); + + // Parse the file + let conf = parse_rndc_conf_file(&main_file).unwrap(); + // Main file options take precedence + assert_eq!(conf.options.default_server, Some("localhost".to_string())); + // But included port is preserved + assert_eq!(conf.options.default_port, Some(8953)); +} diff --git a/src/rndc_conf_types.rs b/src/rndc_conf_types.rs index a45aa8d..6e25249 100644 --- a/src/rndc_conf_types.rs +++ b/src/rndc_conf_types.rs @@ -271,149 +271,5 @@ impl OptionsBlock { } #[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_key_block_serialization() { - let key = KeyBlock::new( - "rndc-key".to_string(), - "hmac-sha256".to_string(), - "dGVzdC1zZWNyZXQ=".to_string(), - ); - - let serialized = key.to_conf_block(); - assert!(serialized.contains("algorithm hmac-sha256;")); - assert!(serialized.contains("secret \"dGVzdC1zZWNyZXQ=\";")); - } - - #[test] - fn test_server_block_serialization() { - let mut server = ServerBlock::new(ServerAddress::Hostname("localhost".to_string())); - server.key = Some("rndc-key".to_string()); - server.port = Some(953); - - let serialized = server.to_conf_block(); - assert!(serialized.contains("key \"rndc-key\";")); - assert!(serialized.contains("port 953;")); - } - - #[test] - fn test_server_block_empty() { - let server = ServerBlock::new(ServerAddress::Hostname("localhost".to_string())); - let serialized = server.to_conf_block(); - assert_eq!(serialized, "{ };"); - } - - #[test] - fn test_options_block_serialization() { - let mut options = OptionsBlock::new(); - options.default_server = Some("localhost".to_string()); - options.default_key = Some("rndc-key".to_string()); - options.default_port = Some(953); - - let serialized = options.to_conf_block(); - assert!(serialized.contains("default-server localhost;")); - assert!(serialized.contains("default-key \"rndc-key\";")); - assert!(serialized.contains("default-port 953;")); - } - - #[test] - fn test_options_block_empty() { - let options = OptionsBlock::new(); - assert!(options.is_empty()); - assert_eq!(options.to_conf_block(), "{ };"); - } - - #[test] - fn test_rndc_conf_file_serialization() { - let mut conf = RndcConfFile::new(); - - conf.keys.insert( - "rndc-key".to_string(), - KeyBlock::new( - "rndc-key".to_string(), - "hmac-sha256".to_string(), - "dGVzdC1zZWNyZXQ=".to_string(), - ), - ); - - conf.options.default_key = Some("rndc-key".to_string()); - conf.options.default_server = Some("localhost".to_string()); - - let serialized = conf.to_conf_file(); - assert!(serialized.contains("key \"rndc-key\"")); - assert!(serialized.contains("algorithm hmac-sha256;")); - assert!(serialized.contains("default-key \"rndc-key\";")); - assert!(serialized.contains("default-server localhost;")); - } - - #[test] - fn test_get_default_key() { - let mut conf = RndcConfFile::new(); - - conf.keys.insert( - "rndc-key".to_string(), - KeyBlock::new( - "rndc-key".to_string(), - "hmac-sha256".to_string(), - "dGVzdC1zZWNyZXQ=".to_string(), - ), - ); - - conf.options.default_key = Some("rndc-key".to_string()); - - let key = conf.get_default_key().unwrap(); - assert_eq!(key.name, "rndc-key"); - assert_eq!(key.algorithm, "hmac-sha256"); - } - - #[test] - fn test_get_default_server() { - let mut conf = RndcConfFile::new(); - conf.options.default_server = Some("localhost".to_string()); - - assert_eq!(conf.get_default_server(), Some("localhost".to_string())); - } - - #[test] - fn test_server_address_parse() { - let ip_addr = ServerAddress::parse("127.0.0.1"); - assert!(matches!(ip_addr, ServerAddress::IpAddr(_))); - - let hostname = ServerAddress::parse("localhost"); - assert!(matches!(hostname, ServerAddress::Hostname(_))); - } - - #[test] - fn test_server_address_display() { - let hostname = ServerAddress::Hostname("localhost".to_string()); - assert_eq!(format!("{}", hostname), "localhost"); - - let ip = ServerAddress::IpAddr("127.0.0.1".parse().unwrap()); - assert_eq!(format!("{}", ip), "127.0.0.1"); - } - - #[test] - fn test_include_serialization() { - let mut conf = RndcConfFile::new(); - conf.includes.push(PathBuf::from("/etc/bind/rndc.key")); - - let serialized = conf.to_conf_file(); - assert!(serialized.contains("include \"/etc/bind/rndc.key\";")); - } - - #[test] - fn test_server_with_addresses() { - let mut server = ServerBlock::new(ServerAddress::Hostname("localhost".to_string())); - server.addresses = Some(vec![ - "192.168.1.1".parse().unwrap(), - "192.168.1.2".parse().unwrap(), - ]); - - let serialized = server.to_conf_block(); - assert!(serialized.contains("addresses {")); - assert!(serialized.contains("192.168.1.1;")); - assert!(serialized.contains("192.168.1.2;")); - } -} +#[path = "rndc_conf_types_tests.rs"] +mod tests; diff --git a/src/rndc_conf_types_tests.rs b/src/rndc_conf_types_tests.rs new file mode 100644 index 0000000..b4b0ceb --- /dev/null +++ b/src/rndc_conf_types_tests.rs @@ -0,0 +1,144 @@ +use super::*; + +#[test] +fn test_key_block_serialization() { + let key = KeyBlock::new( + "rndc-key".to_string(), + "hmac-sha256".to_string(), + "dGVzdC1zZWNyZXQ=".to_string(), + ); + + let serialized = key.to_conf_block(); + assert!(serialized.contains("algorithm hmac-sha256;")); + assert!(serialized.contains("secret \"dGVzdC1zZWNyZXQ=\";")); +} + +#[test] +fn test_server_block_serialization() { + let mut server = ServerBlock::new(ServerAddress::Hostname("localhost".to_string())); + server.key = Some("rndc-key".to_string()); + server.port = Some(953); + + let serialized = server.to_conf_block(); + assert!(serialized.contains("key \"rndc-key\";")); + assert!(serialized.contains("port 953;")); +} + +#[test] +fn test_server_block_empty() { + let server = ServerBlock::new(ServerAddress::Hostname("localhost".to_string())); + let serialized = server.to_conf_block(); + assert_eq!(serialized, "{ };"); +} + +#[test] +fn test_options_block_serialization() { + let mut options = OptionsBlock::new(); + options.default_server = Some("localhost".to_string()); + options.default_key = Some("rndc-key".to_string()); + options.default_port = Some(953); + + let serialized = options.to_conf_block(); + assert!(serialized.contains("default-server localhost;")); + assert!(serialized.contains("default-key \"rndc-key\";")); + assert!(serialized.contains("default-port 953;")); +} + +#[test] +fn test_options_block_empty() { + let options = OptionsBlock::new(); + assert!(options.is_empty()); + assert_eq!(options.to_conf_block(), "{ };"); +} + +#[test] +fn test_rndc_conf_file_serialization() { + let mut conf = RndcConfFile::new(); + + conf.keys.insert( + "rndc-key".to_string(), + KeyBlock::new( + "rndc-key".to_string(), + "hmac-sha256".to_string(), + "dGVzdC1zZWNyZXQ=".to_string(), + ), + ); + + conf.options.default_key = Some("rndc-key".to_string()); + conf.options.default_server = Some("localhost".to_string()); + + let serialized = conf.to_conf_file(); + assert!(serialized.contains("key \"rndc-key\"")); + assert!(serialized.contains("algorithm hmac-sha256;")); + assert!(serialized.contains("default-key \"rndc-key\";")); + assert!(serialized.contains("default-server localhost;")); +} + +#[test] +fn test_get_default_key() { + let mut conf = RndcConfFile::new(); + + conf.keys.insert( + "rndc-key".to_string(), + KeyBlock::new( + "rndc-key".to_string(), + "hmac-sha256".to_string(), + "dGVzdC1zZWNyZXQ=".to_string(), + ), + ); + + conf.options.default_key = Some("rndc-key".to_string()); + + let key = conf.get_default_key().unwrap(); + assert_eq!(key.name, "rndc-key"); + assert_eq!(key.algorithm, "hmac-sha256"); +} + +#[test] +fn test_get_default_server() { + let mut conf = RndcConfFile::new(); + conf.options.default_server = Some("localhost".to_string()); + + assert_eq!(conf.get_default_server(), Some("localhost".to_string())); +} + +#[test] +fn test_server_address_parse() { + let ip_addr = ServerAddress::parse("127.0.0.1"); + assert!(matches!(ip_addr, ServerAddress::IpAddr(_))); + + let hostname = ServerAddress::parse("localhost"); + assert!(matches!(hostname, ServerAddress::Hostname(_))); +} + +#[test] +fn test_server_address_display() { + let hostname = ServerAddress::Hostname("localhost".to_string()); + assert_eq!(format!("{}", hostname), "localhost"); + + let ip = ServerAddress::IpAddr("127.0.0.1".parse().unwrap()); + assert_eq!(format!("{}", ip), "127.0.0.1"); +} + +#[test] +fn test_include_serialization() { + let mut conf = RndcConfFile::new(); + conf.includes.push(PathBuf::from("/etc/bind/rndc.key")); + + let serialized = conf.to_conf_file(); + assert!(serialized.contains("include \"/etc/bind/rndc.key\";")); +} + +#[test] +fn test_server_with_addresses() { + let mut server = ServerBlock::new(ServerAddress::Hostname("localhost".to_string())); + server.addresses = Some(vec![ + "192.168.1.1".parse().unwrap(), + "192.168.1.2".parse().unwrap(), + ]); + + let serialized = server.to_conf_block(); + assert!(serialized.contains("addresses {")); + assert!(serialized.contains("192.168.1.1;")); + assert!(serialized.contains("192.168.1.2;")); +}