1use colored::Colorize;
2use std::path::{Path, PathBuf};
3
4pub type Result<T> = std::result::Result<T, CliTestError>;
6
7fn sanitize_path_for_display(path: &Path) -> String {
18 path.file_name()
19 .and_then(|n| n.to_str())
20 .map(|s| s.to_string())
21 .unwrap_or_else(|| "<invalid-path>".to_string())
22}
23
24#[derive(Debug)]
32pub enum CliTestError {
33 BinaryNotFound(PathBuf),
35
36 BinaryNotExecutable(PathBuf),
38
39 ExecutionFailed(String),
41
42 InvalidHelpOutput,
44
45 OptionParseError(String),
47
48 TemplateError(String),
50
51 BatsExecutionFailed(String),
53
54 ReportError(String),
56
57 Config(String),
59
60 Validation(String),
62
63 InvalidFormat(String),
65
66 IoError(std::io::Error),
68
69 Json(serde_json::Error),
71
72 Yaml(serde_yaml::Error),
74
75 HandlebarsTemplate(handlebars::TemplateError),
77
78 HandlebarsRender(handlebars::RenderError),
80}
81
82impl std::fmt::Display for CliTestError {
84 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85 match self {
86 Self::BinaryNotFound(path) => {
87 write!(f, "Binary not found: {}", sanitize_path_for_display(path))
88 }
89 Self::BinaryNotExecutable(path) => {
90 write!(
91 f,
92 "Binary not executable: {}",
93 sanitize_path_for_display(path)
94 )
95 }
96 Self::ExecutionFailed(msg) => write!(f, "Failed to execute binary: {}", msg),
97 Self::InvalidHelpOutput => write!(f, "Invalid help output"),
98 Self::OptionParseError(details) => write!(f, "Failed to parse option: {}", details),
99 Self::TemplateError(msg) => write!(f, "Template rendering failed: {}", msg),
100 Self::BatsExecutionFailed(msg) => write!(f, "BATS execution failed: {}", msg),
101 Self::ReportError(msg) => write!(f, "Report generation failed: {}", msg),
102 Self::Config(msg) => write!(f, "Configuration error: {}", msg),
103 Self::Validation(msg) => write!(f, "Validation error: {}", msg),
104 Self::InvalidFormat(msg) => write!(f, "Invalid format: {}", msg),
105 Self::IoError(e) => write!(f, "I/O error: {}", e),
106 Self::Json(e) => write!(f, "JSON error: {}", e),
107 Self::Yaml(e) => write!(f, "YAML error: {}", e),
108 Self::HandlebarsTemplate(e) => write!(f, "Template syntax error: {}", e),
109 Self::HandlebarsRender(e) => write!(f, "Template rendering error: {}", e),
110 }
111 }
112}
113
114impl std::error::Error for CliTestError {
116 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
117 match self {
118 Self::IoError(e) => Some(e),
119 Self::Json(e) => Some(e),
120 Self::Yaml(e) => Some(e),
121 Self::HandlebarsTemplate(e) => Some(e),
122 Self::HandlebarsRender(e) => Some(e),
123 _ => None,
124 }
125 }
126}
127
128impl From<std::io::Error> for CliTestError {
130 fn from(err: std::io::Error) -> Self {
131 Self::IoError(err)
132 }
133}
134
135impl From<serde_json::Error> for CliTestError {
136 fn from(err: serde_json::Error) -> Self {
137 Self::Json(err)
138 }
139}
140
141impl From<serde_yaml::Error> for CliTestError {
142 fn from(err: serde_yaml::Error) -> Self {
143 Self::Yaml(err)
144 }
145}
146
147impl From<handlebars::TemplateError> for CliTestError {
148 fn from(err: handlebars::TemplateError) -> Self {
149 Self::HandlebarsTemplate(err)
150 }
151}
152
153impl From<handlebars::RenderError> for CliTestError {
154 fn from(err: handlebars::RenderError) -> Self {
155 Self::HandlebarsRender(err)
156 }
157}
158
159pub use CliTestError as Error;
161
162impl CliTestError {
163 pub fn detailed_message(&self) -> String {
168 match self {
169 Self::BinaryNotFound(path) => {
170 format!("Binary not found at path: {}", path.display())
171 }
172 Self::BinaryNotExecutable(path) => {
173 format!("Binary at {} is not executable", path.display())
174 }
175 Self::ExecutionFailed(msg) => {
176 format!("Binary execution failed: {}", msg)
177 }
178 Self::InvalidHelpOutput => {
179 "Help output could not be parsed - ensure binary supports --help".to_string()
180 }
181 Self::OptionParseError(details) => {
182 format!("Failed to parse option: {}", details)
183 }
184 Self::TemplateError(msg) => {
185 format!("Template rendering error: {}", msg)
186 }
187 Self::BatsExecutionFailed(msg) => {
188 format!("BATS test execution failed: {}", msg)
189 }
190 Self::ReportError(msg) => {
191 format!("Report generation error: {}", msg)
192 }
193 Self::Config(msg) => {
194 format!("Configuration error: {}", msg)
195 }
196 Self::Validation(msg) => {
197 format!("Validation error: {}", msg)
198 }
199 Self::InvalidFormat(msg) => {
200 format!("Invalid format: {}", msg)
201 }
202 Self::IoError(e) => {
203 format!("I/O error: {}", e)
204 }
205 Self::Json(e) => {
206 format!("JSON error: {}", e)
207 }
208 Self::Yaml(e) => {
209 format!("YAML error: {}", e)
210 }
211 Self::HandlebarsTemplate(e) => {
212 format!("Handlebars template error: {}", e)
213 }
214 Self::HandlebarsRender(e) => {
215 format!("Handlebars render error: {}", e)
216 }
217 }
218 }
219
220 pub fn user_message(&self) -> String {
225 match self {
226 Self::BinaryNotFound(path) => {
227 let filename = sanitize_path_for_display(path);
228 format!(
229 "{} {}\n{} {}",
230 "Error:".red().bold(),
231 format!("Binary not found: {}", filename).white(),
232 "Suggestion:".yellow().bold(),
233 "Check that the path is correct and the file exists".white()
234 )
235 }
236 Self::BinaryNotExecutable(path) => {
237 let filename = sanitize_path_for_display(path);
238 format!(
239 "{} {}\n{} {}",
240 "Error:".red().bold(),
241 format!("Binary is not executable: {}", filename).white(),
242 "Suggestion:".yellow().bold(),
243 format!("Try: chmod +x {}", filename).white()
244 )
245 }
246 Self::ExecutionFailed(msg) => {
247 format!(
248 "{} {}\n{} {}",
249 "Error:".red().bold(),
250 format!("Failed to execute binary: {}", msg).white(),
251 "Suggestion:".yellow().bold(),
252 "Verify the binary runs correctly with --help flag".white()
253 )
254 }
255 Self::InvalidHelpOutput => {
256 format!(
257 "{} {}\n{} {}",
258 "Error:".red().bold(),
259 "Help output could not be parsed".white(),
260 "Suggestion:".yellow().bold(),
261 "Ensure the binary supports --help and produces valid output".white()
262 )
263 }
264 Self::OptionParseError(details) => {
265 format!(
266 "{} {}\n{} {}",
267 "Error:".red().bold(),
268 format!("Failed to parse option: {}", details).white(),
269 "Suggestion:".yellow().bold(),
270 "Check if the help text follows standard CLI conventions".white()
271 )
272 }
273 Self::TemplateError(msg) => {
274 format!(
275 "{} {}\n{} {}",
276 "Error:".red().bold(),
277 format!("Template rendering failed: {}", msg).white(),
278 "Suggestion:".yellow().bold(),
279 "Verify template syntax and variable bindings".white()
280 )
281 }
282 Self::BatsExecutionFailed(msg) => {
283 format!(
284 "{} {}\n{} {}",
285 "Error:".red().bold(),
286 format!("BATS test execution failed: {}", msg).white(),
287 "Suggestion:".yellow().bold(),
288 "Install BATS: brew install bats-core or apt-get install bats".white()
289 )
290 }
291 Self::ReportError(msg) => {
292 format!(
293 "{} {}\n{} {}",
294 "Error:".red().bold(),
295 format!("Report generation failed: {}", msg).white(),
296 "Suggestion:".yellow().bold(),
297 "Check output directory permissions and disk space".white()
298 )
299 }
300 Self::Config(msg) => {
301 format!(
302 "{} {}\n{} {}",
303 "Error:".red().bold(),
304 format!("Configuration error: {}", msg).white(),
305 "Suggestion:".yellow().bold(),
306 "Review your configuration file syntax and required fields".white()
307 )
308 }
309 Self::Validation(msg) => {
310 format!(
311 "{} {}\n{} {}",
312 "Error:".red().bold(),
313 format!("Validation error: {}", msg).white(),
314 "Suggestion:".yellow().bold(),
315 "Ensure all required parameters are provided".white()
316 )
317 }
318 Self::InvalidFormat(msg) => {
319 format!(
320 "{} {}\n{} {}",
321 "Error:".red().bold(),
322 format!("Invalid format: {}", msg).white(),
323 "Suggestion:".yellow().bold(),
324 "Use a supported format (bats, assert_cmd, snapbox)".white()
325 )
326 }
327 Self::IoError(e) => {
328 format!(
329 "{} {}\n{} {}",
330 "Error:".red().bold(),
331 format!("I/O error: {}", e).white(),
332 "Suggestion:".yellow().bold(),
333 "Check file permissions and disk space".white()
334 )
335 }
336 Self::Json(e) => {
337 format!(
338 "{} {}\n{} {}",
339 "Error:".red().bold(),
340 format!("JSON error: {}", e).white(),
341 "Suggestion:".yellow().bold(),
342 "Validate JSON syntax using a JSON linter".white()
343 )
344 }
345 Self::Yaml(e) => {
346 format!(
347 "{} {}\n{} {}",
348 "Error:".red().bold(),
349 format!("YAML error: {}", e).white(),
350 "Suggestion:".yellow().bold(),
351 "Check YAML indentation and syntax".white()
352 )
353 }
354 Self::HandlebarsTemplate(e) => {
355 format!(
356 "{} {}\n{} {}",
357 "Error:".red().bold(),
358 format!("Template syntax error: {}", e).white(),
359 "Suggestion:".yellow().bold(),
360 "Check Handlebars template syntax and variable names".white()
361 )
362 }
363 Self::HandlebarsRender(e) => {
364 format!(
365 "{} {}\n{} {}",
366 "Error:".red().bold(),
367 format!("Template rendering error: {}", e).white(),
368 "Suggestion:".yellow().bold(),
369 "Verify template data and variable bindings".white()
370 )
371 }
372 }
373 }
374
375 pub fn print_error(&self) {
377 eprintln!("{}", self.user_message());
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 use super::*;
384
385 #[test]
386 fn test_binary_not_found_error() {
387 let path = PathBuf::from("/nonexistent/binary");
388 let error = CliTestError::BinaryNotFound(path.clone());
389 assert!(error.to_string().contains("Binary not found"));
390 }
391
392 #[test]
393 fn test_binary_not_executable_error() {
394 let path = PathBuf::from("/bin/not-executable");
395 let error = CliTestError::BinaryNotExecutable(path);
396 assert!(error.to_string().contains("not executable"));
397 }
398
399 #[test]
400 fn test_execution_failed_error() {
401 let error = CliTestError::ExecutionFailed("timeout".to_string());
402 assert!(error.to_string().contains("Failed to execute"));
403 }
404
405 #[test]
406 fn test_detailed_message_contains_more_info() {
407 let path = PathBuf::from("/test/binary");
408 let error = CliTestError::BinaryNotFound(path);
409 let detailed = error.detailed_message();
410
411 assert!(detailed.contains("/test/binary"));
413 }
414
415 #[test]
418 fn test_display_hides_sensitive_paths() {
419 let path = PathBuf::from("/home/user/secret/project/binary");
421 let error = CliTestError::BinaryNotFound(path);
422 let display_msg = error.to_string();
423
424 assert!(!display_msg.contains("/home"));
426 assert!(!display_msg.contains("/user"));
427 assert!(!display_msg.contains("/secret"));
428 assert!(!display_msg.contains("/project"));
429
430 assert!(display_msg.contains("binary"));
432 }
433
434 #[test]
435 fn test_display_vs_detailed_security() {
436 let path = PathBuf::from("/var/lib/sensitive/data.json");
437 let error = CliTestError::BinaryNotFound(path);
438
439 let display_msg = error.to_string();
440 let detailed_msg = error.detailed_message();
441
442 assert!(!display_msg.contains("/var"));
444 assert!(!display_msg.contains("sensitive"));
445 assert!(display_msg.contains("data.json"));
446
447 assert!(detailed_msg.contains("/var/lib/sensitive/data.json"));
449 }
450
451 #[test]
452 fn test_path_sanitization_windows_style() {
453 #[cfg(windows)]
456 {
457 let path = PathBuf::from("C:\\Users\\Admin\\Documents\\secret.exe");
458 let error = CliTestError::BinaryNotExecutable(path);
459 let display_msg = error.to_string();
460
461 assert!(!display_msg.contains("Users"));
463 assert!(!display_msg.contains("Admin"));
464 assert!(!display_msg.contains("Documents"));
465
466 assert!(display_msg.contains("secret.exe"));
468 }
469
470 #[cfg(unix)]
471 {
472 let path = PathBuf::from("/home/admin/documents/secret.exe");
474 let error = CliTestError::BinaryNotExecutable(path);
475 let display_msg = error.to_string();
476
477 assert!(!display_msg.contains("home"));
479 assert!(!display_msg.contains("admin"));
480 assert!(!display_msg.contains("documents"));
481
482 assert!(display_msg.contains("secret.exe"));
484 }
485 }
486
487 #[test]
488 fn test_sanitize_path_with_special_characters() {
489 let path = PathBuf::from("/tmp/../../../etc/passwd");
490 let error = CliTestError::BinaryNotFound(path);
491 let display_msg = error.to_string();
492
493 assert!(!display_msg.contains(".."));
495 assert!(!display_msg.contains("/etc"));
496 assert!(!display_msg.contains("/tmp"));
497 }
498
499 #[test]
500 fn test_invalid_path_handling() {
501 let path = PathBuf::from("");
503 let error = CliTestError::BinaryNotFound(path);
504 let display_msg = error.to_string();
505
506 assert!(display_msg.contains("<invalid-path>") || display_msg.is_empty());
508 }
509
510 #[test]
511 fn test_user_message_is_safe() {
512 let path = PathBuf::from("/home/user/.ssh/id_rsa");
513 let error = CliTestError::BinaryNotFound(path);
514 let user_msg = error.user_message();
515
516 assert!(!user_msg.contains(".ssh"));
518 assert!(!user_msg.contains("/home"));
519 }
520
521 #[test]
522 fn test_io_error_does_not_leak_paths() {
523 let io_error = std::io::Error::new(
525 std::io::ErrorKind::NotFound,
526 "File not found: /secret/path/file.txt",
527 );
528 let error = CliTestError::from(io_error);
529 let display_msg = error.to_string();
530
531 assert!(display_msg.contains("I/O error"));
534 }
535}