confium_coordinator/
config_validator.rs1use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone)]
7pub struct ValidationResult {
8 pub valid: bool,
10 pub issues: Vec<ConfigIssue>,
12}
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct ConfigIssue {
17 pub field: String,
19 pub message: String,
21 pub suggestion: String,
23 pub severity: String,
25}
26
27impl ValidationResult {
28 pub fn has_errors(&self) -> bool {
30 self.issues.iter().any(|i| i.severity == "error")
31 }
32
33 pub fn errors(&self) -> impl Iterator<Item = &ConfigIssue> {
35 self.issues.iter().filter(|i| i.severity == "error")
36 }
37}
38
39pub fn validate_coordinator_config(
41 addr: &str,
42 max_sessions: usize,
43 session_timeout_secs: u64,
44) -> ValidationResult {
45 let mut issues = Vec::new();
46
47 if addr.is_empty() {
48 issues.push(ConfigIssue {
49 field: "addr".into(),
50 message: "address is empty".into(),
51 suggestion: "set to e.g. '0.0.0.0:18432'".into(),
52 severity: "error".into(),
53 });
54 } else if !addr.contains(':') {
55 issues.push(ConfigIssue {
56 field: "addr".into(),
57 message: "address missing port".into(),
58 suggestion: "format: 'host:port'".into(),
59 severity: "error".into(),
60 });
61 }
62
63 if max_sessions == 0 {
64 issues.push(ConfigIssue {
65 field: "max_sessions".into(),
66 message: "max_sessions is 0 (unlimited)".into(),
67 suggestion: "set a positive limit for production".into(),
68 severity: "warning".into(),
69 });
70 }
71
72 if session_timeout_secs < 60 {
73 issues.push(ConfigIssue {
74 field: "session_timeout_secs".into(),
75 message: "timeout < 60 seconds may be too short".into(),
76 suggestion: "use at least 300 (5 min) for production".into(),
77 severity: "warning".into(),
78 });
79 }
80
81 ValidationResult {
82 valid: !issues.iter().any(|i| i.severity == "error"),
83 issues,
84 }
85}
86
87pub fn validate_signer_config(
89 coordinator_addr: &str,
90 signer_id: &str,
91 quorum_id: &str,
92 share_file: &str,
93) -> ValidationResult {
94 let mut issues = Vec::new();
95
96 if signer_id.is_empty() {
97 issues.push(ConfigIssue {
98 field: "signer_id".into(),
99 message: "signer_id is empty".into(),
100 suggestion: "set a unique signer identity".into(),
101 severity: "error".into(),
102 });
103 }
104
105 if quorum_id.is_empty() {
106 issues.push(ConfigIssue {
107 field: "quorum_id".into(),
108 message: "quorum_id is empty".into(),
109 suggestion: "set the quorum this signer belongs to".into(),
110 severity: "error".into(),
111 });
112 }
113
114 if coordinator_addr.is_empty() || !coordinator_addr.contains(':') {
115 issues.push(ConfigIssue {
116 field: "coordinator_addr".into(),
117 message: "coordinator_addr is invalid".into(),
118 suggestion: "format: 'host:port'".into(),
119 severity: "error".into(),
120 });
121 }
122
123 if share_file.is_empty() {
124 issues.push(ConfigIssue {
125 field: "share_file".into(),
126 message: "share_file is empty".into(),
127 suggestion: "set path to the share JSON file".into(),
128 severity: "error".into(),
129 });
130 }
131
132 ValidationResult {
133 valid: !issues.iter().any(|i| i.severity == "error"),
134 issues,
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn valid_coordinator_config_passes() {
144 let result = validate_coordinator_config("0.0.0.0:18432", 100, 3600);
145 assert!(result.valid);
146 assert!(result.issues.is_empty());
147 }
148
149 #[test]
150 fn empty_addr_rejected() {
151 let result = validate_coordinator_config("", 100, 3600);
152 assert!(!result.valid);
153 assert!(result.has_errors());
154 }
155
156 #[test]
157 fn missing_port_rejected() {
158 let result = validate_coordinator_config("localhost", 100, 3600);
159 assert!(!result.valid);
160 }
161
162 #[test]
163 fn unlimited_sessions_warns() {
164 let result = validate_coordinator_config("0.0.0.0:80", 0, 3600);
165 assert!(result.valid); assert!(!result.has_errors());
167 }
168
169 #[test]
170 fn short_timeout_warns() {
171 let result = validate_coordinator_config("0.0.0.0:80", 10, 30);
172 assert!(result.valid);
173 assert!(
174 result
175 .issues
176 .iter()
177 .any(|i| i.field == "session_timeout_secs")
178 );
179 }
180
181 #[test]
182 fn valid_signer_config_passes() {
183 let result =
184 validate_signer_config("localhost:18432", "alice", "quorum-1", "/shares/a.json");
185 assert!(result.valid);
186 }
187
188 #[test]
189 fn empty_signer_id_rejected() {
190 let result = validate_signer_config("localhost:18432", "", "q", "/s.json");
191 assert!(!result.valid);
192 }
193
194 #[test]
195 fn empty_share_file_rejected() {
196 let result = validate_signer_config("localhost:18432", "alice", "q", "");
197 assert!(!result.valid);
198 }
199
200 #[test]
201 fn issue_has_suggestion() {
202 let result = validate_coordinator_config("", 10, 3600);
203 for issue in &result.issues {
204 assert!(!issue.suggestion.is_empty());
205 }
206 }
207
208 #[test]
209 fn errors_filtered_correctly() {
210 let result = validate_coordinator_config("", 0, 30);
211 let errors: Vec<_> = result.errors().collect();
212 assert!(errors.iter().any(|e| e.severity == "error"));
213 assert!(result.issues.iter().any(|i| i.severity == "warning"));
214 }
215}