test(config): unit tests for AppConfig serde + defaults
Add #[cfg(test)] coverage for src/config.rs: default round-trip, backward-compat default-fill (old configs missing newer #[serde(default)] fields still load), notifications_enabled default_true wiring, NetworkMode default/ALL/Display/round-trip, and unknown-field tolerance. Derives PartialEq on AppConfig to support equality asserts. Tests-only; no production behavior change. Implemented by Gemini (junior implementer), reviewed and verified by senior (cargo build + clippy --all-targets + cargo test all green). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+102
-1
@@ -40,7 +40,7 @@ fn default_true() -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct AppConfig {
|
||||
pub input_device: String,
|
||||
pub output_device: String,
|
||||
@@ -121,3 +121,104 @@ impl AppConfig {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_round_trip() {
|
||||
let original = AppConfig::default();
|
||||
let serialized = serde_json::to_string(&original).unwrap();
|
||||
let deserialized: AppConfig = serde_json::from_str(&serialized).unwrap();
|
||||
assert_eq!(original, deserialized);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_backward_compat_default_fill() {
|
||||
let minimal_json = r#"{"input_device":"","output_device":"","noise_gate_threshold":0.01}"#;
|
||||
let deserialized: AppConfig = serde_json::from_str(minimal_json).unwrap();
|
||||
|
||||
assert_eq!(deserialized.network_mode, NetworkMode::RelayNoDiscovery);
|
||||
assert!(!deserialized.echo_cancellation_enabled);
|
||||
assert!(deserialized.notifications_enabled);
|
||||
assert!(deserialized.custom_sound_self_join.is_none());
|
||||
assert!(deserialized.custom_sound_peer_join.is_none());
|
||||
assert!(deserialized.custom_sound_peer_leave.is_none());
|
||||
assert!(deserialized.custom_sound_reconnect_attempt.is_none());
|
||||
assert!(deserialized.custom_sound_reconnected.is_none());
|
||||
assert!(deserialized.custom_sound_self_leave.is_none());
|
||||
assert!(deserialized.custom_sound_mic_toggle.is_none());
|
||||
assert!(deserialized.custom_sound_reconnect_failed.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_notifications_enabled_specifically() {
|
||||
let missing_notifications = r#"{"input_device":"","output_device":"","noise_gate_threshold":0.01}"#;
|
||||
let config_missing: AppConfig = serde_json::from_str(missing_notifications).unwrap();
|
||||
assert!(config_missing.notifications_enabled);
|
||||
|
||||
let explicit_false = r#"{"input_device":"","output_device":"","noise_gate_threshold":0.01,"notifications_enabled":false}"#;
|
||||
let config_false: AppConfig = serde_json::from_str(explicit_false).unwrap();
|
||||
assert!(!config_false.notifications_enabled);
|
||||
|
||||
let explicit_true = r#"{"input_device":"","output_device":"","noise_gate_threshold":0.01,"notifications_enabled":true}"#;
|
||||
let config_true: AppConfig = serde_json::from_str(explicit_true).unwrap();
|
||||
assert!(config_true.notifications_enabled);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_network_mode() {
|
||||
// NetworkMode::default() == RelayNoDiscovery
|
||||
assert_eq!(NetworkMode::default(), NetworkMode::RelayNoDiscovery);
|
||||
|
||||
// ALL.len() == 3 and contains all three variants
|
||||
assert_eq!(NetworkMode::ALL.len(), 3);
|
||||
assert!(NetworkMode::ALL.contains(&NetworkMode::RelayNoDiscovery));
|
||||
assert!(NetworkMode::ALL.contains(&NetworkMode::N0Full));
|
||||
assert!(NetworkMode::ALL.contains(&NetworkMode::DirectOnly));
|
||||
|
||||
// each variant serde round-trips to itself
|
||||
for mode in NetworkMode::ALL {
|
||||
let serialized = serde_json::to_string(&mode).unwrap();
|
||||
let deserialized: NetworkMode = serde_json::from_str(&serialized).unwrap();
|
||||
assert_eq!(mode, deserialized);
|
||||
}
|
||||
|
||||
// the three Display strings are non-empty and distinct
|
||||
let display_0 = NetworkMode::RelayNoDiscovery.to_string();
|
||||
let display_1 = NetworkMode::N0Full.to_string();
|
||||
let display_2 = NetworkMode::DirectOnly.to_string();
|
||||
|
||||
assert!(!display_0.is_empty());
|
||||
assert!(!display_1.is_empty());
|
||||
assert!(!display_2.is_empty());
|
||||
|
||||
assert_ne!(display_0, display_1);
|
||||
assert_ne!(display_1, display_2);
|
||||
assert_ne!(display_0, display_2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unknown_field_tolerance() {
|
||||
// Unknown/extra field tolerance: a config JSON containing an extra unrecognized key should still deserialize.
|
||||
// Assert whatever actually happens and leave a comment; do not add deny_unknown_fields.
|
||||
let json_with_extra = r#"{
|
||||
"input_device": "",
|
||||
"output_device": "",
|
||||
"noise_gate_threshold": 0.01,
|
||||
"unrecognized_field_xyz_123": "some_value"
|
||||
}"#;
|
||||
let deserialized_res: Result<AppConfig, _> = serde_json::from_str(json_with_extra);
|
||||
|
||||
// Assert that deserialization succeeds even with unrecognized/unknown fields.
|
||||
// This confirms that serde does not reject unknown fields (i.e. default behavior).
|
||||
assert!(deserialized_res.is_ok(), "Config deserialization failed when an unknown field was present");
|
||||
|
||||
let config = deserialized_res.unwrap();
|
||||
assert_eq!(config.input_device, "");
|
||||
assert_eq!(config.output_device, "");
|
||||
assert_eq!(config.noise_gate_threshold, 0.01);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user