vika_cli/config/
validator.rs1use crate::config::model::Config;
2use crate::error::{ConfigError, Result};
3use std::path::{Path, PathBuf};
4
5pub fn validate_config(config: &Config) -> Result<()> {
6 if config.specs.is_empty() {
8 return Err(ConfigError::NoSpecDefined.into());
9 }
10
11 let mut seen_names = std::collections::HashSet::new();
14 for spec in &config.specs {
15 if seen_names.contains(&spec.name) {
16 return Err(ConfigError::DuplicateSpecName {
17 name: spec.name.clone(),
18 }
19 .into());
20 }
21 seen_names.insert(&spec.name);
22
23 if spec.name.is_empty() {
25 return Err(ConfigError::InvalidSpecName {
26 name: spec.name.clone(),
27 }
28 .into());
29 }
30
31 if !spec
33 .name
34 .chars()
35 .all(|c| c.is_alphanumeric() || c == '-' || c == '_')
36 {
37 return Err(ConfigError::InvalidSpecName {
38 name: spec.name.clone(),
39 }
40 .into());
41 }
42
43 if spec.path.is_empty() {
45 return Err(ConfigError::Invalid {
46 message: format!("Spec '{}' has an empty path", spec.name),
47 }
48 .into());
49 }
50
51 let schemas_output = PathBuf::from(&spec.schemas.output);
53 if schemas_output.is_absolute() {
54 validate_safe_path(&schemas_output)?;
55 }
56
57 let apis_output = PathBuf::from(&spec.apis.output);
59 if apis_output.is_absolute() {
60 validate_safe_path(&apis_output)?;
61 }
62
63 if spec.apis.style != "fetch" {
65 return Err(ConfigError::Invalid {
66 message: format!(
67 "Unsupported API style for spec '{}': {}. Only 'fetch' is supported.",
68 spec.name, spec.apis.style
69 ),
70 }
71 .into());
72 }
73 }
74
75 let root_dir = PathBuf::from(&config.root_dir);
77 if root_dir.is_absolute() && !root_dir.exists() {
78 return Err(ConfigError::Invalid {
79 message: format!("Root directory does not exist: {}", config.root_dir),
80 }
81 .into());
82 }
83
84 Ok(())
85}
86
87fn validate_safe_path(path: &Path) -> Result<()> {
88 let path_str = path.to_string_lossy();
90
91 if path_str.contains("/etc/")
92 || path_str.contains("/usr/")
93 || path_str.contains("/bin/")
94 || path_str.contains("/sbin/")
95 || path_str.contains("/var/")
96 || path_str.contains("/opt/")
97 || path_str == "/"
98 || path_str == "/root"
99 {
100 return Err(ConfigError::InvalidOutputDirectory {
101 path: path_str.to_string(),
102 }
103 .into());
104 }
105
106 Ok(())
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::config::model::Config;
113
114 #[test]
115 fn test_validate_config_valid() {
116 let mut config = Config::default();
117 config.specs = vec![crate::config::model::SpecEntry {
118 name: "test".to_string(),
119 path: "test.yaml".to_string(),
120 schemas: crate::config::model::SchemasConfig::default(),
121 apis: crate::config::model::ApisConfig::default(),
122 modules: crate::config::model::ModulesConfig::default(),
123 }];
124 assert!(validate_config(&config).is_ok());
125 }
126
127 #[test]
128 fn test_validate_config_invalid_style() {
129 let mut config = Config::default();
130 let mut apis = crate::config::model::ApisConfig::default();
131 apis.style = "invalid".to_string();
132 config.specs = vec![crate::config::model::SpecEntry {
133 name: "test".to_string(),
134 path: "test.yaml".to_string(),
135 schemas: crate::config::model::SchemasConfig::default(),
136 apis,
137 modules: crate::config::model::ModulesConfig::default(),
138 }];
139
140 let result = validate_config(&config);
141 assert!(result.is_err());
142 let error = result.unwrap_err();
143 assert!(error.to_string().contains("Unsupported API style"));
144 }
145
146 #[test]
147 fn test_validate_safe_path_etc() {
148 let path = PathBuf::from("/etc/test");
149 let result = validate_safe_path(&path);
150 assert!(result.is_err());
151 }
152
153 #[test]
154 fn test_validate_safe_path_usr() {
155 let path = PathBuf::from("/usr/test");
156 let result = validate_safe_path(&path);
157 assert!(result.is_err());
158 }
159
160 #[test]
161 fn test_validate_safe_path_bin() {
162 let path = PathBuf::from("/bin/test");
163 let result = validate_safe_path(&path);
164 assert!(result.is_err());
165 }
166
167 #[test]
168 fn test_validate_safe_path_root() {
169 let path = PathBuf::from("/");
170 let result = validate_safe_path(&path);
171 assert!(result.is_err());
172 }
173
174 #[test]
175 fn test_validate_safe_path_valid() {
176 let path = PathBuf::from("/home/user/project");
177 let result = validate_safe_path(&path);
178 assert!(result.is_ok());
179 }
180
181 #[test]
182 fn test_validate_config_absolute_paths() {
183 let mut config = Config::default();
184 let mut schemas = crate::config::model::SchemasConfig::default();
185 schemas.output = "/home/user/schemas".to_string();
186 let mut apis = crate::config::model::ApisConfig::default();
187 apis.output = "/home/user/apis".to_string();
188 config.specs = vec![crate::config::model::SpecEntry {
189 name: "test".to_string(),
190 path: "test.yaml".to_string(),
191 schemas,
192 apis,
193 modules: crate::config::model::ModulesConfig::default(),
194 }];
195
196 let result = validate_config(&config);
197 assert!(result.is_ok());
198 }
199
200 #[test]
201 fn test_validate_config_unsafe_schemas_path() {
202 let mut config = Config::default();
203 let mut schemas = crate::config::model::SchemasConfig::default();
204 schemas.output = "/etc/schemas".to_string();
205 config.specs = vec![crate::config::model::SpecEntry {
206 name: "test".to_string(),
207 path: "test.yaml".to_string(),
208 schemas,
209 apis: crate::config::model::ApisConfig::default(),
210 modules: crate::config::model::ModulesConfig::default(),
211 }];
212
213 let result = validate_config(&config);
214 assert!(result.is_err());
215 }
216
217 #[test]
218 fn test_validate_config_unsafe_apis_path() {
219 let mut config = Config::default();
220 let mut apis = crate::config::model::ApisConfig::default();
221 apis.output = "/usr/apis".to_string();
222 config.specs = vec![crate::config::model::SpecEntry {
223 name: "test".to_string(),
224 path: "test.yaml".to_string(),
225 schemas: crate::config::model::SchemasConfig::default(),
226 apis,
227 modules: crate::config::model::ModulesConfig::default(),
228 }];
229
230 let result = validate_config(&config);
231 assert!(result.is_err());
232 }
233
234 #[test]
235 fn test_validate_config_no_spec_defined() {
236 let config = Config::default();
237 let result = validate_config(&config);
239 assert!(result.is_err());
240 let error = result.unwrap_err();
241 assert!(error.to_string().contains("No specs are defined"));
242 }
243
244 #[test]
245 fn test_validate_config_empty_specs_array() {
246 let mut config = Config::default();
247 config.specs = vec![];
248
249 let result = validate_config(&config);
250 assert!(result.is_err());
251 let error = result.unwrap_err();
252 assert!(error.to_string().contains("No specs are defined"));
253 }
254
255 #[test]
256 fn test_validate_config_duplicate_spec_names() {
257 let mut config = Config::default();
258 config.specs = vec![
259 crate::config::model::SpecEntry {
260 name: "auth".to_string(),
261 path: "specs/auth.yaml".to_string(),
262 schemas: crate::config::model::SchemasConfig::default(),
263 apis: crate::config::model::ApisConfig::default(),
264 modules: crate::config::model::ModulesConfig::default(),
265 },
266 crate::config::model::SpecEntry {
267 name: "auth".to_string(),
268 path: "specs/auth2.yaml".to_string(),
269 schemas: crate::config::model::SchemasConfig::default(),
270 apis: crate::config::model::ApisConfig::default(),
271 modules: crate::config::model::ModulesConfig::default(),
272 },
273 ];
274
275 let result = validate_config(&config);
276 assert!(result.is_err());
277 let error = result.unwrap_err();
278 assert!(error.to_string().contains("Duplicate spec name"));
279 }
280
281 #[test]
282 fn test_validate_config_invalid_spec_name() {
283 let mut config = Config::default();
284 config.specs = vec![crate::config::model::SpecEntry {
285 name: "invalid name".to_string(), path: "specs/auth.yaml".to_string(),
287 schemas: crate::config::model::SchemasConfig::default(),
288 apis: crate::config::model::ApisConfig::default(),
289 modules: crate::config::model::ModulesConfig::default(),
290 }];
291
292 let result = validate_config(&config);
293 assert!(result.is_err());
294 let error = result.unwrap_err();
295 assert!(error.to_string().contains("Invalid spec name"));
296 }
297
298 #[test]
299 fn test_validate_config_empty_spec_name() {
300 let mut config = Config::default();
301 config.specs = vec![crate::config::model::SpecEntry {
302 name: "".to_string(),
303 path: "specs/auth.yaml".to_string(),
304 schemas: crate::config::model::SchemasConfig::default(),
305 apis: crate::config::model::ApisConfig::default(),
306 modules: crate::config::model::ModulesConfig::default(),
307 }];
308
309 let result = validate_config(&config);
310 assert!(result.is_err());
311 let error = result.unwrap_err();
312 assert!(error.to_string().contains("Invalid spec name"));
313 }
314
315 #[test]
316 fn test_validate_config_empty_spec_path() {
317 let mut config = Config::default();
318 config.specs = vec![crate::config::model::SpecEntry {
319 name: "auth".to_string(),
320 path: "".to_string(),
321 schemas: crate::config::model::SchemasConfig::default(),
322 apis: crate::config::model::ApisConfig::default(),
323 modules: crate::config::model::ModulesConfig::default(),
324 }];
325
326 let result = validate_config(&config);
327 assert!(result.is_err());
328 let error = result.unwrap_err();
329 assert!(error.to_string().contains("empty path"));
330 }
331
332 #[test]
333 fn test_validate_config_valid_multi_spec() {
334 let mut config = Config::default();
335 config.specs = vec![
336 crate::config::model::SpecEntry {
337 name: "auth".to_string(),
338 path: "specs/auth.yaml".to_string(),
339 schemas: crate::config::model::SchemasConfig::default(),
340 apis: crate::config::model::ApisConfig::default(),
341 modules: crate::config::model::ModulesConfig::default(),
342 },
343 crate::config::model::SpecEntry {
344 name: "orders".to_string(),
345 path: "specs/orders.json".to_string(),
346 schemas: crate::config::model::SchemasConfig::default(),
347 apis: crate::config::model::ApisConfig::default(),
348 modules: crate::config::model::ModulesConfig::default(),
349 },
350 ];
351
352 let result = validate_config(&config);
353 assert!(result.is_ok());
354 }
355
356 #[test]
357 fn test_validate_config_valid_single_spec() {
358 let mut config = Config::default();
359 config.specs = vec![crate::config::model::SpecEntry {
360 name: "default".to_string(),
361 path: "openapi.json".to_string(),
362 schemas: crate::config::model::SchemasConfig::default(),
363 apis: crate::config::model::ApisConfig::default(),
364 modules: crate::config::model::ModulesConfig::default(),
365 }];
366
367 let result = validate_config(&config);
368 assert!(result.is_ok());
369 }
370}