1use serde::{Deserialize, Serialize};
2
3use super::sync_secrets::{NotifyConfig, SyncConfig};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(rename_all = "camelCase", deny_unknown_fields)]
7pub struct DaemonConfig {
8 #[serde(default)]
9 pub enabled: bool,
10 #[serde(default)]
11 pub reconcile: Option<ReconcileConfig>,
12 #[serde(default)]
13 pub sync: Option<SyncConfig>,
14 #[serde(default)]
15 pub notify: Option<NotifyConfig>,
16 #[serde(default)]
22 pub windows_event_log: bool,
23}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
26#[serde(rename_all = "camelCase", deny_unknown_fields)]
27pub struct ReconcileConfig {
28 #[serde(default = "default_reconcile_interval")]
29 pub interval: String,
30 #[serde(default)]
31 pub on_change: bool,
32 #[serde(default)]
33 pub auto_apply: bool,
34 #[serde(default)]
35 pub policy: Option<AutoApplyPolicyConfig>,
36 #[serde(default)]
40 pub drift_policy: DriftPolicy,
41 #[serde(default)]
45 pub patches: Vec<ReconcilePatch>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase", deny_unknown_fields)]
52pub struct ReconcilePatch {
53 pub kind: ReconcilePatchKind,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub name: Option<String>,
56 #[serde(default, skip_serializing_if = "Option::is_none")]
57 pub interval: Option<String>,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub auto_apply: Option<bool>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub drift_policy: Option<DriftPolicy>,
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
65pub enum ReconcilePatchKind {
66 Module,
67 Profile,
68}
69
70#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
72pub enum DriftPolicy {
73 Auto,
75 #[default]
77 NotifyOnly,
78 Prompt,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
83#[serde(rename_all = "camelCase", deny_unknown_fields)]
84pub struct AutoApplyPolicyConfig {
85 #[serde(default = "default_policy_notify")]
86 pub new_recommended: PolicyAction,
87 #[serde(default = "default_policy_ignore")]
88 pub new_optional: PolicyAction,
89 #[serde(default = "default_policy_notify")]
90 pub locked_conflict: PolicyAction,
91}
92
93impl Default for AutoApplyPolicyConfig {
94 fn default() -> Self {
95 Self {
96 new_recommended: PolicyAction::Notify,
97 new_optional: PolicyAction::Ignore,
98 locked_conflict: PolicyAction::Notify,
99 }
100 }
101}
102
103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
104pub enum PolicyAction {
105 Notify,
106 Accept,
107 Reject,
108 Ignore,
109}
110
111fn default_policy_notify() -> PolicyAction {
112 PolicyAction::Notify
113}
114
115fn default_policy_ignore() -> PolicyAction {
116 PolicyAction::Ignore
117}
118
119fn default_reconcile_interval() -> String {
120 "5m".to_string()
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn auto_apply_policy_default_values() {
129 let policy = AutoApplyPolicyConfig::default();
130 assert!(matches!(policy.new_recommended, PolicyAction::Notify));
131 assert!(matches!(policy.new_optional, PolicyAction::Ignore));
132 assert!(matches!(policy.locked_conflict, PolicyAction::Notify));
133 }
134
135 #[test]
136 fn drift_policy_default_is_notify_only() {
137 let dp = DriftPolicy::default();
138 assert_eq!(dp, DriftPolicy::NotifyOnly);
139 }
140
141 #[test]
142 fn reconcile_config_deserializes_with_defaults() {
143 let yaml = "onChange: true";
144 let config: ReconcileConfig = serde_yaml::from_str(yaml).unwrap();
145 assert!(config.on_change);
146 assert!(!config.auto_apply);
147 assert_eq!(config.interval, "5m");
148 assert_eq!(config.drift_policy, DriftPolicy::NotifyOnly);
149 assert!(config.patches.is_empty());
150 }
151
152 #[test]
153 fn auto_apply_policy_deserializes_with_serde_defaults() {
154 let yaml = "newRecommended: Accept";
155 let config: AutoApplyPolicyConfig = serde_yaml::from_str(yaml).unwrap();
156 assert!(matches!(config.new_recommended, PolicyAction::Accept));
157 assert!(matches!(config.new_optional, PolicyAction::Ignore));
158 assert!(matches!(config.locked_conflict, PolicyAction::Notify));
159 }
160
161 #[test]
162 fn daemon_config_deserializes_minimal() {
163 let yaml = "enabled: true";
164 let config: DaemonConfig = serde_yaml::from_str(yaml).unwrap();
165 assert!(config.enabled);
166 assert!(config.reconcile.is_none());
167 assert!(config.sync.is_none());
168 assert!(config.notify.is_none());
169 assert!(!config.windows_event_log);
170 }
171
172 #[test]
173 fn reconcile_patch_deserializes() {
174 let yaml = "kind: Module\nname: docker\ninterval: 10m\nautoApply: true\ndriftPolicy: Auto";
175 let patch: ReconcilePatch = serde_yaml::from_str(yaml).unwrap();
176 assert_eq!(patch.kind, ReconcilePatchKind::Module);
177 assert_eq!(patch.name.as_deref(), Some("docker"));
178 assert_eq!(patch.interval.as_deref(), Some("10m"));
179 assert_eq!(patch.auto_apply, Some(true));
180 assert_eq!(patch.drift_policy, Some(DriftPolicy::Auto));
181 }
182
183 #[test]
184 fn drift_policy_all_variants_deserialize() {
185 let auto: DriftPolicy = serde_yaml::from_str("Auto").unwrap();
186 let notify: DriftPolicy = serde_yaml::from_str("NotifyOnly").unwrap();
187 let prompt: DriftPolicy = serde_yaml::from_str("Prompt").unwrap();
188 assert_eq!(auto, DriftPolicy::Auto);
189 assert_eq!(notify, DriftPolicy::NotifyOnly);
190 assert_eq!(prompt, DriftPolicy::Prompt);
191 }
192
193 #[test]
194 fn daemon_config_rejects_unknown_field() {
195 let yaml = "enabled: true\nbogus: 1\n";
196 let err = serde_yaml::from_str::<DaemonConfig>(yaml)
197 .expect_err("expected deny_unknown_fields to reject bogus");
198 assert!(format!("{}", err).contains("unknown field"));
199 }
200
201 #[test]
202 fn reconcile_config_rejects_drift_policy_typo() {
203 let yaml = "interval: 5m\ndirftPolicy: Auto\n";
207 let err = serde_yaml::from_str::<ReconcileConfig>(yaml)
208 .expect_err("expected deny_unknown_fields to reject dirftPolicy");
209 let msg = format!("{}", err);
210 assert!(
211 msg.contains("unknown field") && msg.contains("dirftPolicy"),
212 "expected unknown-field error mentioning dirftPolicy, got: {msg}"
213 );
214 }
215
216 #[test]
217 fn policy_action_all_variants_deserialize() {
218 let notify: PolicyAction = serde_yaml::from_str("Notify").unwrap();
219 let accept: PolicyAction = serde_yaml::from_str("Accept").unwrap();
220 let reject: PolicyAction = serde_yaml::from_str("Reject").unwrap();
221 let ignore: PolicyAction = serde_yaml::from_str("Ignore").unwrap();
222 assert_eq!(notify, PolicyAction::Notify);
223 assert_eq!(accept, PolicyAction::Accept);
224 assert_eq!(reject, PolicyAction::Reject);
225 assert_eq!(ignore, PolicyAction::Ignore);
226 }
227}