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:
2026-06-02 05:15:43 -04:00
co-authored by Claude Opus 4.8
parent 99bd1f0c57
commit 0f1f7f76a0
+102 -1
View File
@@ -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);
}
}