1use crate::theme::Theme;
2use directories::ProjectDirs;
3use serde::Deserialize;
4use std::fs;
5use std::path::PathBuf;
6
7pub mod output;
8
9#[derive(Debug, Clone)]
34pub struct OutputSource {
35 pub bytes: Option<bool>,
37 pub simple: Option<bool>,
39 pub csv: Option<bool>,
41 pub csv_delimiter: char,
43 pub csv_header: Option<bool>,
45 pub json: Option<bool>,
47 pub list: bool,
49 pub quiet: Option<bool>,
51 pub minimal: Option<bool>,
53 pub profile: Option<String>,
55 pub theme: String,
57 pub format: Option<Format>,
59}
60
61#[derive(Debug, Clone, Default)]
81pub struct TestSource {
82 pub no_download: Option<bool>,
84 pub no_upload: Option<bool>,
86 pub single: Option<bool>,
88}
89
90#[derive(Debug, Clone)]
110pub struct NetworkSource {
111 pub source: Option<String>,
113 pub timeout: u64,
115 pub ca_cert: Option<String>,
117 pub tls_version: Option<String>,
119 pub pin_certs: Option<bool>,
121}
122
123#[derive(Debug, Clone, Default)]
142pub struct ServerSource {
143 pub server_ids: Vec<String>,
145 pub exclude_ids: Vec<String>,
147}
148
149#[derive(Debug, Clone, Default)]
195pub struct ConfigSource {
196 pub output: OutputSource,
198 pub test: TestSource,
200 pub network: NetworkSource,
202 pub servers: ServerSource,
204 pub strict_config: Option<bool>,
206}
207
208impl Default for OutputSource {
212 fn default() -> Self {
213 Self {
214 bytes: None,
215 simple: None,
216 csv: None,
217 csv_delimiter: ',',
218 csv_header: None,
219 json: None,
220 list: false,
221 quiet: None,
222 minimal: None,
223 profile: None,
224 theme: "dark".to_string(),
225 format: None,
226 }
227 }
228}
229
230impl Default for NetworkSource {
231 fn default() -> Self {
232 Self {
233 source: None,
234 timeout: 10,
235 ca_cert: None,
236 tls_version: None,
237 pin_certs: None,
238 }
239 }
240}
241
242impl ConfigSource {
243 #[must_use]
251 #[allow(deprecated)] pub(crate) fn from_args(args: &crate::cli::Args) -> Self {
253 Self {
254 output: OutputSource {
255 bytes: args.bytes,
256 simple: args.simple,
257 csv: args.csv,
258 csv_delimiter: args.csv_delimiter,
259 csv_header: args.csv_header,
260 json: args.json,
261 list: args.list,
262 quiet: args.quiet,
263 minimal: args.minimal,
264 profile: args.profile.clone(),
265 theme: args.theme.clone(),
266 format: args.format.map(Format::from_cli_type),
267 },
268 test: TestSource {
269 no_download: args.no_download,
270 no_upload: args.no_upload,
271 single: args.single,
272 },
273 network: NetworkSource {
274 source: args.source.clone(),
275 timeout: args.timeout,
276 ca_cert: args.ca_cert.clone(),
277 tls_version: args.tls_version.clone(),
278 pin_certs: args.pin_certs,
279 },
280 servers: ServerSource {
281 server_ids: args.server.clone(),
282 exclude_ids: args.exclude.clone(),
283 },
284 strict_config: args.strict_config,
285 }
286 }
287}
288
289#[derive(Debug, Clone, Copy, PartialEq, Eq)]
299pub enum Format {
300 Json,
302 Jsonl,
304 Csv,
306 Minimal,
308 Simple,
310 Compact,
312 Detailed,
314 Dashboard,
316}
317
318impl Format {
319 #[must_use]
337 pub(crate) fn from_cli_type(cli: crate::cli::OutputFormatType) -> Self {
338 match cli {
339 crate::cli::OutputFormatType::Json => Self::Json,
340 crate::cli::OutputFormatType::Jsonl => Self::Jsonl,
341 crate::cli::OutputFormatType::Csv => Self::Csv,
342 crate::cli::OutputFormatType::Minimal => Self::Minimal,
343 crate::cli::OutputFormatType::Simple => Self::Simple,
344 crate::cli::OutputFormatType::Compact => Self::Compact,
345 crate::cli::OutputFormatType::Detailed => Self::Detailed,
346 crate::cli::OutputFormatType::Dashboard => Self::Dashboard,
347 }
348 }
349
350 #[must_use]
369 pub fn is_machine_readable(self) -> bool {
370 matches!(self, Self::Json | Self::Jsonl | Self::Csv)
371 }
372
373 #[must_use]
393 pub fn is_non_verbose(self) -> bool {
394 matches!(
395 self,
396 Self::Simple
397 | Self::Minimal
398 | Self::Compact
399 | Self::Json
400 | Self::Jsonl
401 | Self::Csv
402 | Self::Dashboard
403 )
404 }
405
406 #[must_use]
422 pub fn label(self) -> &'static str {
423 match self {
424 Self::Json => "JSON",
425 Self::Jsonl => "JSONL",
426 Self::Csv => "CSV",
427 Self::Minimal => "Minimal",
428 Self::Simple => "Simple",
429 Self::Compact => "Compact",
430 Self::Detailed => "Detailed",
431 Self::Dashboard => "Dashboard",
432 }
433 }
434}
435
436impl std::fmt::Display for Format {
437 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
438 f.write_str(self.label())
439 }
440}
441
442#[derive(Debug, Clone)]
463pub struct OutputConfig {
464 pub bytes: bool,
466 pub simple: bool,
468 pub csv: bool,
470 pub csv_delimiter: char,
472 pub csv_header: bool,
474 pub json: bool,
476 pub list: bool,
478 pub quiet: bool,
480 pub profile: Option<String>,
482 pub theme: Theme,
484 pub minimal: bool,
486 pub format: Option<Format>,
488}
489
490impl Default for OutputConfig {
494 fn default() -> Self {
495 Self {
496 bytes: false,
497 simple: false,
498 csv: false,
499 csv_delimiter: ',',
500 csv_header: false,
501 json: false,
502 list: false,
503 quiet: false,
504 profile: None,
505 theme: Theme::Dark,
506 minimal: false,
507 format: None,
508 }
509 }
510}
511
512impl OutputConfig {
513 #[must_use]
515 #[allow(deprecated)]
516 pub(crate) fn from_source(
517 source: &OutputSource,
518 file_config: &File,
519 merge_bool: impl Fn(Option<bool>, Option<bool>) -> bool,
520 ) -> Self {
521 let theme = if source.theme == "dark" {
522 file_config
523 .theme
524 .as_ref()
525 .and_then(|t| Theme::from_name(t))
526 .unwrap_or_default()
527 } else {
528 Theme::from_name(&source.theme).unwrap_or_default()
529 };
530
531 Self {
532 bytes: merge_bool(source.bytes, file_config.bytes),
533 simple: merge_bool(source.simple, file_config.simple),
534 csv: merge_bool(source.csv, file_config.csv),
535 csv_delimiter: if source.csv_delimiter == ',' {
536 file_config.csv_delimiter.unwrap_or(',')
537 } else {
538 source.csv_delimiter
539 },
540 csv_header: merge_bool(source.csv_header, file_config.csv_header),
541 json: merge_bool(source.json, file_config.json),
542 list: source.list,
543 quiet: merge_bool(source.quiet, None),
544 profile: source.profile.clone().or(file_config.profile.clone()),
545 theme,
546 minimal: merge_bool(source.minimal, None),
547 format: source.format,
548 }
549 }
550}
551
552#[derive(Debug, Clone, Default)]
577pub struct TestSelection {
578 pub no_download: bool,
580 pub no_upload: bool,
582 pub single: bool,
584}
585
586impl TestSelection {
587 #[must_use]
589 pub(crate) fn from_source(
590 source: &TestSource,
591 file_config: &File,
592 merge_bool: impl Fn(Option<bool>, Option<bool>) -> bool,
593 ) -> Self {
594 Self {
595 no_download: merge_bool(source.no_download, file_config.no_download),
596 no_upload: merge_bool(source.no_upload, file_config.no_upload),
597 single: merge_bool(source.single, file_config.single),
598 }
599 }
600}
601
602#[derive(Debug, Clone)]
629pub struct NetworkConfig {
630 pub source: Option<String>,
632 pub timeout: u64,
634 pub ca_cert: Option<String>,
636 pub tls_version: Option<String>,
638 pub pin_certs: bool,
640}
641
642impl Default for NetworkConfig {
643 fn default() -> Self {
644 Self {
645 source: None,
646 timeout: 10,
647 ca_cert: None,
648 tls_version: None,
649 pin_certs: false,
650 }
651 }
652}
653
654impl NetworkConfig {
655 #[must_use]
657 pub(crate) fn from_source(
658 source: &NetworkSource,
659 file_config: &File,
660 merge_bool: impl Fn(Option<bool>, Option<bool>) -> bool,
661 merge_u64: impl Fn(u64, Option<u64>, u64) -> u64,
662 ) -> Self {
663 Self {
664 source: source.source.clone(),
665 timeout: merge_u64(source.timeout, file_config.timeout, 10),
666 ca_cert: source.ca_cert.clone().or(file_config.ca_cert.clone()),
667 tls_version: source
668 .tls_version
669 .clone()
670 .or(file_config.tls_version.clone()),
671 pin_certs: merge_bool(source.pin_certs, file_config.pin_certs),
672 }
673 }
674}
675
676#[derive(Debug, Clone, Default)]
694pub struct ServerSelection {
695 pub server_ids: Vec<String>,
697 pub exclude_ids: Vec<String>,
699}
700
701impl ServerSelection {
702 #[must_use]
704 pub(crate) fn from_source(source: &ServerSource) -> Self {
705 Self {
706 server_ids: source.server_ids.clone(),
707 exclude_ids: source.exclude_ids.clone(),
708 }
709 }
710}
711
712#[derive(Debug, Default, Clone, Deserialize)]
717pub struct File {
718 pub no_download: Option<bool>,
719 pub no_upload: Option<bool>,
720 pub single: Option<bool>,
721 pub bytes: Option<bool>,
722 pub simple: Option<bool>,
723 pub csv: Option<bool>,
724 pub csv_delimiter: Option<char>,
725 pub csv_header: Option<bool>,
726 pub json: Option<bool>,
727 pub timeout: Option<u64>,
728 pub profile: Option<String>,
729 pub theme: Option<String>,
730 pub custom_user_agent: Option<String>,
732 pub strict: Option<bool>,
734 pub ca_cert: Option<String>,
736 pub tls_version: Option<String>,
738 pub pin_certs: Option<bool>,
740}
741
742#[derive(Debug, Clone, Default)]
751pub struct Config {
752 pub output: OutputConfig,
754 pub test: TestSelection,
756 pub network: NetworkConfig,
758 pub servers: ServerSelection,
760 pub custom_user_agent: Option<String>,
762 pub strict: bool,
764}
765
766pub trait ConfigProvider: Send + Sync {
768 fn config(&self) -> &Config;
769}
770
771impl ConfigProvider for Config {
772 fn config(&self) -> &Config {
773 self
774 }
775}
776
777impl Config {
778 #[allow(deprecated)]
804 #[must_use]
805 pub fn from_args(args: &crate::cli::Args) -> Self {
806 let source = ConfigSource::from_args(args);
807 Self::from_source(&source)
808 }
809
810 #[allow(deprecated)]
846 #[must_use]
847 pub fn from_args_with_file(
848 source: &ConfigSource,
849 file_config: Option<File>,
850 ) -> (Self, ValidationResult) {
851 let config = Self::from_source_with_file(source, file_config);
852
853 let mut validation = ValidationResult::ok();
855 if let Some(ref profile_name) = source.output.profile {
856 if crate::profiles::UserProfile::validate(profile_name).is_err() {
857 validation = validation.with_warning(format!(
858 "Unknown profile '{}'. Valid options: {}. Using 'power-user'.",
859 profile_name,
860 crate::profiles::UserProfile::VALID_NAMES.join(", ")
861 ));
862 }
863 }
864
865 (config, validation)
866 }
867
868 #[must_use]
931 pub fn from_source(source: &ConfigSource) -> Self {
932 let file_config = load_config_file().unwrap_or_default();
933 Self::from_source_with_file(source, Some(file_config))
934 }
935
936 #[must_use]
945 pub(crate) fn from_source_with_file(source: &ConfigSource, file_config: Option<File>) -> Self {
946 let file = file_config.unwrap_or_default();
947
948 let strict = source.strict_config.unwrap_or(file.strict.unwrap_or(false));
949
950 let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
951 let merge_u64 = |cli: u64, file: Option<u64>, default: u64| {
952 if cli == default {
953 file.unwrap_or(default)
954 } else {
955 cli
956 }
957 };
958
959 let output = OutputConfig::from_source(&source.output, &file, merge_bool);
961 let test = TestSelection::from_source(&source.test, &file, merge_bool);
962 let network = NetworkConfig::from_source(&source.network, &file, merge_bool, merge_u64);
963 let servers = ServerSelection::from_source(&source.servers);
964
965 Self {
966 output,
967 test,
968 network,
969 servers,
970 custom_user_agent: file.custom_user_agent.clone(),
971 strict,
972 }
973 }
974
975 #[must_use]
1007 pub fn validate_and_report(
1008 &self,
1009 source: &ConfigSource,
1010 file_config: Option<File>,
1011 ) -> ValidationResult {
1012 let file = file_config.unwrap_or_else(|| load_config_file().unwrap_or_default());
1014
1015 let mut validation = validate_config(&file);
1017
1018 if let Some(ref profile_name) = source.output.profile {
1020 if crate::profiles::UserProfile::validate(profile_name).is_err() {
1021 validation = validation.with_warning(format!(
1022 "Unknown profile '{}'. Valid options: {}. Using 'power-user'.",
1023 profile_name,
1024 crate::profiles::UserProfile::VALID_NAMES.join(", ")
1025 ));
1026 }
1027 }
1028
1029 validation
1030 }
1031
1032 #[must_use]
1047 pub fn should_save_history(&self) -> bool {
1048 if self.format().is_some_and(|f| f.is_machine_readable()) {
1050 return false;
1051 }
1052 if self.json() || self.csv() {
1054 return false;
1055 }
1056 true
1057 }
1058
1059 #[must_use]
1069 pub fn no_download(&self) -> bool {
1070 self.test.no_download
1071 }
1072
1073 #[must_use]
1075 pub fn no_upload(&self) -> bool {
1076 self.test.no_upload
1077 }
1078
1079 #[must_use]
1081 pub fn single(&self) -> bool {
1082 self.test.single
1083 }
1084
1085 #[must_use]
1087 pub fn bytes(&self) -> bool {
1088 self.output.bytes
1089 }
1090
1091 #[must_use]
1093 pub fn simple(&self) -> bool {
1094 self.output.simple
1095 }
1096
1097 #[must_use]
1099 pub fn csv(&self) -> bool {
1100 self.output.csv
1101 }
1102
1103 #[must_use]
1105 pub fn json(&self) -> bool {
1106 self.output.json
1107 }
1108
1109 #[must_use]
1111 pub fn quiet(&self) -> bool {
1112 self.output.quiet
1113 }
1114
1115 #[must_use]
1117 pub fn list(&self) -> bool {
1118 self.output.list
1119 }
1120
1121 #[must_use]
1123 pub fn minimal(&self) -> bool {
1124 self.output.minimal
1125 }
1126
1127 #[must_use]
1129 pub fn theme(&self) -> Theme {
1130 self.output.theme
1131 }
1132
1133 #[must_use]
1135 pub fn csv_delimiter(&self) -> char {
1136 self.output.csv_delimiter
1137 }
1138
1139 #[must_use]
1141 pub fn csv_header(&self) -> bool {
1142 self.output.csv_header
1143 }
1144
1145 #[must_use]
1147 pub fn profile(&self) -> Option<&str> {
1148 self.output.profile.as_deref()
1149 }
1150
1151 #[must_use]
1153 pub fn format(&self) -> Option<Format> {
1154 self.output.format
1155 }
1156
1157 #[must_use]
1163 pub fn timeout(&self) -> u64 {
1164 self.network.timeout
1165 }
1166
1167 #[must_use]
1169 pub fn source(&self) -> Option<&str> {
1170 self.network.source.as_deref()
1171 }
1172
1173 #[must_use]
1175 pub fn ca_cert(&self) -> Option<&str> {
1176 self.network.ca_cert.as_deref()
1177 }
1178
1179 #[must_use]
1183 pub(crate) fn ca_cert_path(&self) -> Option<PathBuf> {
1184 self.network.ca_cert.as_ref().map(PathBuf::from)
1185 }
1186
1187 #[must_use]
1189 pub fn tls_version(&self) -> Option<&str> {
1190 self.network.tls_version.as_deref()
1191 }
1192
1193 #[must_use]
1195 pub fn pin_certs(&self) -> bool {
1196 self.network.pin_certs
1197 }
1198
1199 #[must_use]
1205 pub fn server_ids(&self) -> &[String] {
1206 &self.servers.server_ids
1207 }
1208
1209 #[must_use]
1211 pub fn exclude_ids(&self) -> &[String] {
1212 &self.servers.exclude_ids
1213 }
1214
1215 #[must_use]
1221 pub fn custom_user_agent(&self) -> Option<&str> {
1222 self.custom_user_agent.as_deref()
1223 }
1224
1225 #[must_use]
1227 pub fn strict(&self) -> bool {
1228 self.strict
1229 }
1230}
1231
1232#[derive(Debug, Clone)]
1253pub struct ValidationResult {
1254 pub valid: bool,
1256 pub errors: Vec<String>,
1258 pub warnings: Vec<String>,
1260}
1261
1262impl ValidationResult {
1263 #[must_use]
1276 pub fn ok() -> Self {
1277 Self {
1278 valid: true,
1279 errors: Vec::new(),
1280 warnings: Vec::new(),
1281 }
1282 }
1283
1284 #[must_use]
1308 pub fn with_warning(mut self, warning: impl Into<String>) -> Self {
1309 self.warnings.push(warning.into());
1310 self
1311 }
1312
1313 #[must_use]
1327 pub fn error(msg: impl Into<String>) -> Self {
1328 Self {
1329 valid: false,
1330 errors: vec![msg.into()],
1331 warnings: Vec::new(),
1332 }
1333 }
1334
1335 #[must_use]
1365 pub fn with_error(mut self, error: impl Into<String>) -> Self {
1366 self.errors.push(error.into());
1367 self.valid = false;
1368 self
1369 }
1370
1371 #[must_use]
1404 pub fn merge(mut self, other: ValidationResult) -> Self {
1405 if !other.valid {
1406 self.valid = false;
1407 }
1408 self.errors.extend(other.errors);
1409 self.warnings.extend(other.warnings);
1410 self
1411 }
1412}
1413
1414fn validate_csv_delimiter_config(delimiter: char) -> Result<(), String> {
1418 if !",;|\t".contains(delimiter) {
1419 return Err(format!(
1420 "Invalid CSV delimiter '{}'. Must be one of: comma, semicolon, pipe, or tab",
1421 delimiter
1422 ));
1423 }
1424 Ok(())
1425}
1426
1427pub fn validate_config(file_config: &File) -> ValidationResult {
1429 let mut result = ValidationResult::ok();
1430
1431 if let Some(ref profile) = file_config.profile {
1433 if let Err(e) = crate::profiles::UserProfile::validate(profile) {
1434 result = result.with_error(e);
1435 }
1436 }
1437
1438 if let Some(ref theme) = file_config.theme {
1440 if let Err(e) = crate::theme::Theme::validate(theme) {
1441 result = result.with_error(e);
1442 }
1443 }
1444
1445 if let Some(delimiter) = file_config.csv_delimiter {
1447 if let Err(e) = validate_csv_delimiter_config(delimiter) {
1448 result = result.with_error(e);
1449 }
1450 }
1451
1452 if file_config.simple.unwrap_or(false) {
1454 result = result.with_warning(
1455 "'simple' option is deprecated. Use '--format simple' instead.".to_string(),
1456 );
1457 }
1458 if file_config.csv.unwrap_or(false) {
1459 result = result
1460 .with_warning("'csv' option is deprecated. Use '--format csv' instead.".to_string());
1461 }
1462 if file_config.json.unwrap_or(false) {
1463 result = result
1464 .with_warning("'json' option is deprecated. Use '--format json' instead.".to_string());
1465 }
1466
1467 result
1468}
1469
1470#[must_use]
1472pub fn get_config_path_internal() -> Option<PathBuf> {
1473 ProjectDirs::from("dev", "vibe", "netspeed-cli").map(|proj_dirs| {
1474 let config_dir = proj_dirs.config_dir();
1475 if let Err(e) = fs::create_dir_all(config_dir) {
1476 eprintln!("Warning: Failed to create config directory: {e}");
1477 }
1478 config_dir.join("config.toml")
1479 })
1480}
1481
1482pub fn load_config_file() -> Option<File> {
1486 let path = get_config_path_internal()?;
1487 if !path.exists() {
1488 return None;
1489 }
1490
1491 let content = match fs::read_to_string(&path) {
1492 Ok(c) => c,
1493 Err(e) => {
1494 eprintln!(
1495 "Warning: Failed to read config file {}: {e}",
1496 path.display()
1497 );
1498 return None;
1499 }
1500 };
1501 let mut config: File = match toml::from_str(&content) {
1502 Ok(c) => c,
1503 Err(e) => {
1504 eprintln!("Warning: Failed to parse config: {e}");
1505 return None;
1506 }
1507 };
1508
1509 if let Some(timeout) = config.timeout {
1511 if timeout == 0 || timeout > 300 {
1512 eprintln!(
1513 "Warning: Invalid config timeout ({timeout}s, must be 1-300). Using default."
1514 );
1515 config.timeout = None;
1516 }
1517 }
1518
1519 Some(config)
1520}
1521
1522#[cfg(test)]
1523mod tests {
1524 use super::*;
1525 use crate::cli::Args;
1526 use clap::Parser;
1527
1528 #[test]
1531 fn test_config_source_from_args_list_flag() {
1532 let args = Args::parse_from(["netspeed-cli", "--list"]);
1533 let source = ConfigSource::from_args(&args);
1534 assert!(source.output.list);
1535 }
1536
1537 #[test]
1538 fn test_config_source_from_args_quiet_flag() {
1539 let args = Args::parse_from(["netspeed-cli", "--quiet"]);
1540 let source = ConfigSource::from_args(&args);
1541 assert_eq!(source.output.quiet, Some(true));
1542 }
1543
1544 #[test]
1545 fn test_config_source_from_args_minimal_flag() {
1546 let args = Args::parse_from(["netspeed-cli", "--minimal"]);
1547 let source = ConfigSource::from_args(&args);
1548 assert_eq!(source.output.minimal, Some(true));
1549 }
1550
1551 #[test]
1552 fn test_config_source_strict_config() {
1553 let args = Args::parse_from(["netspeed-cli", "--strict-config"]);
1554 let source = ConfigSource::from_args(&args);
1555 assert_eq!(source.strict_config, Some(true));
1556 }
1557
1558 #[test]
1559 fn test_config_source_from_args_bytes_flag() {
1560 let args = Args::parse_from(["netspeed-cli", "--bytes"]);
1561 let source = ConfigSource::from_args(&args);
1562 assert_eq!(source.output.bytes, Some(true));
1563 }
1564
1565 #[test]
1566 fn test_config_source_from_args_json_flag() {
1567 let args = Args::parse_from(["netspeed-cli", "--json"]);
1568 let source = ConfigSource::from_args(&args);
1569 assert_eq!(source.output.json, Some(true));
1570 }
1571
1572 #[test]
1573 fn test_config_source_from_args_csv_flag() {
1574 let args = Args::parse_from(["netspeed-cli", "--csv"]);
1575 let source = ConfigSource::from_args(&args);
1576 assert_eq!(source.output.csv, Some(true));
1577 }
1578
1579 #[test]
1580 fn test_config_source_from_args_simple_flag() {
1581 let args = Args::parse_from(["netspeed-cli", "--simple"]);
1582 let source = ConfigSource::from_args(&args);
1583 assert_eq!(source.output.simple, Some(true));
1584 }
1585
1586 #[test]
1587 fn test_config_source_from_args_csv_header_flag() {
1588 let args = Args::parse_from(["netspeed-cli", "--csv-header"]);
1589 let source = ConfigSource::from_args(&args);
1590 assert_eq!(source.output.csv_header, Some(true));
1591 }
1592
1593 #[test]
1594 fn test_config_source_from_args_csv_delimiter() {
1595 let args = Args::parse_from(["netspeed-cli", "--csv-delimiter", ";"]);
1596 let source = ConfigSource::from_args(&args);
1597 assert_eq!(source.output.csv_delimiter, ';');
1598 }
1599
1600 #[test]
1603 fn test_config_from_source_with_none_file() {
1604 let source = ConfigSource::default();
1605 let config = Config::from_source_with_file(&source, None);
1606 assert!(!config.output.bytes);
1608 assert_eq!(config.network.timeout, 10);
1609 }
1610
1611 #[test]
1612 fn test_config_from_source_with_file_profile() {
1613 let mut source = ConfigSource::default();
1614 source.output.profile = Some("streamer".to_string());
1615 let file_config = File::default();
1616 let config = Config::from_source_with_file(&source, Some(file_config));
1617 assert_eq!(config.profile(), Some("streamer"));
1618 }
1619
1620 #[test]
1621 fn test_config_from_source_strict_mode() {
1622 let source = ConfigSource {
1623 strict_config: Some(true),
1624 ..Default::default()
1625 };
1626 let file_config = File::default();
1627 let config = Config::from_source_with_file(&source, Some(file_config));
1628 assert!(config.strict());
1629 }
1630
1631 #[test]
1632 fn test_config_from_args_strict_from_file() {
1633 let toml_content = "strict = true";
1635 let file_config: File = toml::from_str(toml_content).unwrap();
1636 let source = ConfigSource::default();
1637 let config = Config::from_source_with_file(&source, Some(file_config));
1638 assert!(config.strict());
1639 }
1640
1641 #[test]
1642 fn test_config_from_args_timeout_from_file() {
1643 let toml_content = "timeout = 60";
1645 let file_config: File = toml::from_str(toml_content).unwrap();
1646 let source = ConfigSource::default(); let config = Config::from_source_with_file(&source, Some(file_config));
1648 assert_eq!(config.timeout(), 60); }
1650
1651 #[test]
1652 fn test_config_from_args_timeout_cli_overrides_file() {
1653 let toml_content = "timeout = 60";
1655 let file_config: File = toml::from_str(toml_content).unwrap();
1656 let args = Args::parse_from(["netspeed-cli", "--timeout", "120"]);
1657 let source = ConfigSource::from_args(&args);
1658 let config = Config::from_source_with_file(&source, Some(file_config));
1659 assert_eq!(config.timeout(), 120); }
1661
1662 #[test]
1663 fn test_config_from_args_custom_user_agent() {
1664 let toml_content = "custom_user_agent = \"MyAgent/1.0\"";
1665 let file_config: File = toml::from_str(toml_content).unwrap();
1666 let source = ConfigSource::default();
1667 let config = Config::from_source_with_file(&source, Some(file_config));
1668 assert_eq!(config.custom_user_agent(), Some("MyAgent/1.0"));
1669 }
1670
1671 #[test]
1674 fn test_output_config_from_source_theme_dark_cli_default() {
1675 let source = OutputSource {
1677 theme: "dark".to_string(),
1678 ..Default::default()
1679 };
1680 let file = File {
1681 theme: Some("light".to_string()),
1682 ..Default::default()
1683 };
1684 let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
1685 let output = OutputConfig::from_source(&source, &file, merge_bool);
1686 assert_eq!(output.theme, Theme::Light);
1688 }
1689
1690 #[test]
1691 fn test_output_config_from_source_theme_cli_override() {
1692 let source = OutputSource {
1694 theme: "light".to_string(),
1695 ..Default::default()
1696 };
1697 let file = File {
1698 theme: Some("high-contrast".to_string()),
1699 ..Default::default()
1700 };
1701 let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
1702 let output = OutputConfig::from_source(&source, &file, merge_bool);
1703 assert_eq!(output.theme, Theme::Light);
1705 }
1706
1707 #[test]
1708 fn test_output_config_from_source_theme_invalid_file_theme() {
1709 let source = OutputSource {
1711 theme: "dark".to_string(),
1712 ..Default::default()
1713 };
1714 let file = File {
1715 theme: Some("invalid_theme".to_string()),
1716 ..Default::default()
1717 };
1718 let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
1719 let output = OutputConfig::from_source(&source, &file, merge_bool);
1720 assert_eq!(output.theme, Theme::Dark);
1722 }
1723
1724 #[test]
1725 fn test_output_config_from_source_csv_delimiter_default() {
1726 let source = OutputSource::default(); let file = File {
1729 csv_delimiter: Some(';'),
1730 ..Default::default()
1731 };
1732 let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
1733 let output = OutputConfig::from_source(&source, &file, merge_bool);
1734 assert_eq!(output.csv_delimiter, ';');
1735 }
1736
1737 #[test]
1738 fn test_output_config_from_source_csv_delimiter_cli_override() {
1739 let source = OutputSource {
1741 csv_delimiter: '|',
1742 ..Default::default()
1743 };
1744 let file = File {
1745 csv_delimiter: Some(';'),
1746 ..Default::default()
1747 };
1748 let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
1749 let output = OutputConfig::from_source(&source, &file, merge_bool);
1750 assert_eq!(output.csv_delimiter, '|');
1751 }
1752
1753 #[test]
1754 fn test_output_config_from_source_profile_merge() {
1755 let source = OutputSource {
1757 profile: Some("cli-profile".to_string()),
1758 ..Default::default()
1759 };
1760 let file = File {
1761 profile: Some("file-profile".to_string()),
1762 ..Default::default()
1763 };
1764 let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
1765 let output = OutputConfig::from_source(&source, &file, merge_bool);
1766 assert_eq!(output.profile, Some("cli-profile".to_string()));
1767 }
1768
1769 #[test]
1770 fn test_output_config_from_source_profile_from_file() {
1771 let source = OutputSource::default();
1773 let file = File {
1774 profile: Some("file-profile".to_string()),
1775 ..Default::default()
1776 };
1777 let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
1778 let output = OutputConfig::from_source(&source, &file, merge_bool);
1779 assert_eq!(output.profile, Some("file-profile".to_string()));
1780 }
1781
1782 #[test]
1783 fn test_output_config_from_source_format() {
1784 let source = OutputSource {
1785 format: Some(Format::Dashboard),
1786 ..Default::default()
1787 };
1788 let file = File::default();
1789 let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
1790 let output = OutputConfig::from_source(&source, &file, merge_bool);
1791 assert_eq!(output.format, Some(Format::Dashboard));
1792 }
1793
1794 #[test]
1797 fn test_test_selection_from_source_all_fields() {
1798 let source = TestSource {
1799 no_download: Some(true),
1800 no_upload: Some(false),
1801 single: Some(true),
1802 };
1803 let file = File::default();
1804 let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
1805 let selection = TestSelection::from_source(&source, &file, merge_bool);
1806 assert!(selection.no_download);
1807 assert!(!selection.no_upload);
1808 assert!(selection.single);
1809 }
1810
1811 #[test]
1812 fn test_test_selection_from_source_file_fallback() {
1813 let source = TestSource::default();
1815 let file = File {
1816 no_download: Some(true),
1817 no_upload: Some(true),
1818 single: Some(false),
1819 ..Default::default()
1820 };
1821 let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
1822 let selection = TestSelection::from_source(&source, &file, merge_bool);
1823 assert!(selection.no_download);
1824 assert!(selection.no_upload);
1825 assert!(!selection.single);
1826 }
1827
1828 #[test]
1829 fn test_test_selection_from_source_both_none() {
1830 let source = TestSource::default();
1832 let file = File::default();
1833 let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
1834 let selection = TestSelection::from_source(&source, &file, merge_bool);
1835 assert!(!selection.no_download);
1836 assert!(!selection.no_upload);
1837 assert!(!selection.single);
1838 }
1839
1840 #[test]
1843 fn test_network_config_from_source_all_fields() {
1844 let source = NetworkSource {
1845 source: Some("192.168.1.1".to_string()),
1846 timeout: 60,
1847 ca_cert: Some("/path/to/cert".to_string()),
1848 tls_version: Some("1.3".to_string()),
1849 pin_certs: Some(true),
1850 };
1851 let file = File::default();
1852 let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
1853 let merge_u64 = |cli: u64, file: Option<u64>, default: u64| {
1854 if cli == default {
1855 file.unwrap_or(default)
1856 } else {
1857 cli
1858 }
1859 };
1860 let network = NetworkConfig::from_source(&source, &file, merge_bool, merge_u64);
1861 assert_eq!(network.source, Some("192.168.1.1".to_string()));
1862 assert_eq!(network.timeout, 60);
1863 assert_eq!(network.ca_cert, Some("/path/to/cert".to_string()));
1864 assert_eq!(network.tls_version, Some("1.3".to_string()));
1865 assert!(network.pin_certs);
1866 }
1867
1868 #[test]
1869 fn test_network_config_from_source_timeout_file_fallback() {
1870 let source = NetworkSource::default(); let file = File {
1873 timeout: Some(30),
1874 ..Default::default()
1875 };
1876 let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
1877 let merge_u64 = |cli: u64, file: Option<u64>, default: u64| {
1878 if cli == default {
1879 file.unwrap_or(default)
1880 } else {
1881 cli
1882 }
1883 };
1884 let network = NetworkConfig::from_source(&source, &file, merge_bool, merge_u64);
1885 assert_eq!(network.timeout, 30);
1886 }
1887
1888 #[test]
1889 fn test_network_config_from_source_ca_cert_file_fallback() {
1890 let source = NetworkSource::default();
1892 let file = File {
1893 ca_cert: Some("/file/cert.pem".to_string()),
1894 ..Default::default()
1895 };
1896 let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
1897 let merge_u64 = |cli: u64, file: Option<u64>, default: u64| {
1898 if cli == default {
1899 file.unwrap_or(default)
1900 } else {
1901 cli
1902 }
1903 };
1904 let network = NetworkConfig::from_source(&source, &file, merge_bool, merge_u64);
1905 assert_eq!(network.ca_cert, Some("/file/cert.pem".to_string()));
1906 }
1907
1908 #[test]
1909 fn test_network_config_from_source_pin_certs_file_fallback() {
1910 let source = NetworkSource::default();
1912 let file = File {
1913 pin_certs: Some(true),
1914 ..Default::default()
1915 };
1916 let merge_bool = |cli: Option<bool>, file: Option<bool>| cli.or(file).unwrap_or(false);
1917 let merge_u64 = |cli: u64, file: Option<u64>, default: u64| {
1918 if cli == default {
1919 file.unwrap_or(default)
1920 } else {
1921 cli
1922 }
1923 };
1924 let network = NetworkConfig::from_source(&source, &file, merge_bool, merge_u64);
1925 assert!(network.pin_certs);
1926 }
1927
1928 #[test]
1931 fn test_validation_result_multiple_warnings() {
1932 let result = ValidationResult::ok()
1933 .with_warning("warning 1")
1934 .with_warning("warning 2")
1935 .with_warning("warning 3");
1936 assert!(result.valid);
1937 assert_eq!(result.warnings.len(), 3);
1938 assert!(result.errors.is_empty());
1939 }
1940
1941 #[test]
1942 fn test_validation_result_multiple_errors() {
1943 let result = ValidationResult::error("error 1").with_error("error 2");
1944 assert!(!result.valid);
1945 assert_eq!(result.errors.len(), 2);
1946 }
1947
1948 #[test]
1949 fn test_validation_result_with_warning_then_error() {
1950 let result = ValidationResult::ok()
1951 .with_warning("just a warning")
1952 .with_error("actual error");
1953 assert!(!result.valid);
1954 assert!(!result.warnings.is_empty());
1955 assert!(!result.errors.is_empty());
1956 }
1957
1958 #[test]
1959 fn test_validation_result_merge_valid_results() {
1960 let a = ValidationResult::ok().with_warning("warn-a");
1961 let b = ValidationResult::ok().with_warning("warn-b");
1962 let merged = a.merge(b);
1963 assert!(merged.valid);
1964 assert_eq!(merged.warnings.len(), 2);
1965 assert!(merged.errors.is_empty());
1966 }
1967
1968 #[test]
1969 fn test_validation_result_merge_with_invalid() {
1970 let a = ValidationResult::ok();
1971 let b = ValidationResult::error("bad");
1972 let merged = a.merge(b);
1973 assert!(!merged.valid);
1974 assert!(merged.errors.contains(&"bad".to_string()));
1975 }
1976
1977 #[test]
1978 fn test_validation_result_merge_accumulates_all() {
1979 let a = ValidationResult::error("err-a").with_warning("warn-a");
1980 let b = ValidationResult::error("err-b").with_warning("warn-b");
1981 let merged = a.merge(b);
1982 assert!(!merged.valid);
1983 assert_eq!(merged.errors.len(), 2);
1984 assert_eq!(merged.warnings.len(), 2);
1985 }
1986
1987 #[test]
1988 fn test_validation_result_merge_empty() {
1989 let a = ValidationResult::ok();
1990 let merged = a.merge(ValidationResult::ok());
1991 assert!(merged.valid);
1992 assert!(merged.errors.is_empty());
1993 assert!(merged.warnings.is_empty());
1994 }
1995
1996 #[test]
1997 fn test_validation_result_valid_then_invalid() {
1998 let a = ValidationResult::error("original error");
2000 let b = ValidationResult::ok();
2001 let merged = a.merge(b);
2002 assert!(!merged.valid);
2003 assert_eq!(merged.errors.len(), 1);
2004 }
2005
2006 #[test]
2009 fn test_validate_config_all_valid() {
2010 let file_config = File {
2011 profile: Some("power-user".to_string()),
2012 theme: Some("dark".to_string()),
2013 csv_delimiter: Some(','),
2014 ..Default::default()
2015 };
2016 let result = validate_config(&file_config);
2017 assert!(result.valid);
2018 assert!(result.errors.is_empty());
2019 assert!(result.warnings.is_empty());
2020 }
2021
2022 #[test]
2023 fn test_validate_config_multiple_warnings() {
2024 let file_config = File {
2025 simple: Some(true),
2026 csv: Some(true),
2027 json: Some(true),
2028 ..Default::default()
2029 };
2030 let result = validate_config(&file_config);
2031 assert!(result.valid); assert!(result.warnings.len() >= 3); }
2034
2035 #[test]
2036 fn test_validate_config_invalid_timeout_zero() {
2037 let file_config = File {
2038 timeout: Some(0),
2039 ..Default::default()
2040 };
2041 let result = validate_config(&file_config);
2042 assert!(result.valid); }
2044
2045 #[test]
2046 fn test_validate_config_invalid_timeout_too_large() {
2047 let file_config = File {
2048 timeout: Some(500),
2049 ..Default::default()
2050 };
2051 let result = validate_config(&file_config);
2052 assert!(result.valid); }
2054
2055 #[test]
2058 fn test_load_config_file_invalid_toml() {
2059 let temp_dir = tempfile::TempDir::new().unwrap();
2062 let config_path = temp_dir.path().join("config.toml");
2063 std::fs::write(&config_path, "invalid toml { =").unwrap();
2064
2065 let content = std::fs::read_to_string(&config_path).unwrap();
2067 let result: Result<File, _> = toml::from_str(&content);
2068 assert!(result.is_err());
2069 }
2070
2071 #[test]
2072 fn test_load_config_file_timeout_zero() {
2073 let temp_dir = tempfile::TempDir::new().unwrap();
2075 let config_path = temp_dir.path().join("config.toml");
2076 std::fs::write(&config_path, "timeout = 0").unwrap();
2077
2078 let content = std::fs::read_to_string(&config_path).unwrap();
2079 let mut config: File = toml::from_str(&content).unwrap();
2080 assert_eq!(config.timeout, Some(0));
2081
2082 if let Some(timeout) = config.timeout {
2084 if timeout == 0 || timeout > 300 {
2085 config.timeout = None;
2086 }
2087 }
2088 assert_eq!(config.timeout, None);
2089 }
2090
2091 #[test]
2092 fn test_load_config_file_timeout_too_large() {
2093 let temp_dir = tempfile::TempDir::new().unwrap();
2095 let config_path = temp_dir.path().join("config.toml");
2096 std::fs::write(&config_path, "timeout = 500").unwrap();
2097
2098 let content = std::fs::read_to_string(&config_path).unwrap();
2099 let mut config: File = toml::from_str(&content).unwrap();
2100 assert_eq!(config.timeout, Some(500));
2101
2102 if let Some(timeout) = config.timeout {
2104 if timeout == 0 || timeout > 300 {
2105 config.timeout = None;
2106 }
2107 }
2108 assert_eq!(config.timeout, None);
2109 }
2110
2111 #[test]
2112 fn test_load_config_file_valid_timeout() {
2113 let temp_dir = tempfile::TempDir::new().unwrap();
2115 let config_path = temp_dir.path().join("config.toml");
2116 std::fs::write(&config_path, "timeout = 60").unwrap();
2117
2118 let content = std::fs::read_to_string(&config_path).unwrap();
2119 let mut config: File = toml::from_str(&content).unwrap();
2120 assert_eq!(config.timeout, Some(60));
2121
2122 if let Some(timeout) = config.timeout {
2124 if timeout == 0 || timeout > 300 {
2125 config.timeout = None;
2126 }
2127 }
2128 assert_eq!(config.timeout, Some(60));
2129 }
2130
2131 #[test]
2134 fn test_get_config_path_internal_returns_path() {
2135 let path = get_config_path_internal();
2136 if let Some(p) = path {
2138 assert!(p.ends_with("config.toml"));
2139 }
2140 }
2142
2143 #[test]
2146 fn test_config_from_args_with_file_valid_profile() {
2147 let args = Args::parse_from(["netspeed-cli", "--profile", "gamer"]);
2148 let source = ConfigSource::from_args(&args);
2149 let (config, validation) = Config::from_args_with_file(&source, None);
2150 assert!(validation.valid); assert!(config.profile().is_some());
2152 }
2153
2154 #[test]
2155 fn test_config_from_args_with_file_invalid_profile_warning() {
2156 let args = Args::parse_from(["netspeed-cli", "--profile", "bad-profile"]);
2157 let source = ConfigSource::from_args(&args);
2158 let (_config, validation) = Config::from_args_with_file(&source, None);
2159 assert!(validation.valid);
2161 assert!(!validation.warnings.is_empty());
2162 assert!(validation.warnings[0].contains("bad-profile"));
2163 }
2164
2165 #[test]
2166 fn test_config_from_args_with_file_preserves_config() {
2167 let args = Args::parse_from(["netspeed-cli", "--timeout", "45"]);
2168 let source = ConfigSource::from_args(&args);
2169 let (config, validation) = Config::from_args_with_file(&source, None);
2170 assert!(validation.valid);
2171 assert_eq!(config.timeout(), 45);
2172 }
2173
2174 #[test]
2175 fn test_config_from_args_with_file_all_formats() {
2176 for format_str in &[
2177 "json",
2178 "jsonl",
2179 "csv",
2180 "minimal",
2181 "simple",
2182 "compact",
2183 "detailed",
2184 "dashboard",
2185 ] {
2186 let args = Args::parse_from(["netspeed-cli", "--format", format_str]);
2187 let source = ConfigSource::from_args(&args);
2188 let (config, _) = Config::from_args_with_file(&source, None);
2189 assert!(
2190 config.format().is_some(),
2191 "Format {} should be set",
2192 format_str
2193 );
2194 }
2195 }
2196
2197 #[test]
2200 fn test_format_debug() {
2201 let fmt = Format::Json;
2202 let debug_str = format!("{fmt:?}");
2203 assert!(debug_str.contains("Json"));
2204 }
2205
2206 #[test]
2207 fn test_format_clone() {
2208 let fmt = Format::Detailed;
2209 let cloned = fmt;
2210 assert_eq!(fmt, cloned);
2211 }
2212
2213 #[test]
2214 fn test_format_copy() {
2215 let fmt = Format::Dashboard;
2216 let copied = fmt; assert_eq!(fmt, copied);
2218 }
2219
2220 #[test]
2223 fn test_config_debug() {
2224 let config = Config::default();
2225 let debug_str = format!("{config:?}");
2226 assert!(debug_str.contains("Config"));
2227 }
2228
2229 #[test]
2230 fn test_config_source_debug() {
2231 let source = ConfigSource::default();
2232 let debug_str = format!("{source:?}");
2233 assert!(debug_str.contains("ConfigSource"));
2234 }
2235
2236 #[test]
2237 fn test_validation_result_debug() {
2238 let result = ValidationResult::ok();
2239 let debug_str = format!("{result:?}");
2240 assert!(debug_str.contains("ValidationResult"));
2241 }
2242
2243 #[test]
2244 fn test_file_config_debug() {
2245 let file = File::default();
2246 let debug_str = format!("{file:?}");
2247 assert!(debug_str.contains("File"));
2248 }
2249
2250 #[test]
2251 fn test_file_config_clone() {
2252 let file = File {
2253 timeout: Some(45),
2254 profile: Some("test".to_string()),
2255 ..Default::default()
2256 };
2257 let cloned = file.clone();
2258 assert_eq!(file.timeout, cloned.timeout);
2259 assert_eq!(file.profile, cloned.profile);
2260 }
2261
2262 #[test]
2263 fn test_config_source_clone() {
2264 let mut source = ConfigSource::default();
2265 source.output.profile = Some("clone-test".to_string());
2266 let cloned = source.clone();
2267 assert_eq!(source.output.profile, cloned.output.profile);
2268 }
2269
2270 #[test]
2273 fn test_file_config_all_fields() {
2274 let toml_content = r#"
2275 no_download = true
2276 no_upload = false
2277 single = true
2278 bytes = true
2279 simple = false
2280 csv = false
2281 csv_delimiter = '|'
2282 csv_header = true
2283 json = false
2284 timeout = 120
2285 profile = "gamer"
2286 theme = "light"
2287 custom_user_agent = "TestAgent/1.0"
2288 strict = true
2289 ca_cert = "/path/to/cert.pem"
2290 tls_version = "1.3"
2291 pin_certs = true
2292 "#;
2293 let config: File = toml::from_str(toml_content).unwrap();
2294 assert_eq!(config.no_download, Some(true));
2295 assert_eq!(config.timeout, Some(120));
2296 assert_eq!(config.profile, Some("gamer".to_string()));
2297 assert_eq!(config.theme, Some("light".to_string()));
2298 assert_eq!(config.custom_user_agent, Some("TestAgent/1.0".to_string()));
2299 assert_eq!(config.strict, Some(true));
2300 assert_eq!(config.ca_cert, Some("/path/to/cert.pem".to_string()));
2301 assert_eq!(config.tls_version, Some("1.3".to_string()));
2302 assert_eq!(config.pin_certs, Some(true));
2303 }
2304
2305 #[test]
2306 fn test_file_config_empty_toml() {
2307 let toml_content = "";
2308 let config: File = toml::from_str(toml_content).unwrap();
2309 assert!(config.no_download.is_none());
2310 assert!(config.timeout.is_none());
2311 assert!(config.profile.is_none());
2312 }
2313
2314 #[test]
2315 fn test_file_config_whitespace_only() {
2316 let toml_content = " ";
2317 let config: File = toml::from_str(toml_content).unwrap();
2318 assert!(config.no_download.is_none());
2319 }
2320
2321 #[test]
2324 fn test_ca_cert_path_some() {
2325 let config = Config {
2326 network: NetworkConfig {
2327 ca_cert: Some("/path/to/cert".to_string()),
2328 ..Default::default()
2329 },
2330 ..Default::default()
2331 };
2332 let path = config.ca_cert_path();
2333 assert!(path.is_some());
2334 assert_eq!(path.unwrap(), std::path::PathBuf::from("/path/to/cert"));
2335 }
2336
2337 #[test]
2338 fn test_ca_cert_path_none() {
2339 let config = Config::default();
2340 let path = config.ca_cert_path();
2341 assert!(path.is_none());
2342 }
2343
2344 #[test]
2347 fn test_output_source_debug() {
2348 let src = OutputSource::default();
2349 let debug_str = format!("{src:?}");
2350 assert!(debug_str.contains("OutputSource"));
2351 }
2352
2353 #[test]
2354 fn test_test_source_debug() {
2355 let src = TestSource::default();
2356 let debug_str = format!("{src:?}");
2357 assert!(debug_str.contains("TestSource"));
2358 }
2359
2360 #[test]
2361 fn test_network_source_debug() {
2362 let src = NetworkSource::default();
2363 let debug_str = format!("{src:?}");
2364 assert!(debug_str.contains("NetworkSource"));
2365 }
2366
2367 #[test]
2368 fn test_server_source_debug() {
2369 let src = ServerSource::default();
2370 let debug_str = format!("{src:?}");
2371 assert!(debug_str.contains("ServerSource"));
2372 }
2373
2374 #[test]
2375 fn test_config_source_default() {
2376 let source = ConfigSource::default();
2377 assert_eq!(source.output.csv_delimiter, ',');
2379 assert_eq!(source.output.theme, "dark");
2380 assert_eq!(source.network.timeout, 10);
2381 assert!(source.test.no_download.is_none());
2382 assert!(source.servers.server_ids.is_empty());
2383 }
2384
2385 #[test]
2386 fn test_config_default() {
2387 let config = Config::default();
2388 assert!(!config.output.bytes);
2390 assert!(!config.test.no_download);
2391 assert_eq!(config.network.timeout, 10);
2392 assert!(config.servers.server_ids.is_empty());
2393 assert!(!config.strict);
2394 }
2395
2396 #[test]
2399 #[allow(deprecated)]
2400 fn test_deprecated_simple_flag() {
2401 let args = Args::parse_from(["netspeed-cli", "--simple"]);
2402 assert_eq!(args.simple, Some(true));
2403 let config = Config::from_args(&args);
2404 assert!(config.simple());
2405 }
2406
2407 #[test]
2408 #[allow(deprecated)]
2409 fn test_deprecated_json_flag() {
2410 let args = Args::parse_from(["netspeed-cli", "--json"]);
2411 assert_eq!(args.json, Some(true));
2412 let config = Config::from_args(&args);
2413 assert!(config.json());
2414 }
2415
2416 #[test]
2417 #[allow(deprecated)]
2418 fn test_deprecated_csv_flag() {
2419 let args = Args::parse_from(["netspeed-cli", "--csv"]);
2420 assert_eq!(args.csv, Some(true));
2421 let config = Config::from_args(&args);
2422 assert!(config.csv());
2423 }
2424
2425 #[test]
2428 fn test_server_selection_clone_preserves_data() {
2429 let selection = ServerSelection {
2430 server_ids: vec!["a".to_string(), "b".to_string()],
2431 exclude_ids: vec!["c".to_string()],
2432 };
2433 let cloned = selection.clone();
2434 assert_eq!(selection.server_ids, cloned.server_ids);
2435 assert_eq!(selection.exclude_ids, cloned.exclude_ids);
2436 }
2437
2438 #[test]
2439 fn test_config_server_ids_empty() {
2440 let args = Args::parse_from(["netspeed-cli"]);
2441 let config = Config::from_args(&args);
2442 assert!(config.server_ids().is_empty());
2443 }
2444
2445 #[test]
2446 fn test_config_exclude_ids_empty() {
2447 let args = Args::parse_from(["netspeed-cli"]);
2448 let config = Config::from_args(&args);
2449 assert!(config.exclude_ids().is_empty());
2450 }
2451
2452 #[test]
2455 fn test_config_from_args_defaults() {
2456 let args = Args::parse_from(["netspeed-cli"]);
2457 let config = Config::from_args(&args);
2458
2459 assert!(!config.test.no_download);
2460 assert!(!config.test.no_upload);
2461 assert!(!config.test.single);
2462 assert!(!config.output.bytes);
2463 assert!(!config.output.simple);
2464 assert!(!config.output.csv);
2465 assert!(!config.output.json);
2466 assert!(!config.output.list);
2467 assert!(!config.output.quiet);
2468 assert_eq!(config.network.timeout, 10);
2469 assert_eq!(config.output.csv_delimiter, ',');
2470 assert!(!config.output.csv_header);
2471 assert!(config.servers.server_ids.is_empty());
2472 assert!(config.servers.exclude_ids.is_empty());
2473 }
2474
2475 #[test]
2476 fn test_config_from_args_no_download() {
2477 let args = Args::parse_from(["netspeed-cli", "--no-download"]);
2478 let config = Config::from_args(&args);
2479 assert!(config.test.no_download);
2480 assert!(!config.test.no_upload);
2481 }
2482
2483 #[test]
2484 fn test_config_file_deserialization() {
2485 let toml_content = r"
2486 no_download = true
2487 no_upload = false
2488 single = true
2489 bytes = true
2490 simple = false
2491 csv = false
2492 csv_delimiter = ';'
2493 csv_header = true
2494 json = true
2495 timeout = 30
2496 ";
2497
2498 let config: File = toml::from_str(toml_content).unwrap();
2499 assert_eq!(config.no_download, Some(true));
2500 assert_eq!(config.no_upload, Some(false));
2501 assert_eq!(config.single, Some(true));
2502 assert_eq!(config.bytes, Some(true));
2503 assert_eq!(config.simple, Some(false));
2504 assert_eq!(config.csv, Some(false));
2505 assert_eq!(config.csv_delimiter, Some(';'));
2506 assert_eq!(config.csv_header, Some(true));
2507 assert_eq!(config.json, Some(true));
2508 assert_eq!(config.timeout, Some(30));
2509 }
2510
2511 #[test]
2512 fn test_config_file_partial() {
2513 let toml_content = r"
2514 no_download = true
2515 timeout = 20
2516 ";
2517
2518 let config: File = toml::from_str(toml_content).unwrap();
2519 assert_eq!(config.no_download, Some(true));
2520 assert!(config.no_upload.is_none());
2521 assert!(config.single.is_none());
2522 assert_eq!(config.timeout, Some(20));
2523 assert!(config.csv_delimiter.is_none());
2524 }
2525
2526 #[test]
2527 fn test_config_from_args_overrides_file() {
2528 let args = Args::parse_from(["netspeed-cli", "--no-download"]);
2530 let config = Config::from_args(&args);
2531 assert!(config.test.no_download);
2532 }
2533
2534 #[test]
2535 fn test_config_merge_bool_file_true_cli_false() {
2536 let toml_content = r"
2538 no_download = true
2539 ";
2540 let file_config: File = toml::from_str(toml_content).unwrap();
2541
2542 let args = Args::parse_from(["netspeed-cli"]);
2544 let file_config_loaded = Some(file_config);
2545
2546 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);
2550 assert!(merged);
2551 }
2552
2553 #[test]
2554 fn test_validate_config_valid_profile() {
2555 let file_config = File {
2556 profile: Some("gamer".to_string()),
2557 ..Default::default()
2558 };
2559 let result = validate_config(&file_config);
2560 assert!(result.valid);
2561 assert!(result.errors.is_empty());
2562 }
2563
2564 #[test]
2565 fn test_validate_config_empty_is_valid() {
2566 let file_config = File::default();
2568 let result = validate_config(&file_config);
2569 assert!(result.valid);
2570 assert!(result.errors.is_empty());
2571 assert!(result.warnings.is_empty());
2572 }
2573
2574 #[test]
2575 fn test_validate_config_invalid_profile() {
2576 let file_config = File {
2577 profile: Some("invalid_profile".to_string()),
2578 ..Default::default()
2579 };
2580 let result = validate_config(&file_config);
2581 assert!(!result.valid);
2582 assert!(!result.errors.is_empty());
2583 assert!(result.errors[0].contains("invalid_profile"));
2584 }
2585
2586 #[test]
2587 fn test_validate_config_invalid_theme() {
2588 let file_config = File {
2589 theme: Some("neon".to_string()),
2590 ..Default::default()
2591 };
2592 let result = validate_config(&file_config);
2593 assert!(!result.valid);
2594 assert!(!result.errors.is_empty());
2595 assert!(result.errors[0].contains("neon"));
2596 }
2597
2598 #[test]
2599 fn test_validate_config_invalid_csv_delimiter() {
2600 let file_config = File {
2601 csv_delimiter: Some('X'),
2602 ..Default::default()
2603 };
2604 let result = validate_config(&file_config);
2605 assert!(!result.valid);
2606 assert!(!result.errors.is_empty());
2607 }
2608
2609 #[test]
2610 fn test_validate_config_deprecated_simple() {
2611 let file_config = File {
2612 simple: Some(true),
2613 ..Default::default()
2614 };
2615 let result = validate_config(&file_config);
2616 assert!(result.valid);
2617 assert!(!result.warnings.is_empty());
2618 assert!(
2619 result
2620 .warnings
2621 .iter()
2622 .any(|w| w.contains("simple") && w.contains("deprecated"))
2623 );
2624 }
2625
2626 #[test]
2627 fn test_validate_config_multiple_issues() {
2628 let file_config = File {
2629 profile: Some("bad".to_string()),
2630 theme: Some("ugly".to_string()),
2631 csv_delimiter: Some('@'),
2632 ..Default::default()
2633 };
2634 let result = validate_config(&file_config);
2635 assert!(!result.valid);
2636 assert!(result.errors.len() >= 3); }
2638
2639 #[test]
2642 fn test_tls_config_defaults() {
2643 let args = Args::parse_from(["netspeed-cli"]);
2645 let config = Config::from_args(&args);
2646 assert!(config.network.ca_cert.is_none());
2647 assert!(config.network.tls_version.is_none());
2648 assert!(!config.network.pin_certs);
2649 }
2650
2651 #[test]
2652 fn test_tls_config_file_deserialization() {
2653 let toml_content = r#"
2655 ca_cert = "/custom/ca.pem"
2656 tls_version = "1.2"
2657 pin_certs = true
2658 "#;
2659
2660 let file_config: File = toml::from_str(toml_content).unwrap();
2661 assert_eq!(file_config.ca_cert, Some("/custom/ca.pem".to_string()));
2662 assert_eq!(file_config.tls_version, Some("1.2".to_string()));
2663 assert_eq!(file_config.pin_certs, Some(true));
2664 }
2665
2666 #[test]
2667 fn test_tls_config_file_partial() {
2668 let toml_content = r#"
2670 ca_cert = "/my/ca.pem"
2671 "#;
2672
2673 let file_config: File = toml::from_str(toml_content).unwrap();
2674 assert_eq!(file_config.ca_cert, Some("/my/ca.pem".to_string()));
2675 assert!(file_config.tls_version.is_none());
2676 assert!(file_config.pin_certs.is_none());
2677 }
2678
2679 #[test]
2680 fn test_tls_config_cli_ca_cert() {
2681 let temp_file = tempfile::NamedTempFile::new().unwrap();
2684 std::fs::write(temp_file.path(), "fake cert content").unwrap();
2685 let args = Args::parse_from([
2686 "netspeed-cli",
2687 "--ca-cert",
2688 temp_file.path().to_str().unwrap(),
2689 ]);
2690 assert_eq!(
2691 args.ca_cert,
2692 Some(temp_file.path().to_string_lossy().to_string())
2693 );
2694 }
2696
2697 #[test]
2698 fn test_tls_config_cli_tls_version() {
2699 let args = Args::parse_from(["netspeed-cli", "--tls-version", "1.3"]);
2701 assert_eq!(args.tls_version, Some("1.3".to_string()));
2702 }
2703
2704 #[test]
2705 fn test_tls_config_cli_pin_certs() {
2706 let args = Args::parse_from(["netspeed-cli", "--pin-certs"]);
2708 assert_eq!(args.pin_certs, Some(true));
2709 }
2710
2711 #[test]
2712 fn test_tls_config_cli_pin_certs_false() {
2713 let args = Args::parse_from(["netspeed-cli", "--pin-certs=false"]);
2715 assert_eq!(args.pin_certs, Some(false));
2716 }
2717
2718 #[test]
2719 fn test_tls_config_all_cli_options() {
2720 let temp_file = tempfile::NamedTempFile::new().unwrap();
2723 std::fs::write(temp_file.path(), "fake cert content").unwrap();
2724 let args = Args::parse_from([
2725 "netspeed-cli",
2726 "--ca-cert",
2727 temp_file.path().to_str().unwrap(),
2728 "--tls-version",
2729 "1.2",
2730 "--pin-certs",
2731 ]);
2732
2733 assert_eq!(
2734 args.ca_cert,
2735 Some(temp_file.path().to_string_lossy().to_string())
2736 );
2737 assert_eq!(args.tls_version, Some("1.2".to_string()));
2738 assert_eq!(args.pin_certs, Some(true));
2739 }
2741
2742 #[test]
2743 fn test_tls_config_string_merge_cli_takes_precedence() {
2744 let cli_val = Some("/cli/ca.pem".to_string());
2750 let file_val = Some("/file/ca.pem".to_string());
2751 let merged = cli_val.or(file_val.clone());
2752 assert_eq!(merged, Some("/cli/ca.pem".to_string()));
2753
2754 let cli_val_none: Option<String> = None;
2756 let merged = cli_val_none.or(file_val.clone());
2757 assert_eq!(merged, Some("/file/ca.pem".to_string()));
2758
2759 let merged = Option::<String>::None.or(None);
2761 assert!(merged.is_none());
2762 }
2763
2764 #[test]
2765 fn test_tls_config_bool_merge() {
2766 assert!(merge_bool_test(Some(true), Some(false)));
2772
2773 assert!(!merge_bool_test(Some(false), Some(true)));
2775
2776 assert!(merge_bool_test(Some(true), None));
2778
2779 assert!(!merge_bool_test(Some(false), None));
2781
2782 assert!(merge_bool_test(None, Some(true)));
2784
2785 assert!(!merge_bool_test(None, Some(false)));
2787
2788 assert!(!merge_bool_test(None::<bool>, None));
2790 }
2791
2792 fn merge_bool_test(cli: Option<bool>, file: Option<bool>) -> bool {
2794 cli.or(file).unwrap_or(false)
2795 }
2796
2797 #[test]
2800 fn test_format_from_cli_type_all_variants() {
2801 use crate::cli::OutputFormatType;
2802 assert_eq!(Format::from_cli_type(OutputFormatType::Json), Format::Json);
2803 assert_eq!(
2804 Format::from_cli_type(OutputFormatType::Jsonl),
2805 Format::Jsonl
2806 );
2807 assert_eq!(Format::from_cli_type(OutputFormatType::Csv), Format::Csv);
2808 assert_eq!(
2809 Format::from_cli_type(OutputFormatType::Minimal),
2810 Format::Minimal
2811 );
2812 assert_eq!(
2813 Format::from_cli_type(OutputFormatType::Simple),
2814 Format::Simple
2815 );
2816 assert_eq!(
2817 Format::from_cli_type(OutputFormatType::Compact),
2818 Format::Compact
2819 );
2820 assert_eq!(
2821 Format::from_cli_type(OutputFormatType::Detailed),
2822 Format::Detailed
2823 );
2824 assert_eq!(
2825 Format::from_cli_type(OutputFormatType::Dashboard),
2826 Format::Dashboard
2827 );
2828 }
2829
2830 #[test]
2831 fn test_format_is_machine_readable() {
2832 assert!(Format::Json.is_machine_readable());
2833 assert!(Format::Jsonl.is_machine_readable());
2834 assert!(Format::Csv.is_machine_readable());
2835 assert!(!Format::Minimal.is_machine_readable());
2836 assert!(!Format::Simple.is_machine_readable());
2837 assert!(!Format::Compact.is_machine_readable());
2838 assert!(!Format::Detailed.is_machine_readable());
2839 assert!(!Format::Dashboard.is_machine_readable());
2840 }
2841
2842 #[test]
2843 fn test_format_is_non_verbose() {
2844 assert!(Format::Simple.is_non_verbose());
2846 assert!(Format::Minimal.is_non_verbose());
2847 assert!(Format::Compact.is_non_verbose());
2848 assert!(Format::Json.is_non_verbose());
2849 assert!(Format::Jsonl.is_non_verbose());
2850 assert!(Format::Csv.is_non_verbose());
2851 assert!(Format::Dashboard.is_non_verbose());
2852 assert!(!Format::Detailed.is_non_verbose());
2854 }
2855
2856 #[test]
2857 fn test_format_label() {
2858 assert_eq!(Format::Json.label(), "JSON");
2859 assert_eq!(Format::Jsonl.label(), "JSONL");
2860 assert_eq!(Format::Csv.label(), "CSV");
2861 assert_eq!(Format::Minimal.label(), "Minimal");
2862 assert_eq!(Format::Simple.label(), "Simple");
2863 assert_eq!(Format::Compact.label(), "Compact");
2864 assert_eq!(Format::Detailed.label(), "Detailed");
2865 assert_eq!(Format::Dashboard.label(), "Dashboard");
2866 }
2867
2868 #[test]
2869 fn test_format_display() {
2870 assert_eq!(format!("{}", Format::Json), "JSON");
2871 assert_eq!(format!("{}", Format::Detailed), "Detailed");
2872 }
2873
2874 #[test]
2875 fn test_format_equality() {
2876 assert_eq!(Format::Json, Format::Json);
2877 assert_ne!(Format::Json, Format::Csv);
2878 }
2879
2880 #[test]
2883 fn test_config_source_from_args_defaults() {
2884 let args = Args::parse_from(["netspeed-cli"]);
2885 let source = ConfigSource::from_args(&args);
2886
2887 assert!(source.output.bytes.is_none());
2889 assert!(source.output.simple.is_none());
2890 assert!(source.output.csv.is_none());
2891 assert_eq!(source.output.csv_delimiter, ',');
2892 assert!(source.output.csv_header.is_none());
2893 assert!(source.output.json.is_none());
2894 assert!(!source.output.list);
2895 assert!(source.output.quiet.is_none());
2896 assert!(source.output.minimal.is_none());
2897 assert!(source.output.profile.is_none());
2898 assert_eq!(source.output.theme, "dark");
2899 assert!(source.output.format.is_none());
2900
2901 assert!(source.test.no_download.is_none());
2903 assert!(source.test.no_upload.is_none());
2904 assert!(source.test.single.is_none());
2905
2906 assert!(source.network.source.is_none());
2908 assert_eq!(source.network.timeout, 10);
2909 assert!(source.network.ca_cert.is_none());
2910 assert!(source.network.tls_version.is_none());
2911 assert!(source.network.pin_certs.is_none());
2912
2913 assert!(source.servers.server_ids.is_empty());
2915 assert!(source.servers.exclude_ids.is_empty());
2916
2917 assert!(source.strict_config.is_none());
2919 }
2920
2921 #[test]
2922 fn test_config_source_from_args_all_set() {
2923 let args = Args::parse_from([
2924 "netspeed-cli",
2925 "--bytes",
2926 "--no-download",
2927 "--no-upload",
2928 "--single",
2929 "--timeout",
2930 "30",
2931 "--source",
2932 "0.0.0.0",
2933 "--server",
2934 "1234",
2935 "--exclude",
2936 "5678",
2937 "--profile",
2938 "gamer",
2939 "--theme",
2940 "light",
2941 "--format",
2942 "json",
2943 ]);
2944 let source = ConfigSource::from_args(&args);
2945
2946 assert_eq!(source.output.bytes, Some(true));
2947 assert_eq!(source.test.no_download, Some(true));
2948 assert_eq!(source.test.no_upload, Some(true));
2949 assert_eq!(source.test.single, Some(true));
2950 assert_eq!(source.network.timeout, 30);
2951 assert_eq!(source.network.source, Some("0.0.0.0".to_string()));
2952 assert_eq!(source.servers.server_ids, vec!["1234".to_string()]);
2953 assert_eq!(source.servers.exclude_ids, vec!["5678".to_string()]);
2954 assert_eq!(source.output.profile, Some("gamer".to_string()));
2955 assert_eq!(source.output.theme, "light");
2956 assert_eq!(source.output.format, Some(Format::Json));
2957 }
2958
2959 #[test]
2960 fn test_config_source_format_conversion() {
2961 let args = Args::parse_from(["netspeed-cli", "--format", "csv"]);
2963 let source = ConfigSource::from_args(&args);
2964 assert_eq!(source.output.format, Some(Format::Csv));
2965
2966 let args = Args::parse_from(["netspeed-cli", "--format", "dashboard"]);
2967 let source = ConfigSource::from_args(&args);
2968 assert_eq!(source.output.format, Some(Format::Dashboard));
2969 }
2970
2971 #[test]
2972 fn test_config_source_preserves_option_bools() {
2973 let args = Args::parse_from(["netspeed-cli", "--no-download=false"]);
2975 let source = ConfigSource::from_args(&args);
2976 assert_eq!(source.test.no_download, Some(false));
2977
2978 let args = Args::parse_from(["netspeed-cli"]);
2980 let source = ConfigSource::from_args(&args);
2981 assert!(source.test.no_download.is_none());
2982 }
2983
2984 #[test]
2985 fn test_config_source_default_composes_sub_sources() {
2986 let source = ConfigSource::default();
2987
2988 assert_eq!(
2990 source.output.csv_delimiter,
2991 OutputSource::default().csv_delimiter
2992 );
2993 assert_eq!(source.output.theme, OutputSource::default().theme);
2994 assert_eq!(source.network.timeout, NetworkSource::default().timeout);
2995 assert!(source.test.no_download.is_none()); assert!(source.servers.server_ids.is_empty()); assert!(source.output.bytes.is_none());
3000 assert!(source.network.source.is_none());
3001 assert!(source.strict_config.is_none());
3002 }
3003
3004 #[test]
3007 fn test_output_source_default() {
3008 let src = OutputSource::default();
3009 assert!(src.bytes.is_none());
3010 assert!(src.simple.is_none());
3011 assert!(src.csv.is_none());
3012 assert_eq!(src.csv_delimiter, ',');
3013 assert!(src.csv_header.is_none());
3014 assert!(src.json.is_none());
3015 assert!(!src.list);
3016 assert!(src.quiet.is_none());
3017 assert!(src.minimal.is_none());
3018 assert!(src.profile.is_none());
3019 assert_eq!(src.theme, "dark");
3020 assert!(src.format.is_none());
3021 }
3022
3023 #[test]
3024 fn test_output_source_custom() {
3025 let src = OutputSource {
3026 bytes: Some(true),
3027 csv_delimiter: ';',
3028 list: true,
3029 profile: Some("gamer".to_string()),
3030 theme: "light".to_string(),
3031 format: Some(Format::Json),
3032 ..Default::default()
3033 };
3034 assert_eq!(src.bytes, Some(true));
3035 assert_eq!(src.csv_delimiter, ';');
3036 assert!(src.list);
3037 assert_eq!(src.profile, Some("gamer".to_string()));
3038 assert_eq!(src.theme, "light");
3039 assert_eq!(src.format, Some(Format::Json));
3040 assert!(src.simple.is_none());
3042 assert!(src.csv.is_none());
3043 assert!(src.json.is_none());
3044 }
3045
3046 #[test]
3047 fn test_output_source_clone() {
3048 let src = OutputSource {
3049 profile: Some("streamer".to_string()),
3050 ..Default::default()
3051 };
3052 let cloned = src.clone();
3053 assert_eq!(src.profile, cloned.profile);
3054 assert_eq!(src.csv_delimiter, cloned.csv_delimiter);
3055 assert_eq!(src.theme, cloned.theme);
3056 }
3057
3058 #[test]
3061 fn test_test_source_default() {
3062 let src = TestSource::default();
3063 assert!(src.no_download.is_none());
3064 assert!(src.no_upload.is_none());
3065 assert!(src.single.is_none());
3066 }
3067
3068 #[test]
3069 fn test_test_source_custom() {
3070 let src = TestSource {
3071 no_download: Some(true),
3072 no_upload: Some(false),
3073 single: Some(true),
3074 };
3075 assert_eq!(src.no_download, Some(true));
3076 assert_eq!(src.no_upload, Some(false));
3077 assert_eq!(src.single, Some(true));
3078 }
3079
3080 #[test]
3081 fn test_test_source_clone() {
3082 let src = TestSource {
3083 no_download: Some(true),
3084 ..Default::default()
3085 };
3086 let cloned = src.clone();
3087 assert_eq!(src.no_download, cloned.no_download);
3088 }
3089
3090 #[test]
3093 fn test_network_source_default() {
3094 let src = NetworkSource::default();
3095 assert!(src.source.is_none());
3096 assert_eq!(src.timeout, 10);
3097 assert!(src.ca_cert.is_none());
3098 assert!(src.tls_version.is_none());
3099 assert!(src.pin_certs.is_none());
3100 }
3101
3102 #[test]
3103 fn test_network_source_custom() {
3104 let src = NetworkSource {
3105 source: Some("0.0.0.0".to_string()),
3106 timeout: 60,
3107 ca_cert: Some("/path/to/ca.pem".to_string()),
3108 tls_version: Some("1.3".to_string()),
3109 pin_certs: Some(true),
3110 };
3111 assert_eq!(src.source, Some("0.0.0.0".to_string()));
3112 assert_eq!(src.timeout, 60);
3113 assert_eq!(src.ca_cert, Some("/path/to/ca.pem".to_string()));
3114 assert_eq!(src.tls_version, Some("1.3".to_string()));
3115 assert_eq!(src.pin_certs, Some(true));
3116 }
3117
3118 #[test]
3119 fn test_network_source_clone() {
3120 let src = NetworkSource {
3121 source: Some("192.168.1.1".to_string()),
3122 ..Default::default()
3123 };
3124 let cloned = src.clone();
3125 assert_eq!(src.source, cloned.source);
3126 assert_eq!(src.timeout, cloned.timeout);
3127 }
3128
3129 #[test]
3132 fn test_server_source_default() {
3133 let src = ServerSource::default();
3134 assert!(src.server_ids.is_empty());
3135 assert!(src.exclude_ids.is_empty());
3136 }
3137
3138 #[test]
3139 fn test_server_source_custom() {
3140 let src = ServerSource {
3141 server_ids: vec!["1234".to_string()],
3142 exclude_ids: vec!["5678".to_string()],
3143 };
3144 assert_eq!(src.server_ids, vec!["1234".to_string()]);
3145 assert_eq!(src.exclude_ids, vec!["5678".to_string()]);
3146 }
3147
3148 #[test]
3149 fn test_server_source_clone() {
3150 let src = ServerSource {
3151 server_ids: vec!["1234".to_string(), "5678".to_string()],
3152 ..Default::default()
3153 };
3154 let cloned = src.clone();
3155 assert_eq!(src.server_ids, cloned.server_ids);
3156 assert_eq!(src.exclude_ids, cloned.exclude_ids);
3157 }
3158
3159 #[test]
3162 fn test_output_config_default() {
3163 let config = OutputConfig::default();
3164 assert!(!config.bytes);
3165 assert!(!config.simple);
3166 assert!(!config.csv);
3167 assert_eq!(config.csv_delimiter, ',');
3168 assert!(!config.csv_header);
3169 assert!(!config.json);
3170 assert!(!config.list);
3171 assert!(!config.quiet);
3172 assert!(config.profile.is_none());
3173 assert_eq!(config.theme, Theme::Dark);
3174 assert!(!config.minimal);
3175 assert!(config.format.is_none());
3176 }
3177
3178 #[test]
3179 fn test_output_config_clone() {
3180 let config = OutputConfig::default();
3181 let cloned = config.clone();
3182 assert_eq!(config.bytes, cloned.bytes);
3183 assert_eq!(config.csv_delimiter, cloned.csv_delimiter);
3184 assert_eq!(config.theme, cloned.theme);
3185 }
3186
3187 #[test]
3188 fn test_output_config_debug() {
3189 let config = OutputConfig::default();
3190 let debug_str = format!("{config:?}");
3191 assert!(debug_str.contains("OutputConfig"));
3192 }
3193
3194 #[test]
3195 fn test_output_config_custom_theme() {
3196 let custom = OutputConfig {
3197 theme: Theme::Light,
3198 ..Default::default()
3199 };
3200 assert_eq!(custom.theme, Theme::Light);
3201 }
3202
3203 #[test]
3204 fn test_output_config_csv_settings() {
3205 let custom = OutputConfig {
3206 csv: true,
3207 csv_delimiter: ';',
3208 csv_header: true,
3209 ..Default::default()
3210 };
3211 assert!(custom.csv);
3212 assert_eq!(custom.csv_delimiter, ';');
3213 assert!(custom.csv_header);
3214 }
3215
3216 #[test]
3217 fn test_test_selection_defaults() {
3218 let config = TestSelection::default();
3219 assert!(!config.no_download);
3220 assert!(!config.no_upload);
3221 assert!(!config.single);
3222 }
3223
3224 #[test]
3225 fn test_test_selection_skip_tests() {
3226 let custom = TestSelection {
3227 no_download: true,
3228 no_upload: true,
3229 single: true,
3230 };
3231 assert!(custom.no_download);
3232 assert!(custom.no_upload);
3233 assert!(custom.single);
3234 }
3235
3236 #[test]
3237 fn test_output_config_profile() {
3238 let with_profile = OutputConfig {
3239 profile: Some("gamer".to_string()),
3240 ..Default::default()
3241 };
3242 assert_eq!(with_profile.profile, Some("gamer".to_string()));
3243 }
3244
3245 #[test]
3248 fn test_network_config_default() {
3249 let config = NetworkConfig::default();
3250 assert!(config.source.is_none());
3251 assert_eq!(config.timeout, 10);
3252 assert!(config.ca_cert.is_none());
3253 assert!(config.tls_version.is_none());
3254 assert!(!config.pin_certs);
3255 }
3256
3257 #[test]
3258 fn test_network_config_clone() {
3259 let config = NetworkConfig::default();
3260 let cloned = config.clone();
3261 assert_eq!(config.timeout, cloned.timeout);
3262 assert_eq!(config.pin_certs, cloned.pin_certs);
3263 }
3264
3265 #[test]
3266 fn test_network_config_debug() {
3267 let config = NetworkConfig::default();
3268 let debug_str = format!("{config:?}");
3269 assert!(debug_str.contains("NetworkConfig"));
3270 }
3271
3272 #[test]
3273 fn test_network_config_custom_timeout() {
3274 let custom = NetworkConfig {
3275 timeout: 60,
3276 ..Default::default()
3277 };
3278 assert_eq!(custom.timeout, 60);
3279 }
3280
3281 #[test]
3282 fn test_network_config_source_ip() {
3283 let with_source = NetworkConfig {
3284 source: Some("192.168.1.100".to_string()),
3285 ..Default::default()
3286 };
3287 assert_eq!(with_source.source, Some("192.168.1.100".to_string()));
3288 }
3289
3290 #[test]
3291 fn test_network_config_tls_settings() {
3292 let custom = NetworkConfig {
3293 ca_cert: Some("/path/to/ca.pem".to_string()),
3294 tls_version: Some("1.2".to_string()),
3295 pin_certs: true,
3296 ..Default::default()
3297 };
3298 assert_eq!(custom.ca_cert, Some("/path/to/ca.pem".to_string()));
3299 assert_eq!(custom.tls_version, Some("1.2".to_string()));
3300 assert!(custom.pin_certs);
3301 }
3302
3303 #[test]
3304 fn test_network_config_tls_1_3() {
3305 let custom = NetworkConfig {
3306 tls_version: Some("1.3".to_string()),
3307 pin_certs: true,
3308 ..Default::default()
3309 };
3310 assert_eq!(custom.tls_version, Some("1.3".to_string()));
3311 assert!(custom.pin_certs);
3312 }
3313
3314 #[test]
3317 fn test_server_selection_default() {
3318 let selection = ServerSelection::default();
3319 assert!(selection.server_ids.is_empty());
3320 assert!(selection.exclude_ids.is_empty());
3321 }
3322
3323 #[test]
3324 fn test_server_selection_clone() {
3325 let selection = ServerSelection::default();
3326 let cloned = selection.clone();
3327 assert!(cloned.server_ids.is_empty());
3328 assert!(cloned.exclude_ids.is_empty());
3329 }
3330
3331 #[test]
3332 fn test_server_selection_debug() {
3333 let selection = ServerSelection::default();
3334 let debug_str = format!("{selection:?}");
3335 assert!(debug_str.contains("ServerSelection"));
3336 }
3337
3338 #[test]
3339 fn test_server_selection_specific_ids() {
3340 let selection = ServerSelection {
3341 server_ids: vec!["1234".to_string(), "5678".to_string()],
3342 exclude_ids: Vec::new(),
3343 };
3344 assert_eq!(selection.server_ids.len(), 2);
3345 assert!(selection.exclude_ids.is_empty());
3346 }
3347
3348 #[test]
3349 fn test_server_selection_exclude() {
3350 let selection = ServerSelection {
3351 server_ids: Vec::new(),
3352 exclude_ids: vec!["9999".to_string()],
3353 };
3354 assert!(selection.server_ids.is_empty());
3355 assert_eq!(selection.exclude_ids.len(), 1);
3356 assert_eq!(selection.exclude_ids[0], "9999");
3357 }
3358
3359 #[test]
3360 fn test_server_selection_both() {
3361 let selection = ServerSelection {
3362 server_ids: vec!["1234".to_string()],
3363 exclude_ids: vec!["5678".to_string()],
3364 };
3365 assert_eq!(selection.server_ids.len(), 1);
3366 assert_eq!(selection.exclude_ids.len(), 1);
3367 }
3368
3369 #[test]
3370 fn test_server_selection_from_source_empty() {
3371 let args = Args::parse_from(["netspeed-cli"]);
3372 let source = ConfigSource::from_args(&args);
3373 let selection = ServerSelection::from_source(&source.servers);
3374 assert!(selection.server_ids.is_empty());
3375 assert!(selection.exclude_ids.is_empty());
3376 }
3377
3378 #[test]
3379 fn test_server_selection_from_source_with_servers() {
3380 let args = Args::parse_from(["netspeed-cli", "--server", "1234", "--server", "5678"]);
3381 let source = ConfigSource::from_args(&args);
3382 let selection = ServerSelection::from_source(&source.servers);
3383 assert_eq!(selection.server_ids, vec!["1234", "5678"]);
3384 }
3385
3386 #[test]
3387 fn test_server_selection_from_source_with_excludes() {
3388 let args = Args::parse_from(["netspeed-cli", "--exclude", "9999", "--exclude", "8888"]);
3389 let source = ConfigSource::from_args(&args);
3390 let selection = ServerSelection::from_source(&source.servers);
3391 assert_eq!(selection.exclude_ids, vec!["9999", "8888"]);
3392 }
3393
3394 #[test]
3397 fn test_config_getters_match_direct_access() {
3398 let config = Config::default();
3399
3400 assert_eq!(config.no_download(), config.test.no_download);
3402 assert_eq!(config.no_upload(), config.test.no_upload);
3403 assert_eq!(config.single(), config.test.single);
3404
3405 assert_eq!(config.bytes(), config.output.bytes);
3407 assert_eq!(config.simple(), config.output.simple);
3408 assert_eq!(config.csv(), config.output.csv);
3409 assert_eq!(config.json(), config.output.json);
3410 assert_eq!(config.quiet(), config.output.quiet);
3411 assert_eq!(config.list(), config.output.list);
3412 assert_eq!(config.minimal(), config.output.minimal);
3413 assert_eq!(config.theme(), config.output.theme);
3414 assert_eq!(config.csv_delimiter(), config.output.csv_delimiter);
3415 assert_eq!(config.csv_header(), config.output.csv_header);
3416 assert_eq!(config.profile(), config.output.profile.as_deref());
3417 assert_eq!(config.format(), config.output.format);
3418
3419 assert_eq!(config.timeout(), config.network.timeout);
3421 assert_eq!(config.source(), config.network.source.as_deref());
3422 assert_eq!(config.ca_cert(), config.network.ca_cert.as_deref());
3423 assert_eq!(config.tls_version(), config.network.tls_version.as_deref());
3424 assert_eq!(config.pin_certs(), config.network.pin_certs);
3425
3426 assert_eq!(config.server_ids(), &config.servers.server_ids[..]);
3428 assert_eq!(config.exclude_ids(), &config.servers.exclude_ids[..]);
3429
3430 assert_eq!(
3432 config.custom_user_agent(),
3433 config.custom_user_agent.as_deref()
3434 );
3435 assert_eq!(config.strict(), config.strict);
3436 }
3437
3438 #[test]
3439 fn test_config_getter_returns_for_option_fields() {
3440 let config = Config {
3442 output: OutputConfig {
3443 profile: Some("gamer".to_string()),
3444 ..Default::default()
3445 },
3446 test: TestSelection {
3447 no_download: false,
3448 no_upload: false,
3449 single: false,
3450 },
3451 network: NetworkConfig {
3452 source: Some("192.168.1.1".to_string()),
3453 ca_cert: Some("/path/to/cert".to_string()),
3454 tls_version: Some("1.3".to_string()),
3455 ..Default::default()
3456 },
3457 servers: ServerSelection {
3458 server_ids: vec!["1234".to_string()],
3459 exclude_ids: vec!["5678".to_string()],
3460 },
3461 custom_user_agent: Some("CustomAgent/1.0".to_string()),
3462 strict: true,
3463 };
3464
3465 assert_eq!(config.profile(), Some("gamer"));
3467 assert_eq!(config.source(), Some("192.168.1.1"));
3468 assert_eq!(config.ca_cert(), Some("/path/to/cert"));
3469 assert_eq!(config.tls_version(), Some("1.3"));
3470 assert_eq!(config.custom_user_agent(), Some("CustomAgent/1.0"));
3471
3472 assert_eq!(config.server_ids(), ["1234"]);
3474 assert_eq!(config.exclude_ids(), ["5678"]);
3475
3476 assert!(!config.pin_certs()); assert!(config.strict());
3479 }
3480
3481 #[test]
3482 fn test_config_getters_none_for_unset_options() {
3483 let config = Config::default();
3484
3485 assert_eq!(config.profile(), None);
3486 assert_eq!(config.source(), None);
3487 assert_eq!(config.ca_cert(), None);
3488 assert_eq!(config.tls_version(), None);
3489 assert_eq!(config.custom_user_agent(), None);
3490 assert!(config.server_ids().is_empty());
3491 assert!(config.exclude_ids().is_empty());
3492 }
3493
3494 #[test]
3497 fn test_should_save_history_default_format() {
3498 let config = Config::default();
3499 assert!(config.should_save_history());
3501 }
3502
3503 #[test]
3504 fn test_should_save_history_json_format() {
3505 let mut config = Config::default();
3506 config.output.format = Some(Format::Json);
3507 assert!(!config.should_save_history());
3508 }
3509
3510 #[test]
3511 fn test_should_save_history_jsonl_format() {
3512 let mut config = Config::default();
3513 config.output.format = Some(Format::Jsonl);
3514 assert!(!config.should_save_history());
3515 }
3516
3517 #[test]
3518 fn test_should_save_history_csv_format() {
3519 let mut config = Config::default();
3520 config.output.format = Some(Format::Csv);
3521 assert!(!config.should_save_history());
3522 }
3523
3524 #[test]
3525 fn test_should_save_history_non_machine_readable_formats() {
3526 for fmt in [
3528 Format::Minimal,
3529 Format::Simple,
3530 Format::Compact,
3531 Format::Detailed,
3532 Format::Dashboard,
3533 ] {
3534 let mut config = Config::default();
3535 config.output.format = Some(fmt);
3536 assert!(
3537 config.should_save_history(),
3538 "format {:?} should save history",
3539 fmt
3540 );
3541 }
3542 }
3543
3544 #[test]
3545 fn test_should_save_history_legacy_json_flag() {
3546 let mut config = Config::default();
3547 config.output.json = true;
3548 assert!(!config.should_save_history());
3549 }
3550
3551 #[test]
3552 fn test_should_save_history_legacy_csv_flag() {
3553 let mut config = Config::default();
3554 config.output.csv = true;
3555 assert!(!config.should_save_history());
3556 }
3557
3558 #[test]
3559 fn test_should_save_history_both_format_and_legacy() {
3560 let mut config = Config::default();
3562 config.output.format = Some(Format::Detailed); config.output.json = true; assert!(!config.should_save_history());
3565 }
3566
3567 #[test]
3568 fn test_should_save_history_verbose_detailed() {
3569 let mut config = Config::default();
3571 config.output.format = Some(Format::Detailed);
3572 assert!(config.should_save_history());
3573 }
3574
3575 #[test]
3578 fn test_validate_and_report_with_file_config() {
3579 let source = ConfigSource::default();
3580 let config = Config::from_source(&source);
3581 let file_config = File::default();
3582
3583 let result = config.validate_and_report(&source, Some(file_config));
3585 assert!(result.valid);
3586 }
3587
3588 #[test]
3589 fn test_validate_and_report_invalid_profile() {
3590 let mut source = ConfigSource::default();
3591 source.output.profile = Some("invalid_profile_xyz".to_string());
3592 let config = Config::from_source(&source);
3593 let file_config = File::default();
3594
3595 let result = config.validate_and_report(&source, Some(file_config));
3596 assert!(result.valid); assert!(!result.warnings.is_empty());
3599 assert!(result.warnings[0].contains("invalid_profile_xyz"));
3600 }
3601
3602 #[test]
3603 fn test_validate_and_report_invalid_file_config() {
3604 let source = ConfigSource::default();
3605 let config = Config::from_source(&source);
3606
3607 let file_config = File {
3609 profile: Some("bad_profile".to_string()),
3610 ..Default::default()
3611 };
3612
3613 let result = config.validate_and_report(&source, Some(file_config));
3614 assert!(!result.valid);
3615 assert!(!result.errors.is_empty());
3616 }
3617
3618 #[test]
3621 fn test_config_default_composes_sub_structs() {
3622 let config = Config::default();
3623
3624 assert!(!config.output.bytes); let _ = config.output;
3627 let _ = config.test;
3628 let _ = config.network;
3629 let _ = config.servers;
3630
3631 assert!(!config.test.no_download);
3633 assert_eq!(config.network.timeout, 10);
3634 assert!(config.servers.server_ids.is_empty());
3635 }
3636
3637 #[test]
3638 fn test_config_clone_preserves_all_fields() {
3639 let config = Config {
3640 output: OutputConfig {
3641 bytes: true,
3642 theme: Theme::Light,
3643 profile: Some("test".to_string()),
3644 ..Default::default()
3645 },
3646 test: TestSelection {
3647 no_download: true,
3648 no_upload: false,
3649 single: true,
3650 },
3651 network: NetworkConfig {
3652 timeout: 30,
3653 source: Some("127.0.0.1".to_string()),
3654 pin_certs: true,
3655 ..Default::default()
3656 },
3657 servers: ServerSelection {
3658 server_ids: vec!["abc".to_string()],
3659 exclude_ids: vec!["xyz".to_string()],
3660 },
3661 custom_user_agent: Some("TestAgent".to_string()),
3662 strict: true,
3663 };
3664
3665 let cloned = config.clone();
3666
3667 assert!(cloned.output.bytes);
3669 assert_eq!(cloned.output.theme, Theme::Light);
3670 assert_eq!(cloned.output.profile, Some("test".to_string()));
3671 assert!(cloned.test.no_download);
3672 assert!(!cloned.test.no_upload);
3673 assert!(cloned.test.single);
3674 assert_eq!(cloned.network.timeout, 30);
3675 assert_eq!(cloned.network.source, Some("127.0.0.1".to_string()));
3676 assert!(cloned.network.pin_certs);
3677 assert_eq!(cloned.servers.server_ids, vec!["abc".to_string()]);
3678 assert_eq!(cloned.servers.exclude_ids, vec!["xyz".to_string()]);
3679 assert_eq!(cloned.custom_user_agent, Some("TestAgent".to_string()));
3680 assert!(cloned.strict);
3681 }
3682}