use crate::theme::Theme;
use directories::ProjectDirs;
use serde::Deserialize;
use std::fs;
use std::path::PathBuf;
pub mod output;
#[derive(Debug, Clone)]
pub struct OutputSource {
pub bytes: Option<bool>,
pub simple: Option<bool>,
pub csv: Option<bool>,
pub csv_delimiter: char,
pub csv_header: Option<bool>,
pub json: Option<bool>,
pub list: bool,
pub quiet: Option<bool>,
pub minimal: Option<bool>,
pub profile: Option<String>,
pub theme: String,
pub format: Option<Format>,
}
#[derive(Debug, Clone, Default)]
pub struct TestSource {
pub no_download: Option<bool>,
pub no_upload: Option<bool>,
pub single: Option<bool>,
}
#[derive(Debug, Clone)]
pub struct NetworkSource {
pub source: Option<String>,
pub timeout: u64,
pub ca_cert: Option<String>,
pub tls_version: Option<String>,
pub pin_certs: Option<bool>,
}
#[derive(Debug, Clone, Default)]
pub struct ServerSource {
pub server_ids: Vec<String>,
pub exclude_ids: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub struct ConfigSource {
pub output: OutputSource,
pub test: TestSource,
pub network: NetworkSource,
pub servers: ServerSource,
pub strict_config: Option<bool>,
}
impl Default for OutputSource {
fn default() -> Self {
Self {
bytes: None,
simple: None,
csv: None,
csv_delimiter: ',',
csv_header: None,
json: None,
list: false,
quiet: None,
minimal: None,
profile: None,
theme: "dark".to_string(),
format: None,
}
}
}
impl Default for NetworkSource {
fn default() -> Self {
Self {
source: None,
timeout: 10,
ca_cert: None,
tls_version: None,
pin_certs: None,
}
}
}
impl ConfigSource {
#[must_use]
#[allow(deprecated)] pub(crate) fn from_args(args: &crate::cli::Args) -> Self {
Self {
output: OutputSource {
bytes: args.bytes,
simple: args.simple,
csv: args.csv,
csv_delimiter: args.csv_delimiter,
csv_header: args.csv_header,
json: args.json,
list: args.list,
quiet: args.quiet,
minimal: args.minimal,
profile: args.profile.clone(),
theme: args.theme.clone(),
format: args.format.map(Format::from_cli_type),
},
test: TestSource {
no_download: args.no_download,
no_upload: args.no_upload,
single: args.single,
},
network: NetworkSource {
source: args.source.clone(),
timeout: args.timeout,
ca_cert: args.ca_cert.clone(),
tls_version: args.tls_version.clone(),
pin_certs: args.pin_certs,
},
servers: ServerSource {
server_ids: args.server.clone(),
exclude_ids: args.exclude.clone(),
},
strict_config: args.strict_config,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Format {
Json,
Jsonl,
Csv,
Minimal,
Simple,
Compact,
Detailed,
Dashboard,
}
impl Format {
#[must_use]
pub(crate) fn from_cli_type(cli: crate::cli::OutputFormatType) -> Self {
match cli {
crate::cli::OutputFormatType::Json => Self::Json,
crate::cli::OutputFormatType::Jsonl => Self::Jsonl,
crate::cli::OutputFormatType::Csv => Self::Csv,
crate::cli::OutputFormatType::Minimal => Self::Minimal,
crate::cli::OutputFormatType::Simple => Self::Simple,
crate::cli::OutputFormatType::Compact => Self::Compact,
crate::cli::OutputFormatType::Detailed => Self::Detailed,
crate::cli::OutputFormatType::Dashboard => Self::Dashboard,
}
}
#[must_use]
pub fn is_machine_readable(self) -> bool {
matches!(self, Self::Json | Self::Jsonl | Self::Csv)
}
#[must_use]
pub fn is_non_verbose(self) -> bool {
matches!(
self,
Self::Simple
| Self::Minimal
| Self::Compact
| Self::Json
| Self::Jsonl
| Self::Csv
| Self::Dashboard
)
}
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::Json => "JSON",
Self::Jsonl => "JSONL",
Self::Csv => "CSV",
Self::Minimal => "Minimal",
Self::Simple => "Simple",
Self::Compact => "Compact",
Self::Detailed => "Detailed",
Self::Dashboard => "Dashboard",
}
}
}
impl std::fmt::Display for Format {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.label())
}
}
#[derive(Debug, Clone)]
pub struct OutputConfig {
pub bytes: bool,
pub simple: bool,
pub csv: bool,
pub csv_delimiter: char,
pub csv_header: bool,
pub json: bool,
pub list: bool,
pub quiet: bool,
pub profile: Option<String>,
pub theme: Theme,
pub minimal: bool,
pub format: Option<Format>,
}
impl Default for OutputConfig {
fn default() -> Self {
Self {
bytes: false,
simple: false,
csv: false,
csv_delimiter: ',',
csv_header: false,
json: false,
list: false,
quiet: false,
profile: None,
theme: Theme::Dark,
minimal: false,
format: None,
}
}
}
impl OutputConfig {
#[must_use]
#[allow(deprecated)]
pub(crate) fn from_source(
source: &OutputSource,
file_config: &File,
merge_bool: impl Fn(Option<bool>, Option<bool>) -> bool,
) -> Self {
let theme = if source.theme == "dark" {
file_config
.theme
.as_ref()
.and_then(|t| Theme::from_name(t))
.unwrap_or_default()
} else {
Theme::from_name(&source.theme).unwrap_or_default()
};
Self {
bytes: merge_bool(source.bytes, file_config.bytes),
simple: merge_bool(source.simple, file_config.simple),
csv: merge_bool(source.csv, file_config.csv),
csv_delimiter: if source.csv_delimiter == ',' {
file_config.csv_delimiter.unwrap_or(',')
} else {
source.csv_delimiter
},
csv_header: merge_bool(source.csv_header, file_config.csv_header),
json: merge_bool(source.json, file_config.json),
list: source.list,
quiet: merge_bool(source.quiet, None),
profile: source.profile.clone().or(file_config.profile.clone()),
theme,
minimal: merge_bool(source.minimal, None),
format: source.format,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct TestSelection {
pub no_download: bool,
pub no_upload: bool,
pub single: bool,
}
impl TestSelection {
#[must_use]
pub(crate) fn from_source(
source: &TestSource,
file_config: &File,
merge_bool: impl Fn(Option<bool>, Option<bool>) -> bool,
) -> Self {
Self {
no_download: merge_bool(source.no_download, file_config.no_download),
no_upload: merge_bool(source.no_upload, file_config.no_upload),
single: merge_bool(source.single, file_config.single),
}
}
}
#[derive(Debug, Clone)]
pub struct NetworkConfig {
pub source: Option<String>,
pub timeout: u64,
pub ca_cert: Option<String>,
pub tls_version: Option<String>,
pub pin_certs: bool,
}
impl Default for NetworkConfig {
fn default() -> Self {
Self {
source: None,
timeout: 10,
ca_cert: None,
tls_version: None,
pin_certs: false,
}
}
}
impl NetworkConfig {
#[must_use]
pub(crate) fn from_source(
source: &NetworkSource,
file_config: &File,
merge_bool: impl Fn(Option<bool>, Option<bool>) -> bool,
merge_u64: impl Fn(u64, Option<u64>, u64) -> u64,
) -> Self {
Self {
source: source.source.clone(),
timeout: merge_u64(source.timeout, file_config.timeout, 10),
ca_cert: source.ca_cert.clone().or(file_config.ca_cert.clone()),
tls_version: source
.tls_version
.clone()
.or(file_config.tls_version.clone()),
pin_certs: merge_bool(source.pin_certs, file_config.pin_certs),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ServerSelection {
pub server_ids: Vec<String>,
pub exclude_ids: Vec<String>,
}
impl ServerSelection {
#[must_use]
pub(crate) fn from_source(source: &ServerSource) -> Self {
Self {
server_ids: source.server_ids.clone(),
exclude_ids: source.exclude_ids.clone(),
}
}
}
#[derive(Debug, Default, Clone, Deserialize)]
pub struct File {
pub no_download: Option<bool>,
pub no_upload: Option<bool>,
pub single: Option<bool>,
pub bytes: Option<bool>,
pub simple: Option<bool>,
pub csv: Option<bool>,
pub csv_delimiter: Option<char>,
pub csv_header: Option<bool>,
pub json: Option<bool>,
pub timeout: Option<u64>,
pub profile: Option<String>,
pub theme: Option<String>,
pub custom_user_agent: Option<String>,
pub strict: Option<bool>,
pub ca_cert: Option<String>,
pub tls_version: Option<String>,
pub pin_certs: Option<bool>,
}
#[derive(Debug, Clone, Default)]
pub struct Config {
pub output: OutputConfig,
pub test: TestSelection,
pub network: NetworkConfig,
pub servers: ServerSelection,
pub custom_user_agent: Option<String>,
pub strict: bool,
}
pub trait ConfigProvider: Send + Sync {
fn config(&self) -> &Config;
}
impl ConfigProvider for Config {
fn config(&self) -> &Config {
self
}
}
impl Config {
#[allow(deprecated)]
#[must_use]
pub fn from_args(args: &crate::cli::Args) -> Self {
let source = ConfigSource::from_args(args);
Self::from_source(&source)
}
#[allow(deprecated)]
#[must_use]
pub fn from_args_with_file(
source: &ConfigSource,
file_config: Option<File>,
) -> (Self, ValidationResult) {
let config = Self::from_source_with_file(source, file_config);
let mut validation = ValidationResult::ok();
if let Some(ref profile_name) = source.output.profile {
if crate::profiles::UserProfile::validate(profile_name).is_err() {
validation = validation.with_warning(format!(
"Unknown profile '{}'. Valid options: {}. Using 'power-user'.",
profile_name,
crate::profiles::UserProfile::VALID_NAMES.join(", ")
));
}
}
(config, validation)
}
#[must_use]
pub fn from_source(source: &ConfigSource) -> Self {
let file_config = load_config_file().unwrap_or_default();
Self::from_source_with_file(source, Some(file_config))
}
#[must_use]
pub(crate) fn from_source_with_file(source: &ConfigSource, file_config: Option<File>) -> Self {
let file = file_config.unwrap_or_default();
let strict = source.strict_config.unwrap_or(file.strict.unwrap_or(false));
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let merge_u64 = |cli: u64, file: Option<u64>, default: u64| {
if cli == default {
file.unwrap_or(default)
} else {
cli
}
};
let output = OutputConfig::from_source(&source.output, &file, merge_bool);
let test = TestSelection::from_source(&source.test, &file, merge_bool);
let network = NetworkConfig::from_source(&source.network, &file, merge_bool, merge_u64);
let servers = ServerSelection::from_source(&source.servers);
Self {
output,
test,
network,
servers,
custom_user_agent: file.custom_user_agent.clone(),
strict,
}
}
#[must_use]
pub fn validate_and_report(
&self,
source: &ConfigSource,
file_config: Option<File>,
) -> ValidationResult {
let file = file_config.unwrap_or_else(|| load_config_file().unwrap_or_default());
let mut validation = validate_config(&file);
if let Some(ref profile_name) = source.output.profile {
if crate::profiles::UserProfile::validate(profile_name).is_err() {
validation = validation.with_warning(format!(
"Unknown profile '{}'. Valid options: {}. Using 'power-user'.",
profile_name,
crate::profiles::UserProfile::VALID_NAMES.join(", ")
));
}
}
validation
}
#[must_use]
pub fn should_save_history(&self) -> bool {
if self.format().is_some_and(|f| f.is_machine_readable()) {
return false;
}
if self.json() || self.csv() {
return false;
}
true
}
#[must_use]
pub fn no_download(&self) -> bool {
self.test.no_download
}
#[must_use]
pub fn no_upload(&self) -> bool {
self.test.no_upload
}
#[must_use]
pub fn single(&self) -> bool {
self.test.single
}
#[must_use]
pub fn bytes(&self) -> bool {
self.output.bytes
}
#[must_use]
pub fn simple(&self) -> bool {
self.output.simple
}
#[must_use]
pub fn csv(&self) -> bool {
self.output.csv
}
#[must_use]
pub fn json(&self) -> bool {
self.output.json
}
#[must_use]
pub fn quiet(&self) -> bool {
self.output.quiet
}
#[must_use]
pub fn list(&self) -> bool {
self.output.list
}
#[must_use]
pub fn minimal(&self) -> bool {
self.output.minimal
}
#[must_use]
pub fn theme(&self) -> Theme {
self.output.theme
}
#[must_use]
pub fn csv_delimiter(&self) -> char {
self.output.csv_delimiter
}
#[must_use]
pub fn csv_header(&self) -> bool {
self.output.csv_header
}
#[must_use]
pub fn profile(&self) -> Option<&str> {
self.output.profile.as_deref()
}
#[must_use]
pub fn format(&self) -> Option<Format> {
self.output.format
}
#[must_use]
pub fn timeout(&self) -> u64 {
self.network.timeout
}
#[must_use]
pub fn source(&self) -> Option<&str> {
self.network.source.as_deref()
}
#[must_use]
pub fn ca_cert(&self) -> Option<&str> {
self.network.ca_cert.as_deref()
}
#[must_use]
pub(crate) fn ca_cert_path(&self) -> Option<PathBuf> {
self.network.ca_cert.as_ref().map(PathBuf::from)
}
#[must_use]
pub fn tls_version(&self) -> Option<&str> {
self.network.tls_version.as_deref()
}
#[must_use]
pub fn pin_certs(&self) -> bool {
self.network.pin_certs
}
#[must_use]
pub fn server_ids(&self) -> &[String] {
&self.servers.server_ids
}
#[must_use]
pub fn exclude_ids(&self) -> &[String] {
&self.servers.exclude_ids
}
#[must_use]
pub fn custom_user_agent(&self) -> Option<&str> {
self.custom_user_agent.as_deref()
}
#[must_use]
pub fn strict(&self) -> bool {
self.strict
}
}
#[derive(Debug, Clone)]
pub struct ValidationResult {
pub valid: bool,
pub errors: Vec<String>,
pub warnings: Vec<String>,
}
impl ValidationResult {
#[must_use]
pub fn ok() -> Self {
Self {
valid: true,
errors: Vec::new(),
warnings: Vec::new(),
}
}
#[must_use]
pub fn with_warning(mut self, warning: impl Into<String>) -> Self {
self.warnings.push(warning.into());
self
}
#[must_use]
pub fn error(msg: impl Into<String>) -> Self {
Self {
valid: false,
errors: vec![msg.into()],
warnings: Vec::new(),
}
}
#[must_use]
pub fn with_error(mut self, error: impl Into<String>) -> Self {
self.errors.push(error.into());
self.valid = false;
self
}
#[must_use]
pub fn merge(mut self, other: ValidationResult) -> Self {
if !other.valid {
self.valid = false;
}
self.errors.extend(other.errors);
self.warnings.extend(other.warnings);
self
}
}
fn validate_csv_delimiter_config(delimiter: char) -> Result<(), String> {
if !",;|\t".contains(delimiter) {
return Err(format!(
"Invalid CSV delimiter '{}'. Must be one of: comma, semicolon, pipe, or tab",
delimiter
));
}
Ok(())
}
pub fn validate_config(file_config: &File) -> ValidationResult {
let mut result = ValidationResult::ok();
if let Some(ref profile) = file_config.profile {
if let Err(e) = crate::profiles::UserProfile::validate(profile) {
result = result.with_error(e);
}
}
if let Some(ref theme) = file_config.theme {
if let Err(e) = crate::theme::Theme::validate(theme) {
result = result.with_error(e);
}
}
if let Some(delimiter) = file_config.csv_delimiter {
if let Err(e) = validate_csv_delimiter_config(delimiter) {
result = result.with_error(e);
}
}
if file_config.simple.unwrap_or(false) {
result = result.with_warning(
"'simple' option is deprecated. Use '--format simple' instead.".to_string(),
);
}
if file_config.csv.unwrap_or(false) {
result = result
.with_warning("'csv' option is deprecated. Use '--format csv' instead.".to_string());
}
if file_config.json.unwrap_or(false) {
result = result
.with_warning("'json' option is deprecated. Use '--format json' instead.".to_string());
}
result
}
#[must_use]
pub fn get_config_path_internal() -> Option<PathBuf> {
ProjectDirs::from("dev", "vibe", "netspeed-cli").map(|proj_dirs| {
let config_dir = proj_dirs.config_dir();
if let Err(e) = fs::create_dir_all(config_dir) {
eprintln!("Warning: Failed to create config directory: {e}");
}
config_dir.join("config.toml")
})
}
pub fn load_config_file() -> Option<File> {
let path = get_config_path_internal()?;
if !path.exists() {
return None;
}
let content = match fs::read_to_string(&path) {
Ok(c) => c,
Err(e) => {
eprintln!(
"Warning: Failed to read config file {}: {e}",
path.display()
);
return None;
}
};
let mut config: File = match toml::from_str(&content) {
Ok(c) => c,
Err(e) => {
eprintln!("Warning: Failed to parse config: {e}");
return None;
}
};
if let Some(timeout) = config.timeout {
if timeout == 0 || timeout > 300 {
eprintln!(
"Warning: Invalid config timeout ({timeout}s, must be 1-300). Using default."
);
config.timeout = None;
}
}
Some(config)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cli::Args;
use clap::Parser;
#[test]
fn test_config_source_from_args_list_flag() {
let args = Args::parse_from(["netspeed-cli", "--list"]);
let source = ConfigSource::from_args(&args);
assert!(source.output.list);
}
#[test]
fn test_config_source_from_args_quiet_flag() {
let args = Args::parse_from(["netspeed-cli", "--quiet"]);
let source = ConfigSource::from_args(&args);
assert_eq!(source.output.quiet, Some(true));
}
#[test]
fn test_config_source_from_args_minimal_flag() {
let args = Args::parse_from(["netspeed-cli", "--minimal"]);
let source = ConfigSource::from_args(&args);
assert_eq!(source.output.minimal, Some(true));
}
#[test]
fn test_config_source_strict_config() {
let args = Args::parse_from(["netspeed-cli", "--strict-config"]);
let source = ConfigSource::from_args(&args);
assert_eq!(source.strict_config, Some(true));
}
#[test]
fn test_config_source_from_args_bytes_flag() {
let args = Args::parse_from(["netspeed-cli", "--bytes"]);
let source = ConfigSource::from_args(&args);
assert_eq!(source.output.bytes, Some(true));
}
#[test]
fn test_config_source_from_args_json_flag() {
let args = Args::parse_from(["netspeed-cli", "--json"]);
let source = ConfigSource::from_args(&args);
assert_eq!(source.output.json, Some(true));
}
#[test]
fn test_config_source_from_args_csv_flag() {
let args = Args::parse_from(["netspeed-cli", "--csv"]);
let source = ConfigSource::from_args(&args);
assert_eq!(source.output.csv, Some(true));
}
#[test]
fn test_config_source_from_args_simple_flag() {
let args = Args::parse_from(["netspeed-cli", "--simple"]);
let source = ConfigSource::from_args(&args);
assert_eq!(source.output.simple, Some(true));
}
#[test]
fn test_config_source_from_args_csv_header_flag() {
let args = Args::parse_from(["netspeed-cli", "--csv-header"]);
let source = ConfigSource::from_args(&args);
assert_eq!(source.output.csv_header, Some(true));
}
#[test]
fn test_config_source_from_args_csv_delimiter() {
let args = Args::parse_from(["netspeed-cli", "--csv-delimiter", ";"]);
let source = ConfigSource::from_args(&args);
assert_eq!(source.output.csv_delimiter, ';');
}
#[test]
fn test_config_from_source_with_none_file() {
let source = ConfigSource::default();
let config = Config::from_source_with_file(&source, None);
assert!(!config.output.bytes);
assert_eq!(config.network.timeout, 10);
}
#[test]
fn test_config_from_source_with_file_profile() {
let mut source = ConfigSource::default();
source.output.profile = Some("streamer".to_string());
let file_config = File::default();
let config = Config::from_source_with_file(&source, Some(file_config));
assert_eq!(config.profile(), Some("streamer"));
}
#[test]
fn test_config_from_source_strict_mode() {
let source = ConfigSource {
strict_config: Some(true),
..Default::default()
};
let file_config = File::default();
let config = Config::from_source_with_file(&source, Some(file_config));
assert!(config.strict());
}
#[test]
fn test_config_from_args_strict_from_file() {
let toml_content = "strict = true";
let file_config: File = toml::from_str(toml_content).unwrap();
let source = ConfigSource::default();
let config = Config::from_source_with_file(&source, Some(file_config));
assert!(config.strict());
}
#[test]
fn test_config_from_args_timeout_from_file() {
let toml_content = "timeout = 60";
let file_config: File = toml::from_str(toml_content).unwrap();
let source = ConfigSource::default(); let config = Config::from_source_with_file(&source, Some(file_config));
assert_eq!(config.timeout(), 60); }
#[test]
fn test_config_from_args_timeout_cli_overrides_file() {
let toml_content = "timeout = 60";
let file_config: File = toml::from_str(toml_content).unwrap();
let args = Args::parse_from(["netspeed-cli", "--timeout", "120"]);
let source = ConfigSource::from_args(&args);
let config = Config::from_source_with_file(&source, Some(file_config));
assert_eq!(config.timeout(), 120); }
#[test]
fn test_config_from_args_custom_user_agent() {
let toml_content = "custom_user_agent = \"MyAgent/1.0\"";
let file_config: File = toml::from_str(toml_content).unwrap();
let source = ConfigSource::default();
let config = Config::from_source_with_file(&source, Some(file_config));
assert_eq!(config.custom_user_agent(), Some("MyAgent/1.0"));
}
#[test]
fn test_output_config_from_source_theme_dark_cli_default() {
let source = OutputSource {
theme: "dark".to_string(),
..Default::default()
};
let file = File {
theme: Some("light".to_string()),
..Default::default()
};
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let output = OutputConfig::from_source(&source, &file, merge_bool);
assert_eq!(output.theme, Theme::Light);
}
#[test]
fn test_output_config_from_source_theme_cli_override() {
let source = OutputSource {
theme: "light".to_string(),
..Default::default()
};
let file = File {
theme: Some("high-contrast".to_string()),
..Default::default()
};
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let output = OutputConfig::from_source(&source, &file, merge_bool);
assert_eq!(output.theme, Theme::Light);
}
#[test]
fn test_output_config_from_source_theme_invalid_file_theme() {
let source = OutputSource {
theme: "dark".to_string(),
..Default::default()
};
let file = File {
theme: Some("invalid_theme".to_string()),
..Default::default()
};
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let output = OutputConfig::from_source(&source, &file, merge_bool);
assert_eq!(output.theme, Theme::Dark);
}
#[test]
fn test_output_config_from_source_csv_delimiter_default() {
let source = OutputSource::default(); let file = File {
csv_delimiter: Some(';'),
..Default::default()
};
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let output = OutputConfig::from_source(&source, &file, merge_bool);
assert_eq!(output.csv_delimiter, ';');
}
#[test]
fn test_output_config_from_source_csv_delimiter_cli_override() {
let source = OutputSource {
csv_delimiter: '|',
..Default::default()
};
let file = File {
csv_delimiter: Some(';'),
..Default::default()
};
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let output = OutputConfig::from_source(&source, &file, merge_bool);
assert_eq!(output.csv_delimiter, '|');
}
#[test]
fn test_output_config_from_source_profile_merge() {
let source = OutputSource {
profile: Some("cli-profile".to_string()),
..Default::default()
};
let file = File {
profile: Some("file-profile".to_string()),
..Default::default()
};
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let output = OutputConfig::from_source(&source, &file, merge_bool);
assert_eq!(output.profile, Some("cli-profile".to_string()));
}
#[test]
fn test_output_config_from_source_profile_from_file() {
let source = OutputSource::default();
let file = File {
profile: Some("file-profile".to_string()),
..Default::default()
};
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let output = OutputConfig::from_source(&source, &file, merge_bool);
assert_eq!(output.profile, Some("file-profile".to_string()));
}
#[test]
fn test_output_config_from_source_format() {
let source = OutputSource {
format: Some(Format::Dashboard),
..Default::default()
};
let file = File::default();
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let output = OutputConfig::from_source(&source, &file, merge_bool);
assert_eq!(output.format, Some(Format::Dashboard));
}
#[test]
fn test_test_selection_from_source_all_fields() {
let source = TestSource {
no_download: Some(true),
no_upload: Some(false),
single: Some(true),
};
let file = File::default();
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let selection = TestSelection::from_source(&source, &file, merge_bool);
assert!(selection.no_download);
assert!(!selection.no_upload);
assert!(selection.single);
}
#[test]
fn test_test_selection_from_source_file_fallback() {
let source = TestSource::default();
let file = File {
no_download: Some(true),
no_upload: Some(true),
single: Some(false),
..Default::default()
};
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let selection = TestSelection::from_source(&source, &file, merge_bool);
assert!(selection.no_download);
assert!(selection.no_upload);
assert!(!selection.single);
}
#[test]
fn test_test_selection_from_source_both_none() {
let source = TestSource::default();
let file = File::default();
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let selection = TestSelection::from_source(&source, &file, merge_bool);
assert!(!selection.no_download);
assert!(!selection.no_upload);
assert!(!selection.single);
}
#[test]
fn test_network_config_from_source_all_fields() {
let source = NetworkSource {
source: Some("192.168.1.1".to_string()),
timeout: 60,
ca_cert: Some("/path/to/cert".to_string()),
tls_version: Some("1.3".to_string()),
pin_certs: Some(true),
};
let file = File::default();
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let merge_u64 = |cli: u64, file: Option<u64>, default: u64| {
if cli == default {
file.unwrap_or(default)
} else {
cli
}
};
let network = NetworkConfig::from_source(&source, &file, merge_bool, merge_u64);
assert_eq!(network.source, Some("192.168.1.1".to_string()));
assert_eq!(network.timeout, 60);
assert_eq!(network.ca_cert, Some("/path/to/cert".to_string()));
assert_eq!(network.tls_version, Some("1.3".to_string()));
assert!(network.pin_certs);
}
#[test]
fn test_network_config_from_source_timeout_file_fallback() {
let source = NetworkSource::default(); let file = File {
timeout: Some(30),
..Default::default()
};
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let merge_u64 = |cli: u64, file: Option<u64>, default: u64| {
if cli == default {
file.unwrap_or(default)
} else {
cli
}
};
let network = NetworkConfig::from_source(&source, &file, merge_bool, merge_u64);
assert_eq!(network.timeout, 30);
}
#[test]
fn test_network_config_from_source_ca_cert_file_fallback() {
let source = NetworkSource::default();
let file = File {
ca_cert: Some("/file/cert.pem".to_string()),
..Default::default()
};
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let merge_u64 = |cli: u64, file: Option<u64>, default: u64| {
if cli == default {
file.unwrap_or(default)
} else {
cli
}
};
let network = NetworkConfig::from_source(&source, &file, merge_bool, merge_u64);
assert_eq!(network.ca_cert, Some("/file/cert.pem".to_string()));
}
#[test]
fn test_network_config_from_source_pin_certs_file_fallback() {
let source = NetworkSource::default();
let file = File {
pin_certs: Some(true),
..Default::default()
};
let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
let merge_u64 = |cli: u64, file: Option<u64>, default: u64| {
if cli == default {
file.unwrap_or(default)
} else {
cli
}
};
let network = NetworkConfig::from_source(&source, &file, merge_bool, merge_u64);
assert!(network.pin_certs);
}
#[test]
fn test_validation_result_multiple_warnings() {
let result = ValidationResult::ok()
.with_warning("warning 1")
.with_warning("warning 2")
.with_warning("warning 3");
assert!(result.valid);
assert_eq!(result.warnings.len(), 3);
assert!(result.errors.is_empty());
}
#[test]
fn test_validation_result_multiple_errors() {
let result = ValidationResult::error("error 1").with_error("error 2");
assert!(!result.valid);
assert_eq!(result.errors.len(), 2);
}
#[test]
fn test_validation_result_with_warning_then_error() {
let result = ValidationResult::ok()
.with_warning("just a warning")
.with_error("actual error");
assert!(!result.valid);
assert!(!result.warnings.is_empty());
assert!(!result.errors.is_empty());
}
#[test]
fn test_validation_result_merge_valid_results() {
let a = ValidationResult::ok().with_warning("warn-a");
let b = ValidationResult::ok().with_warning("warn-b");
let merged = a.merge(b);
assert!(merged.valid);
assert_eq!(merged.warnings.len(), 2);
assert!(merged.errors.is_empty());
}
#[test]
fn test_validation_result_merge_with_invalid() {
let a = ValidationResult::ok();
let b = ValidationResult::error("bad");
let merged = a.merge(b);
assert!(!merged.valid);
assert!(merged.errors.contains(&"bad".to_string()));
}
#[test]
fn test_validation_result_merge_accumulates_all() {
let a = ValidationResult::error("err-a").with_warning("warn-a");
let b = ValidationResult::error("err-b").with_warning("warn-b");
let merged = a.merge(b);
assert!(!merged.valid);
assert_eq!(merged.errors.len(), 2);
assert_eq!(merged.warnings.len(), 2);
}
#[test]
fn test_validation_result_merge_empty() {
let a = ValidationResult::ok();
let merged = a.merge(ValidationResult::ok());
assert!(merged.valid);
assert!(merged.errors.is_empty());
assert!(merged.warnings.is_empty());
}
#[test]
fn test_validation_result_valid_then_invalid() {
let a = ValidationResult::error("original error");
let b = ValidationResult::ok();
let merged = a.merge(b);
assert!(!merged.valid);
assert_eq!(merged.errors.len(), 1);
}
#[test]
fn test_validate_config_all_valid() {
let file_config = File {
profile: Some("power-user".to_string()),
theme: Some("dark".to_string()),
csv_delimiter: Some(','),
..Default::default()
};
let result = validate_config(&file_config);
assert!(result.valid);
assert!(result.errors.is_empty());
assert!(result.warnings.is_empty());
}
#[test]
fn test_validate_config_multiple_warnings() {
let file_config = File {
simple: Some(true),
csv: Some(true),
json: Some(true),
..Default::default()
};
let result = validate_config(&file_config);
assert!(result.valid); assert!(result.warnings.len() >= 3); }
#[test]
fn test_validate_config_invalid_timeout_zero() {
let file_config = File {
timeout: Some(0),
..Default::default()
};
let result = validate_config(&file_config);
assert!(result.valid); }
#[test]
fn test_validate_config_invalid_timeout_too_large() {
let file_config = File {
timeout: Some(500),
..Default::default()
};
let result = validate_config(&file_config);
assert!(result.valid); }
#[test]
fn test_load_config_file_invalid_toml() {
let temp_dir = tempfile::TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.toml");
std::fs::write(&config_path, "invalid toml { =").unwrap();
let content = std::fs::read_to_string(&config_path).unwrap();
let result: Result<File, _> = toml::from_str(&content);
assert!(result.is_err());
}
#[test]
fn test_load_config_file_timeout_zero() {
let temp_dir = tempfile::TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.toml");
std::fs::write(&config_path, "timeout = 0").unwrap();
let content = std::fs::read_to_string(&config_path).unwrap();
let mut config: File = toml::from_str(&content).unwrap();
assert_eq!(config.timeout, Some(0));
if let Some(timeout) = config.timeout {
if timeout == 0 || timeout > 300 {
config.timeout = None;
}
}
assert_eq!(config.timeout, None);
}
#[test]
fn test_load_config_file_timeout_too_large() {
let temp_dir = tempfile::TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.toml");
std::fs::write(&config_path, "timeout = 500").unwrap();
let content = std::fs::read_to_string(&config_path).unwrap();
let mut config: File = toml::from_str(&content).unwrap();
assert_eq!(config.timeout, Some(500));
if let Some(timeout) = config.timeout {
if timeout == 0 || timeout > 300 {
config.timeout = None;
}
}
assert_eq!(config.timeout, None);
}
#[test]
fn test_load_config_file_valid_timeout() {
let temp_dir = tempfile::TempDir::new().unwrap();
let config_path = temp_dir.path().join("config.toml");
std::fs::write(&config_path, "timeout = 60").unwrap();
let content = std::fs::read_to_string(&config_path).unwrap();
let mut config: File = toml::from_str(&content).unwrap();
assert_eq!(config.timeout, Some(60));
if let Some(timeout) = config.timeout {
if timeout == 0 || timeout > 300 {
config.timeout = None;
}
}
assert_eq!(config.timeout, Some(60));
}
#[test]
fn test_get_config_path_internal_returns_path() {
let path = get_config_path_internal();
if let Some(p) = path {
assert!(p.ends_with("config.toml"));
}
}
#[test]
fn test_config_from_args_with_file_valid_profile() {
let args = Args::parse_from(["netspeed-cli", "--profile", "gamer"]);
let source = ConfigSource::from_args(&args);
let (config, validation) = Config::from_args_with_file(&source, None);
assert!(validation.valid); assert!(config.profile().is_some());
}
#[test]
fn test_config_from_args_with_file_invalid_profile_warning() {
let args = Args::parse_from(["netspeed-cli", "--profile", "bad-profile"]);
let source = ConfigSource::from_args(&args);
let (_config, validation) = Config::from_args_with_file(&source, None);
assert!(validation.valid);
assert!(!validation.warnings.is_empty());
assert!(validation.warnings[0].contains("bad-profile"));
}
#[test]
fn test_config_from_args_with_file_preserves_config() {
let args = Args::parse_from(["netspeed-cli", "--timeout", "45"]);
let source = ConfigSource::from_args(&args);
let (config, validation) = Config::from_args_with_file(&source, None);
assert!(validation.valid);
assert_eq!(config.timeout(), 45);
}
#[test]
fn test_config_from_args_with_file_all_formats() {
for format_str in &[
"json",
"jsonl",
"csv",
"minimal",
"simple",
"compact",
"detailed",
"dashboard",
] {
let args = Args::parse_from(["netspeed-cli", "--format", format_str]);
let source = ConfigSource::from_args(&args);
let (config, _) = Config::from_args_with_file(&source, None);
assert!(
config.format().is_some(),
"Format {} should be set",
format_str
);
}
}
#[test]
fn test_format_debug() {
let fmt = Format::Json;
let debug_str = format!("{fmt:?}");
assert!(debug_str.contains("Json"));
}
#[test]
fn test_format_clone() {
let fmt = Format::Detailed;
let cloned = fmt;
assert_eq!(fmt, cloned);
}
#[test]
fn test_format_copy() {
let fmt = Format::Dashboard;
let copied = fmt; assert_eq!(fmt, copied);
}
#[test]
fn test_config_debug() {
let config = Config::default();
let debug_str = format!("{config:?}");
assert!(debug_str.contains("Config"));
}
#[test]
fn test_config_source_debug() {
let source = ConfigSource::default();
let debug_str = format!("{source:?}");
assert!(debug_str.contains("ConfigSource"));
}
#[test]
fn test_validation_result_debug() {
let result = ValidationResult::ok();
let debug_str = format!("{result:?}");
assert!(debug_str.contains("ValidationResult"));
}
#[test]
fn test_file_config_debug() {
let file = File::default();
let debug_str = format!("{file:?}");
assert!(debug_str.contains("File"));
}
#[test]
fn test_file_config_clone() {
let file = File {
timeout: Some(45),
profile: Some("test".to_string()),
..Default::default()
};
let cloned = file.clone();
assert_eq!(file.timeout, cloned.timeout);
assert_eq!(file.profile, cloned.profile);
}
#[test]
fn test_config_source_clone() {
let mut source = ConfigSource::default();
source.output.profile = Some("clone-test".to_string());
let cloned = source.clone();
assert_eq!(source.output.profile, cloned.output.profile);
}
#[test]
fn test_file_config_all_fields() {
let toml_content = r#"
no_download = true
no_upload = false
single = true
bytes = true
simple = false
csv = false
csv_delimiter = '|'
csv_header = true
json = false
timeout = 120
profile = "gamer"
theme = "light"
custom_user_agent = "TestAgent/1.0"
strict = true
ca_cert = "/path/to/cert.pem"
tls_version = "1.3"
pin_certs = true
"#;
let config: File = toml::from_str(toml_content).unwrap();
assert_eq!(config.no_download, Some(true));
assert_eq!(config.timeout, Some(120));
assert_eq!(config.profile, Some("gamer".to_string()));
assert_eq!(config.theme, Some("light".to_string()));
assert_eq!(config.custom_user_agent, Some("TestAgent/1.0".to_string()));
assert_eq!(config.strict, Some(true));
assert_eq!(config.ca_cert, Some("/path/to/cert.pem".to_string()));
assert_eq!(config.tls_version, Some("1.3".to_string()));
assert_eq!(config.pin_certs, Some(true));
}
#[test]
fn test_file_config_empty_toml() {
let toml_content = "";
let config: File = toml::from_str(toml_content).unwrap();
assert!(config.no_download.is_none());
assert!(config.timeout.is_none());
assert!(config.profile.is_none());
}
#[test]
fn test_file_config_whitespace_only() {
let toml_content = " ";
let config: File = toml::from_str(toml_content).unwrap();
assert!(config.no_download.is_none());
}
#[test]
fn test_ca_cert_path_some() {
let config = Config {
network: NetworkConfig {
ca_cert: Some("/path/to/cert".to_string()),
..Default::default()
},
..Default::default()
};
let path = config.ca_cert_path();
assert!(path.is_some());
assert_eq!(path.unwrap(), std::path::PathBuf::from("/path/to/cert"));
}
#[test]
fn test_ca_cert_path_none() {
let config = Config::default();
let path = config.ca_cert_path();
assert!(path.is_none());
}
#[test]
fn test_output_source_debug() {
let src = OutputSource::default();
let debug_str = format!("{src:?}");
assert!(debug_str.contains("OutputSource"));
}
#[test]
fn test_test_source_debug() {
let src = TestSource::default();
let debug_str = format!("{src:?}");
assert!(debug_str.contains("TestSource"));
}
#[test]
fn test_network_source_debug() {
let src = NetworkSource::default();
let debug_str = format!("{src:?}");
assert!(debug_str.contains("NetworkSource"));
}
#[test]
fn test_server_source_debug() {
let src = ServerSource::default();
let debug_str = format!("{src:?}");
assert!(debug_str.contains("ServerSource"));
}
#[test]
fn test_config_source_default() {
let source = ConfigSource::default();
assert_eq!(source.output.csv_delimiter, ',');
assert_eq!(source.output.theme, "dark");
assert_eq!(source.network.timeout, 10);
assert!(source.test.no_download.is_none());
assert!(source.servers.server_ids.is_empty());
}
#[test]
fn test_config_default() {
let config = Config::default();
assert!(!config.output.bytes);
assert!(!config.test.no_download);
assert_eq!(config.network.timeout, 10);
assert!(config.servers.server_ids.is_empty());
assert!(!config.strict);
}
#[test]
#[allow(deprecated)]
fn test_deprecated_simple_flag() {
let args = Args::parse_from(["netspeed-cli", "--simple"]);
assert_eq!(args.simple, Some(true));
let config = Config::from_args(&args);
assert!(config.simple());
}
#[test]
#[allow(deprecated)]
fn test_deprecated_json_flag() {
let args = Args::parse_from(["netspeed-cli", "--json"]);
assert_eq!(args.json, Some(true));
let config = Config::from_args(&args);
assert!(config.json());
}
#[test]
#[allow(deprecated)]
fn test_deprecated_csv_flag() {
let args = Args::parse_from(["netspeed-cli", "--csv"]);
assert_eq!(args.csv, Some(true));
let config = Config::from_args(&args);
assert!(config.csv());
}
#[test]
fn test_server_selection_clone_preserves_data() {
let selection = ServerSelection {
server_ids: vec!["a".to_string(), "b".to_string()],
exclude_ids: vec!["c".to_string()],
};
let cloned = selection.clone();
assert_eq!(selection.server_ids, cloned.server_ids);
assert_eq!(selection.exclude_ids, cloned.exclude_ids);
}
#[test]
fn test_config_server_ids_empty() {
let args = Args::parse_from(["netspeed-cli"]);
let config = Config::from_args(&args);
assert!(config.server_ids().is_empty());
}
#[test]
fn test_config_exclude_ids_empty() {
let args = Args::parse_from(["netspeed-cli"]);
let config = Config::from_args(&args);
assert!(config.exclude_ids().is_empty());
}
#[test]
fn test_config_from_args_defaults() {
let args = Args::parse_from(["netspeed-cli"]);
let config = Config::from_args(&args);
assert!(!config.test.no_download);
assert!(!config.test.no_upload);
assert!(!config.test.single);
assert!(!config.output.bytes);
assert!(!config.output.simple);
assert!(!config.output.csv);
assert!(!config.output.json);
assert!(!config.output.list);
assert!(!config.output.quiet);
assert_eq!(config.network.timeout, 10);
assert_eq!(config.output.csv_delimiter, ',');
assert!(!config.output.csv_header);
assert!(config.servers.server_ids.is_empty());
assert!(config.servers.exclude_ids.is_empty());
}
#[test]
fn test_config_from_args_no_download() {
let args = Args::parse_from(["netspeed-cli", "--no-download"]);
let config = Config::from_args(&args);
assert!(config.test.no_download);
assert!(!config.test.no_upload);
}
#[test]
fn test_config_file_deserialization() {
let toml_content = r"
no_download = true
no_upload = false
single = true
bytes = true
simple = false
csv = false
csv_delimiter = ';'
csv_header = true
json = true
timeout = 30
";
let config: File = toml::from_str(toml_content).unwrap();
assert_eq!(config.no_download, Some(true));
assert_eq!(config.no_upload, Some(false));
assert_eq!(config.single, Some(true));
assert_eq!(config.bytes, Some(true));
assert_eq!(config.simple, Some(false));
assert_eq!(config.csv, Some(false));
assert_eq!(config.csv_delimiter, Some(';'));
assert_eq!(config.csv_header, Some(true));
assert_eq!(config.json, Some(true));
assert_eq!(config.timeout, Some(30));
}
#[test]
fn test_config_file_partial() {
let toml_content = r"
no_download = true
timeout = 20
";
let config: File = toml::from_str(toml_content).unwrap();
assert_eq!(config.no_download, Some(true));
assert!(config.no_upload.is_none());
assert!(config.single.is_none());
assert_eq!(config.timeout, Some(20));
assert!(config.csv_delimiter.is_none());
}
#[test]
fn test_config_from_args_overrides_file() {
let args = Args::parse_from(["netspeed-cli", "--no-download"]);
let config = Config::from_args(&args);
assert!(config.test.no_download);
}
#[test]
fn test_config_merge_bool_file_true_cli_false() {
let toml_content = r"
no_download = true
";
let file_config: File = toml::from_str(toml_content).unwrap();
let args = Args::parse_from(["netspeed-cli"]);
let file_config_loaded = Some(file_config);
let cli_val = args.no_download; let file_val = file_config_loaded.and_then(|c| c.no_download); let merged = cli_val.or(file_val).unwrap_or(false);
assert!(merged);
}
#[test]
fn test_validate_config_valid_profile() {
let file_config = File {
profile: Some("gamer".to_string()),
..Default::default()
};
let result = validate_config(&file_config);
assert!(result.valid);
assert!(result.errors.is_empty());
}
#[test]
fn test_validate_config_empty_is_valid() {
let file_config = File::default();
let result = validate_config(&file_config);
assert!(result.valid);
assert!(result.errors.is_empty());
assert!(result.warnings.is_empty());
}
#[test]
fn test_validate_config_invalid_profile() {
let file_config = File {
profile: Some("invalid_profile".to_string()),
..Default::default()
};
let result = validate_config(&file_config);
assert!(!result.valid);
assert!(!result.errors.is_empty());
assert!(result.errors[0].contains("invalid_profile"));
}
#[test]
fn test_validate_config_invalid_theme() {
let file_config = File {
theme: Some("neon".to_string()),
..Default::default()
};
let result = validate_config(&file_config);
assert!(!result.valid);
assert!(!result.errors.is_empty());
assert!(result.errors[0].contains("neon"));
}
#[test]
fn test_validate_config_invalid_csv_delimiter() {
let file_config = File {
csv_delimiter: Some('X'),
..Default::default()
};
let result = validate_config(&file_config);
assert!(!result.valid);
assert!(!result.errors.is_empty());
}
#[test]
fn test_validate_config_deprecated_simple() {
let file_config = File {
simple: Some(true),
..Default::default()
};
let result = validate_config(&file_config);
assert!(result.valid);
assert!(!result.warnings.is_empty());
assert!(
result
.warnings
.iter()
.any(|w| w.contains("simple") && w.contains("deprecated"))
);
}
#[test]
fn test_validate_config_multiple_issues() {
let file_config = File {
profile: Some("bad".to_string()),
theme: Some("ugly".to_string()),
csv_delimiter: Some('@'),
..Default::default()
};
let result = validate_config(&file_config);
assert!(!result.valid);
assert!(result.errors.len() >= 3); }
#[test]
fn test_tls_config_defaults() {
let args = Args::parse_from(["netspeed-cli"]);
let config = Config::from_args(&args);
assert!(config.network.ca_cert.is_none());
assert!(config.network.tls_version.is_none());
assert!(!config.network.pin_certs);
}
#[test]
fn test_tls_config_file_deserialization() {
let toml_content = r#"
ca_cert = "/custom/ca.pem"
tls_version = "1.2"
pin_certs = true
"#;
let file_config: File = toml::from_str(toml_content).unwrap();
assert_eq!(file_config.ca_cert, Some("/custom/ca.pem".to_string()));
assert_eq!(file_config.tls_version, Some("1.2".to_string()));
assert_eq!(file_config.pin_certs, Some(true));
}
#[test]
fn test_tls_config_file_partial() {
let toml_content = r#"
ca_cert = "/my/ca.pem"
"#;
let file_config: File = toml::from_str(toml_content).unwrap();
assert_eq!(file_config.ca_cert, Some("/my/ca.pem".to_string()));
assert!(file_config.tls_version.is_none());
assert!(file_config.pin_certs.is_none());
}
#[test]
fn test_tls_config_cli_ca_cert() {
let temp_file = tempfile::NamedTempFile::new().unwrap();
std::fs::write(temp_file.path(), "fake cert content").unwrap();
let args = Args::parse_from([
"netspeed-cli",
"--ca-cert",
temp_file.path().to_str().unwrap(),
]);
assert_eq!(
args.ca_cert,
Some(temp_file.path().to_string_lossy().to_string())
);
}
#[test]
fn test_tls_config_cli_tls_version() {
let args = Args::parse_from(["netspeed-cli", "--tls-version", "1.3"]);
assert_eq!(args.tls_version, Some("1.3".to_string()));
}
#[test]
fn test_tls_config_cli_pin_certs() {
let args = Args::parse_from(["netspeed-cli", "--pin-certs"]);
assert_eq!(args.pin_certs, Some(true));
}
#[test]
fn test_tls_config_cli_pin_certs_false() {
let args = Args::parse_from(["netspeed-cli", "--pin-certs=false"]);
assert_eq!(args.pin_certs, Some(false));
}
#[test]
fn test_tls_config_all_cli_options() {
let temp_file = tempfile::NamedTempFile::new().unwrap();
std::fs::write(temp_file.path(), "fake cert content").unwrap();
let args = Args::parse_from([
"netspeed-cli",
"--ca-cert",
temp_file.path().to_str().unwrap(),
"--tls-version",
"1.2",
"--pin-certs",
]);
assert_eq!(
args.ca_cert,
Some(temp_file.path().to_string_lossy().to_string())
);
assert_eq!(args.tls_version, Some("1.2".to_string()));
assert_eq!(args.pin_certs, Some(true));
}
#[test]
fn test_tls_config_string_merge_cli_takes_precedence() {
let cli_val = Some("/cli/ca.pem".to_string());
let file_val = Some("/file/ca.pem".to_string());
let merged = cli_val.or(file_val.clone());
assert_eq!(merged, Some("/cli/ca.pem".to_string()));
let cli_val_none: Option<String> = None;
let merged = cli_val_none.or(file_val.clone());
assert_eq!(merged, Some("/file/ca.pem".to_string()));
let merged = Option::<String>::None.or(None);
assert!(merged.is_none());
}
#[test]
fn test_tls_config_bool_merge() {
assert!(merge_bool_test(Some(true), Some(false)));
assert!(!merge_bool_test(Some(false), Some(true)));
assert!(merge_bool_test(Some(true), None));
assert!(!merge_bool_test(Some(false), None));
assert!(merge_bool_test(None, Some(true)));
assert!(!merge_bool_test(None, Some(false)));
assert!(!merge_bool_test(None::<bool>, None));
}
fn merge_bool_test(cli: Option<bool>, file: Option<bool>) -> bool {
cli.or(file).unwrap_or(false)
}
#[test]
fn test_format_from_cli_type_all_variants() {
use crate::cli::OutputFormatType;
assert_eq!(Format::from_cli_type(OutputFormatType::Json), Format::Json);
assert_eq!(
Format::from_cli_type(OutputFormatType::Jsonl),
Format::Jsonl
);
assert_eq!(Format::from_cli_type(OutputFormatType::Csv), Format::Csv);
assert_eq!(
Format::from_cli_type(OutputFormatType::Minimal),
Format::Minimal
);
assert_eq!(
Format::from_cli_type(OutputFormatType::Simple),
Format::Simple
);
assert_eq!(
Format::from_cli_type(OutputFormatType::Compact),
Format::Compact
);
assert_eq!(
Format::from_cli_type(OutputFormatType::Detailed),
Format::Detailed
);
assert_eq!(
Format::from_cli_type(OutputFormatType::Dashboard),
Format::Dashboard
);
}
#[test]
fn test_format_is_machine_readable() {
assert!(Format::Json.is_machine_readable());
assert!(Format::Jsonl.is_machine_readable());
assert!(Format::Csv.is_machine_readable());
assert!(!Format::Minimal.is_machine_readable());
assert!(!Format::Simple.is_machine_readable());
assert!(!Format::Compact.is_machine_readable());
assert!(!Format::Detailed.is_machine_readable());
assert!(!Format::Dashboard.is_machine_readable());
}
#[test]
fn test_format_is_non_verbose() {
assert!(Format::Simple.is_non_verbose());
assert!(Format::Minimal.is_non_verbose());
assert!(Format::Compact.is_non_verbose());
assert!(Format::Json.is_non_verbose());
assert!(Format::Jsonl.is_non_verbose());
assert!(Format::Csv.is_non_verbose());
assert!(Format::Dashboard.is_non_verbose());
assert!(!Format::Detailed.is_non_verbose());
}
#[test]
fn test_format_label() {
assert_eq!(Format::Json.label(), "JSON");
assert_eq!(Format::Jsonl.label(), "JSONL");
assert_eq!(Format::Csv.label(), "CSV");
assert_eq!(Format::Minimal.label(), "Minimal");
assert_eq!(Format::Simple.label(), "Simple");
assert_eq!(Format::Compact.label(), "Compact");
assert_eq!(Format::Detailed.label(), "Detailed");
assert_eq!(Format::Dashboard.label(), "Dashboard");
}
#[test]
fn test_format_display() {
assert_eq!(format!("{}", Format::Json), "JSON");
assert_eq!(format!("{}", Format::Detailed), "Detailed");
}
#[test]
fn test_format_equality() {
assert_eq!(Format::Json, Format::Json);
assert_ne!(Format::Json, Format::Csv);
}
#[test]
fn test_config_source_from_args_defaults() {
let args = Args::parse_from(["netspeed-cli"]);
let source = ConfigSource::from_args(&args);
assert!(source.output.bytes.is_none());
assert!(source.output.simple.is_none());
assert!(source.output.csv.is_none());
assert_eq!(source.output.csv_delimiter, ',');
assert!(source.output.csv_header.is_none());
assert!(source.output.json.is_none());
assert!(!source.output.list);
assert!(source.output.quiet.is_none());
assert!(source.output.minimal.is_none());
assert!(source.output.profile.is_none());
assert_eq!(source.output.theme, "dark");
assert!(source.output.format.is_none());
assert!(source.test.no_download.is_none());
assert!(source.test.no_upload.is_none());
assert!(source.test.single.is_none());
assert!(source.network.source.is_none());
assert_eq!(source.network.timeout, 10);
assert!(source.network.ca_cert.is_none());
assert!(source.network.tls_version.is_none());
assert!(source.network.pin_certs.is_none());
assert!(source.servers.server_ids.is_empty());
assert!(source.servers.exclude_ids.is_empty());
assert!(source.strict_config.is_none());
}
#[test]
fn test_config_source_from_args_all_set() {
let args = Args::parse_from([
"netspeed-cli",
"--bytes",
"--no-download",
"--no-upload",
"--single",
"--timeout",
"30",
"--source",
"0.0.0.0",
"--server",
"1234",
"--exclude",
"5678",
"--profile",
"gamer",
"--theme",
"light",
"--format",
"json",
]);
let source = ConfigSource::from_args(&args);
assert_eq!(source.output.bytes, Some(true));
assert_eq!(source.test.no_download, Some(true));
assert_eq!(source.test.no_upload, Some(true));
assert_eq!(source.test.single, Some(true));
assert_eq!(source.network.timeout, 30);
assert_eq!(source.network.source, Some("0.0.0.0".to_string()));
assert_eq!(source.servers.server_ids, vec!["1234".to_string()]);
assert_eq!(source.servers.exclude_ids, vec!["5678".to_string()]);
assert_eq!(source.output.profile, Some("gamer".to_string()));
assert_eq!(source.output.theme, "light");
assert_eq!(source.output.format, Some(Format::Json));
}
#[test]
fn test_config_source_format_conversion() {
let args = Args::parse_from(["netspeed-cli", "--format", "csv"]);
let source = ConfigSource::from_args(&args);
assert_eq!(source.output.format, Some(Format::Csv));
let args = Args::parse_from(["netspeed-cli", "--format", "dashboard"]);
let source = ConfigSource::from_args(&args);
assert_eq!(source.output.format, Some(Format::Dashboard));
}
#[test]
fn test_config_source_preserves_option_bools() {
let args = Args::parse_from(["netspeed-cli", "--no-download=false"]);
let source = ConfigSource::from_args(&args);
assert_eq!(source.test.no_download, Some(false));
let args = Args::parse_from(["netspeed-cli"]);
let source = ConfigSource::from_args(&args);
assert!(source.test.no_download.is_none());
}
#[test]
fn test_config_source_default_composes_sub_sources() {
let source = ConfigSource::default();
assert_eq!(
source.output.csv_delimiter,
OutputSource::default().csv_delimiter
);
assert_eq!(source.output.theme, OutputSource::default().theme);
assert_eq!(source.network.timeout, NetworkSource::default().timeout);
assert!(source.test.no_download.is_none()); assert!(source.servers.server_ids.is_empty());
assert!(source.output.bytes.is_none());
assert!(source.network.source.is_none());
assert!(source.strict_config.is_none());
}
#[test]
fn test_output_source_default() {
let src = OutputSource::default();
assert!(src.bytes.is_none());
assert!(src.simple.is_none());
assert!(src.csv.is_none());
assert_eq!(src.csv_delimiter, ',');
assert!(src.csv_header.is_none());
assert!(src.json.is_none());
assert!(!src.list);
assert!(src.quiet.is_none());
assert!(src.minimal.is_none());
assert!(src.profile.is_none());
assert_eq!(src.theme, "dark");
assert!(src.format.is_none());
}
#[test]
fn test_output_source_custom() {
let src = OutputSource {
bytes: Some(true),
csv_delimiter: ';',
list: true,
profile: Some("gamer".to_string()),
theme: "light".to_string(),
format: Some(Format::Json),
..Default::default()
};
assert_eq!(src.bytes, Some(true));
assert_eq!(src.csv_delimiter, ';');
assert!(src.list);
assert_eq!(src.profile, Some("gamer".to_string()));
assert_eq!(src.theme, "light");
assert_eq!(src.format, Some(Format::Json));
assert!(src.simple.is_none());
assert!(src.csv.is_none());
assert!(src.json.is_none());
}
#[test]
fn test_output_source_clone() {
let src = OutputSource {
profile: Some("streamer".to_string()),
..Default::default()
};
let cloned = src.clone();
assert_eq!(src.profile, cloned.profile);
assert_eq!(src.csv_delimiter, cloned.csv_delimiter);
assert_eq!(src.theme, cloned.theme);
}
#[test]
fn test_test_source_default() {
let src = TestSource::default();
assert!(src.no_download.is_none());
assert!(src.no_upload.is_none());
assert!(src.single.is_none());
}
#[test]
fn test_test_source_custom() {
let src = TestSource {
no_download: Some(true),
no_upload: Some(false),
single: Some(true),
};
assert_eq!(src.no_download, Some(true));
assert_eq!(src.no_upload, Some(false));
assert_eq!(src.single, Some(true));
}
#[test]
fn test_test_source_clone() {
let src = TestSource {
no_download: Some(true),
..Default::default()
};
let cloned = src.clone();
assert_eq!(src.no_download, cloned.no_download);
}
#[test]
fn test_network_source_default() {
let src = NetworkSource::default();
assert!(src.source.is_none());
assert_eq!(src.timeout, 10);
assert!(src.ca_cert.is_none());
assert!(src.tls_version.is_none());
assert!(src.pin_certs.is_none());
}
#[test]
fn test_network_source_custom() {
let src = NetworkSource {
source: Some("0.0.0.0".to_string()),
timeout: 60,
ca_cert: Some("/path/to/ca.pem".to_string()),
tls_version: Some("1.3".to_string()),
pin_certs: Some(true),
};
assert_eq!(src.source, Some("0.0.0.0".to_string()));
assert_eq!(src.timeout, 60);
assert_eq!(src.ca_cert, Some("/path/to/ca.pem".to_string()));
assert_eq!(src.tls_version, Some("1.3".to_string()));
assert_eq!(src.pin_certs, Some(true));
}
#[test]
fn test_network_source_clone() {
let src = NetworkSource {
source: Some("192.168.1.1".to_string()),
..Default::default()
};
let cloned = src.clone();
assert_eq!(src.source, cloned.source);
assert_eq!(src.timeout, cloned.timeout);
}
#[test]
fn test_server_source_default() {
let src = ServerSource::default();
assert!(src.server_ids.is_empty());
assert!(src.exclude_ids.is_empty());
}
#[test]
fn test_server_source_custom() {
let src = ServerSource {
server_ids: vec!["1234".to_string()],
exclude_ids: vec!["5678".to_string()],
};
assert_eq!(src.server_ids, vec!["1234".to_string()]);
assert_eq!(src.exclude_ids, vec!["5678".to_string()]);
}
#[test]
fn test_server_source_clone() {
let src = ServerSource {
server_ids: vec!["1234".to_string(), "5678".to_string()],
..Default::default()
};
let cloned = src.clone();
assert_eq!(src.server_ids, cloned.server_ids);
assert_eq!(src.exclude_ids, cloned.exclude_ids);
}
#[test]
fn test_output_config_default() {
let config = OutputConfig::default();
assert!(!config.bytes);
assert!(!config.simple);
assert!(!config.csv);
assert_eq!(config.csv_delimiter, ',');
assert!(!config.csv_header);
assert!(!config.json);
assert!(!config.list);
assert!(!config.quiet);
assert!(config.profile.is_none());
assert_eq!(config.theme, Theme::Dark);
assert!(!config.minimal);
assert!(config.format.is_none());
}
#[test]
fn test_output_config_clone() {
let config = OutputConfig::default();
let cloned = config.clone();
assert_eq!(config.bytes, cloned.bytes);
assert_eq!(config.csv_delimiter, cloned.csv_delimiter);
assert_eq!(config.theme, cloned.theme);
}
#[test]
fn test_output_config_debug() {
let config = OutputConfig::default();
let debug_str = format!("{config:?}");
assert!(debug_str.contains("OutputConfig"));
}
#[test]
fn test_output_config_custom_theme() {
let custom = OutputConfig {
theme: Theme::Light,
..Default::default()
};
assert_eq!(custom.theme, Theme::Light);
}
#[test]
fn test_output_config_csv_settings() {
let custom = OutputConfig {
csv: true,
csv_delimiter: ';',
csv_header: true,
..Default::default()
};
assert!(custom.csv);
assert_eq!(custom.csv_delimiter, ';');
assert!(custom.csv_header);
}
#[test]
fn test_test_selection_defaults() {
let config = TestSelection::default();
assert!(!config.no_download);
assert!(!config.no_upload);
assert!(!config.single);
}
#[test]
fn test_test_selection_skip_tests() {
let custom = TestSelection {
no_download: true,
no_upload: true,
single: true,
};
assert!(custom.no_download);
assert!(custom.no_upload);
assert!(custom.single);
}
#[test]
fn test_output_config_profile() {
let with_profile = OutputConfig {
profile: Some("gamer".to_string()),
..Default::default()
};
assert_eq!(with_profile.profile, Some("gamer".to_string()));
}
#[test]
fn test_network_config_default() {
let config = NetworkConfig::default();
assert!(config.source.is_none());
assert_eq!(config.timeout, 10);
assert!(config.ca_cert.is_none());
assert!(config.tls_version.is_none());
assert!(!config.pin_certs);
}
#[test]
fn test_network_config_clone() {
let config = NetworkConfig::default();
let cloned = config.clone();
assert_eq!(config.timeout, cloned.timeout);
assert_eq!(config.pin_certs, cloned.pin_certs);
}
#[test]
fn test_network_config_debug() {
let config = NetworkConfig::default();
let debug_str = format!("{config:?}");
assert!(debug_str.contains("NetworkConfig"));
}
#[test]
fn test_network_config_custom_timeout() {
let custom = NetworkConfig {
timeout: 60,
..Default::default()
};
assert_eq!(custom.timeout, 60);
}
#[test]
fn test_network_config_source_ip() {
let with_source = NetworkConfig {
source: Some("192.168.1.100".to_string()),
..Default::default()
};
assert_eq!(with_source.source, Some("192.168.1.100".to_string()));
}
#[test]
fn test_network_config_tls_settings() {
let custom = NetworkConfig {
ca_cert: Some("/path/to/ca.pem".to_string()),
tls_version: Some("1.2".to_string()),
pin_certs: true,
..Default::default()
};
assert_eq!(custom.ca_cert, Some("/path/to/ca.pem".to_string()));
assert_eq!(custom.tls_version, Some("1.2".to_string()));
assert!(custom.pin_certs);
}
#[test]
fn test_network_config_tls_1_3() {
let custom = NetworkConfig {
tls_version: Some("1.3".to_string()),
pin_certs: true,
..Default::default()
};
assert_eq!(custom.tls_version, Some("1.3".to_string()));
assert!(custom.pin_certs);
}
#[test]
fn test_server_selection_default() {
let selection = ServerSelection::default();
assert!(selection.server_ids.is_empty());
assert!(selection.exclude_ids.is_empty());
}
#[test]
fn test_server_selection_clone() {
let selection = ServerSelection::default();
let cloned = selection.clone();
assert!(cloned.server_ids.is_empty());
assert!(cloned.exclude_ids.is_empty());
}
#[test]
fn test_server_selection_debug() {
let selection = ServerSelection::default();
let debug_str = format!("{selection:?}");
assert!(debug_str.contains("ServerSelection"));
}
#[test]
fn test_server_selection_specific_ids() {
let selection = ServerSelection {
server_ids: vec!["1234".to_string(), "5678".to_string()],
exclude_ids: Vec::new(),
};
assert_eq!(selection.server_ids.len(), 2);
assert!(selection.exclude_ids.is_empty());
}
#[test]
fn test_server_selection_exclude() {
let selection = ServerSelection {
server_ids: Vec::new(),
exclude_ids: vec!["9999".to_string()],
};
assert!(selection.server_ids.is_empty());
assert_eq!(selection.exclude_ids.len(), 1);
assert_eq!(selection.exclude_ids[0], "9999");
}
#[test]
fn test_server_selection_both() {
let selection = ServerSelection {
server_ids: vec!["1234".to_string()],
exclude_ids: vec!["5678".to_string()],
};
assert_eq!(selection.server_ids.len(), 1);
assert_eq!(selection.exclude_ids.len(), 1);
}
#[test]
fn test_server_selection_from_source_empty() {
let args = Args::parse_from(["netspeed-cli"]);
let source = ConfigSource::from_args(&args);
let selection = ServerSelection::from_source(&source.servers);
assert!(selection.server_ids.is_empty());
assert!(selection.exclude_ids.is_empty());
}
#[test]
fn test_server_selection_from_source_with_servers() {
let args = Args::parse_from(["netspeed-cli", "--server", "1234", "--server", "5678"]);
let source = ConfigSource::from_args(&args);
let selection = ServerSelection::from_source(&source.servers);
assert_eq!(selection.server_ids, vec!["1234", "5678"]);
}
#[test]
fn test_server_selection_from_source_with_excludes() {
let args = Args::parse_from(["netspeed-cli", "--exclude", "9999", "--exclude", "8888"]);
let source = ConfigSource::from_args(&args);
let selection = ServerSelection::from_source(&source.servers);
assert_eq!(selection.exclude_ids, vec!["9999", "8888"]);
}
#[test]
fn test_config_getters_match_direct_access() {
let config = Config::default();
assert_eq!(config.no_download(), config.test.no_download);
assert_eq!(config.no_upload(), config.test.no_upload);
assert_eq!(config.single(), config.test.single);
assert_eq!(config.bytes(), config.output.bytes);
assert_eq!(config.simple(), config.output.simple);
assert_eq!(config.csv(), config.output.csv);
assert_eq!(config.json(), config.output.json);
assert_eq!(config.quiet(), config.output.quiet);
assert_eq!(config.list(), config.output.list);
assert_eq!(config.minimal(), config.output.minimal);
assert_eq!(config.theme(), config.output.theme);
assert_eq!(config.csv_delimiter(), config.output.csv_delimiter);
assert_eq!(config.csv_header(), config.output.csv_header);
assert_eq!(config.profile(), config.output.profile.as_deref());
assert_eq!(config.format(), config.output.format);
assert_eq!(config.timeout(), config.network.timeout);
assert_eq!(config.source(), config.network.source.as_deref());
assert_eq!(config.ca_cert(), config.network.ca_cert.as_deref());
assert_eq!(config.tls_version(), config.network.tls_version.as_deref());
assert_eq!(config.pin_certs(), config.network.pin_certs);
assert_eq!(config.server_ids(), &config.servers.server_ids[..]);
assert_eq!(config.exclude_ids(), &config.servers.exclude_ids[..]);
assert_eq!(
config.custom_user_agent(),
config.custom_user_agent.as_deref()
);
assert_eq!(config.strict(), config.strict);
}
#[test]
fn test_config_getter_returns_for_option_fields() {
let config = Config {
output: OutputConfig {
profile: Some("gamer".to_string()),
..Default::default()
},
test: TestSelection {
no_download: false,
no_upload: false,
single: false,
},
network: NetworkConfig {
source: Some("192.168.1.1".to_string()),
ca_cert: Some("/path/to/cert".to_string()),
tls_version: Some("1.3".to_string()),
..Default::default()
},
servers: ServerSelection {
server_ids: vec!["1234".to_string()],
exclude_ids: vec!["5678".to_string()],
},
custom_user_agent: Some("CustomAgent/1.0".to_string()),
strict: true,
};
assert_eq!(config.profile(), Some("gamer"));
assert_eq!(config.source(), Some("192.168.1.1"));
assert_eq!(config.ca_cert(), Some("/path/to/cert"));
assert_eq!(config.tls_version(), Some("1.3"));
assert_eq!(config.custom_user_agent(), Some("CustomAgent/1.0"));
assert_eq!(config.server_ids(), ["1234"]);
assert_eq!(config.exclude_ids(), ["5678"]);
assert!(!config.pin_certs()); assert!(config.strict());
}
#[test]
fn test_config_getters_none_for_unset_options() {
let config = Config::default();
assert_eq!(config.profile(), None);
assert_eq!(config.source(), None);
assert_eq!(config.ca_cert(), None);
assert_eq!(config.tls_version(), None);
assert_eq!(config.custom_user_agent(), None);
assert!(config.server_ids().is_empty());
assert!(config.exclude_ids().is_empty());
}
#[test]
fn test_should_save_history_default_format() {
let config = Config::default();
assert!(config.should_save_history());
}
#[test]
fn test_should_save_history_json_format() {
let mut config = Config::default();
config.output.format = Some(Format::Json);
assert!(!config.should_save_history());
}
#[test]
fn test_should_save_history_jsonl_format() {
let mut config = Config::default();
config.output.format = Some(Format::Jsonl);
assert!(!config.should_save_history());
}
#[test]
fn test_should_save_history_csv_format() {
let mut config = Config::default();
config.output.format = Some(Format::Csv);
assert!(!config.should_save_history());
}
#[test]
fn test_should_save_history_non_machine_readable_formats() {
for fmt in [
Format::Minimal,
Format::Simple,
Format::Compact,
Format::Detailed,
Format::Dashboard,
] {
let mut config = Config::default();
config.output.format = Some(fmt);
assert!(
config.should_save_history(),
"format {:?} should save history",
fmt
);
}
}
#[test]
fn test_should_save_history_legacy_json_flag() {
let mut config = Config::default();
config.output.json = true;
assert!(!config.should_save_history());
}
#[test]
fn test_should_save_history_legacy_csv_flag() {
let mut config = Config::default();
config.output.csv = true;
assert!(!config.should_save_history());
}
#[test]
fn test_should_save_history_both_format_and_legacy() {
let mut config = Config::default();
config.output.format = Some(Format::Detailed); config.output.json = true; assert!(!config.should_save_history());
}
#[test]
fn test_should_save_history_verbose_detailed() {
let mut config = Config::default();
config.output.format = Some(Format::Detailed);
assert!(config.should_save_history());
}
#[test]
fn test_validate_and_report_with_file_config() {
let source = ConfigSource::default();
let config = Config::from_source(&source);
let file_config = File::default();
let result = config.validate_and_report(&source, Some(file_config));
assert!(result.valid);
}
#[test]
fn test_validate_and_report_invalid_profile() {
let mut source = ConfigSource::default();
source.output.profile = Some("invalid_profile_xyz".to_string());
let config = Config::from_source(&source);
let file_config = File::default();
let result = config.validate_and_report(&source, Some(file_config));
assert!(result.valid); assert!(!result.warnings.is_empty());
assert!(result.warnings[0].contains("invalid_profile_xyz"));
}
#[test]
fn test_validate_and_report_invalid_file_config() {
let source = ConfigSource::default();
let config = Config::from_source(&source);
let file_config = File {
profile: Some("bad_profile".to_string()),
..Default::default()
};
let result = config.validate_and_report(&source, Some(file_config));
assert!(!result.valid);
assert!(!result.errors.is_empty());
}
#[test]
fn test_config_default_composes_sub_structs() {
let config = Config::default();
assert!(!config.output.bytes); let _ = config.output;
let _ = config.test;
let _ = config.network;
let _ = config.servers;
assert!(!config.test.no_download);
assert_eq!(config.network.timeout, 10);
assert!(config.servers.server_ids.is_empty());
}
#[test]
fn test_config_clone_preserves_all_fields() {
let config = Config {
output: OutputConfig {
bytes: true,
theme: Theme::Light,
profile: Some("test".to_string()),
..Default::default()
},
test: TestSelection {
no_download: true,
no_upload: false,
single: true,
},
network: NetworkConfig {
timeout: 30,
source: Some("127.0.0.1".to_string()),
pin_certs: true,
..Default::default()
},
servers: ServerSelection {
server_ids: vec!["abc".to_string()],
exclude_ids: vec!["xyz".to_string()],
},
custom_user_agent: Some("TestAgent".to_string()),
strict: true,
};
let cloned = config.clone();
assert!(cloned.output.bytes);
assert_eq!(cloned.output.theme, Theme::Light);
assert_eq!(cloned.output.profile, Some("test".to_string()));
assert!(cloned.test.no_download);
assert!(!cloned.test.no_upload);
assert!(cloned.test.single);
assert_eq!(cloned.network.timeout, 30);
assert_eq!(cloned.network.source, Some("127.0.0.1".to_string()));
assert!(cloned.network.pin_certs);
assert_eq!(cloned.servers.server_ids, vec!["abc".to_string()]);
assert_eq!(cloned.servers.exclude_ids, vec!["xyz".to_string()]);
assert_eq!(cloned.custom_user_agent, Some("TestAgent".to_string()));
assert!(cloned.strict);
}
}