Skip to main content

torrust_tracker_deployer_lib/presentation/cli/input/cli/
mod.rs

1//! CLI Module
2//!
3//! This module provides the command-line interface structure and functionality
4//! for the Torrust Tracker Deployer application. It handles CLI argument parsing
5//! and provides the CLI data structures.
6
7use clap::Parser;
8
9// Re-export submodules for convenient access
10pub mod args;
11pub mod commands;
12pub mod output_format;
13
14pub use args::GlobalArgs;
15pub use commands::{Commands, CreateAction};
16pub use output_format::OutputFormat;
17
18/// Command-line interface for Torrust Tracker Deployer
19///
20/// This struct defines the top-level CLI structure including global arguments
21/// and available subcommands. It uses clap for argument parsing and provides
22/// comprehensive help documentation.
23#[derive(Parser, Debug)]
24#[command(name = "torrust-tracker-deployer")]
25#[command(about = "Automated deployment infrastructure for Torrust Tracker")]
26#[command(version)]
27#[allow(clippy::struct_field_names)] // CLI arguments intentionally share 'log_' prefix for clarity
28pub struct Cli {
29    /// Global arguments (logging configuration)
30    #[command(flatten)]
31    pub global: GlobalArgs,
32
33    /// Subcommand to execute
34    #[command(subcommand)]
35    pub command: Option<Commands>,
36}
37
38#[cfg(test)]
39mod tests {
40    use super::*;
41
42    #[test]
43    fn it_should_parse_destroy_subcommand() {
44        let args = vec!["torrust-tracker-deployer", "destroy", "test-env"];
45        let cli = Cli::try_parse_from(args).unwrap();
46
47        assert!(cli.command.is_some());
48        match cli.command.unwrap() {
49            Commands::Destroy { environment } => {
50                assert_eq!(environment, "test-env");
51            }
52            Commands::Create { .. }
53            | Commands::Provision { .. }
54            | Commands::Configure { .. }
55            | Commands::Test { .. }
56            | Commands::Register { .. }
57            | Commands::Release { .. }
58            | Commands::Run { .. }
59            | Commands::Show { .. }
60            | Commands::List
61            | Commands::Purge { .. }
62            | Commands::Validate { .. }
63            | Commands::Render { .. }
64            | Commands::Exists { .. }
65            | Commands::Docs { .. } => {
66                panic!("Expected Destroy command")
67            }
68        }
69    }
70
71    #[test]
72    fn it_should_parse_destroy_with_different_environment_names() {
73        let test_cases = vec!["e2e-provision", "production", "test-123", "dev-environment"];
74
75        for env_name in test_cases {
76            let args = vec!["torrust-tracker-deployer", "destroy", env_name];
77            let cli = Cli::try_parse_from(args).unwrap();
78
79            match cli.command.unwrap() {
80                Commands::Destroy { environment } => {
81                    assert_eq!(environment, env_name);
82                }
83                Commands::Create { .. }
84                | Commands::Provision { .. }
85                | Commands::Configure { .. }
86                | Commands::Test { .. }
87                | Commands::Register { .. }
88                | Commands::Release { .. }
89                | Commands::Run { .. }
90                | Commands::Show { .. }
91                | Commands::List
92                | Commands::Purge { .. }
93                | Commands::Validate { .. }
94                | Commands::Render { .. }
95                | Commands::Exists { .. }
96                | Commands::Docs { .. } => {
97                    panic!("Expected Destroy command")
98                }
99            }
100        }
101    }
102
103    #[test]
104    fn it_should_require_environment_parameter() {
105        let args = vec!["torrust-tracker-deployer", "destroy"];
106        let result = Cli::try_parse_from(args);
107
108        assert!(result.is_err());
109        let error = result.unwrap_err();
110        let error_message = error.to_string();
111        assert!(
112            error_message.contains("required") || error_message.contains("argument"),
113            "Error message should indicate missing required argument: {error_message}"
114        );
115    }
116
117    #[test]
118    fn it_should_parse_global_log_options_with_destroy_command() {
119        let args = vec![
120            "torrust-tracker-deployer",
121            "--log-file-format",
122            "json",
123            "--log-stderr-format",
124            "compact",
125            "--log-output",
126            "file-and-stderr",
127            "--log-dir",
128            "/tmp/logs",
129            "destroy",
130            "test-env",
131        ];
132        let cli = Cli::try_parse_from(args).unwrap();
133
134        // Verify the destroy command was parsed correctly
135        match cli.command.unwrap() {
136            Commands::Destroy { environment } => {
137                assert_eq!(environment, "test-env");
138            }
139            Commands::Create { .. }
140            | Commands::Provision { .. }
141            | Commands::Configure { .. }
142            | Commands::Test { .. }
143            | Commands::Register { .. }
144            | Commands::Release { .. }
145            | Commands::Run { .. }
146            | Commands::Show { .. }
147            | Commands::List
148            | Commands::Purge { .. }
149            | Commands::Validate { .. }
150            | Commands::Render { .. }
151            | Commands::Exists { .. }
152            | Commands::Docs { .. } => {
153                panic!("Expected Destroy command")
154            }
155        }
156
157        // Log options are set but we don't compare them as they don't implement PartialEq
158        assert_eq!(cli.global.log_dir, std::path::PathBuf::from("/tmp/logs"));
159    }
160
161    #[test]
162    fn it_should_use_default_log_dir_when_not_specified() {
163        let args = vec!["torrust-tracker-deployer", "destroy", "test-env"];
164        let cli = Cli::try_parse_from(args).unwrap();
165
166        assert_eq!(cli.global.log_dir, std::path::PathBuf::from("./data/logs"));
167    }
168
169    #[test]
170    fn it_should_handle_no_command() {
171        let args = vec!["torrust-tracker-deployer"];
172        let cli = Cli::try_parse_from(args).unwrap();
173
174        assert!(cli.command.is_none());
175    }
176
177    #[test]
178    fn it_should_show_help_with_help_flag() {
179        let args = vec!["torrust-tracker-deployer", "--help"];
180        let result = Cli::try_parse_from(args);
181
182        // Help flag causes a "display help" error
183        assert!(result.is_err());
184        let error = result.unwrap_err();
185        assert_eq!(error.kind(), clap::error::ErrorKind::DisplayHelp);
186    }
187
188    #[test]
189    fn it_should_show_version_with_version_flag() {
190        let args = vec!["torrust-tracker-deployer", "--version"];
191        let result = Cli::try_parse_from(args);
192
193        // Version flag causes a "display version" error
194        assert!(result.is_err());
195        let error = result.unwrap_err();
196        assert_eq!(error.kind(), clap::error::ErrorKind::DisplayVersion);
197    }
198
199    #[test]
200    fn it_should_show_destroy_help() {
201        let args = vec!["torrust-tracker-deployer", "destroy", "--help"];
202        let result = Cli::try_parse_from(args);
203
204        // Help flag causes a "display help" error
205        assert!(result.is_err());
206        let error = result.unwrap_err();
207        assert_eq!(error.kind(), clap::error::ErrorKind::DisplayHelp);
208
209        // Verify the help text mentions the environment parameter
210        let help_text = error.to_string();
211        assert!(
212            help_text.contains("environment") || help_text.contains("<ENVIRONMENT>"),
213            "Help text should mention environment parameter"
214        );
215    }
216
217    #[test]
218    fn it_should_parse_create_environment_subcommand() {
219        let args = vec![
220            "torrust-tracker-deployer",
221            "create",
222            "environment",
223            "--env-file",
224            "config.json",
225        ];
226        let cli = Cli::try_parse_from(args).unwrap();
227
228        assert!(cli.command.is_some());
229        match cli.command.unwrap() {
230            Commands::Create { action } => match action {
231                crate::presentation::cli::input::cli::CreateAction::Environment { env_file } => {
232                    assert_eq!(env_file, std::path::PathBuf::from("config.json"));
233                }
234                crate::presentation::cli::input::cli::CreateAction::Template { .. }
235                | crate::presentation::cli::input::cli::CreateAction::Schema { .. } => {
236                    panic!("Expected Environment action")
237                }
238            },
239            Commands::Destroy { .. }
240            | Commands::Provision { .. }
241            | Commands::Configure { .. }
242            | Commands::Test { .. }
243            | Commands::Register { .. }
244            | Commands::Release { .. }
245            | Commands::Run { .. }
246            | Commands::Show { .. }
247            | Commands::List
248            | Commands::Purge { .. }
249            | Commands::Validate { .. }
250            | Commands::Render { .. }
251            | Commands::Exists { .. }
252            | Commands::Docs { .. } => {
253                panic!("Expected Create command")
254            }
255        }
256    }
257
258    #[test]
259    fn it_should_parse_create_environment_with_short_flag() {
260        let args = vec![
261            "torrust-tracker-deployer",
262            "create",
263            "environment",
264            "-f",
265            "env.json",
266        ];
267        let cli = Cli::try_parse_from(args).unwrap();
268
269        match cli.command.unwrap() {
270            Commands::Create { action } => match action {
271                crate::presentation::cli::input::cli::CreateAction::Environment { env_file } => {
272                    assert_eq!(env_file, std::path::PathBuf::from("env.json"));
273                }
274                crate::presentation::cli::input::cli::CreateAction::Template { .. }
275                | crate::presentation::cli::input::cli::CreateAction::Schema { .. } => {
276                    panic!("Expected Environment action")
277                }
278            },
279            Commands::Destroy { .. }
280            | Commands::Provision { .. }
281            | Commands::Configure { .. }
282            | Commands::Test { .. }
283            | Commands::Register { .. }
284            | Commands::Release { .. }
285            | Commands::Run { .. }
286            | Commands::Show { .. }
287            | Commands::List
288            | Commands::Purge { .. }
289            | Commands::Validate { .. }
290            | Commands::Render { .. }
291            | Commands::Exists { .. }
292            | Commands::Docs { .. } => {
293                panic!("Expected Create command")
294            }
295        }
296    }
297
298    #[test]
299    fn it_should_require_env_file_parameter_for_create_environment() {
300        let args = vec!["torrust-tracker-deployer", "create", "environment"];
301        let result = Cli::try_parse_from(args);
302
303        assert!(result.is_err());
304        let error = result.unwrap_err();
305        let error_message = error.to_string();
306        assert!(
307            error_message.contains("required") || error_message.contains("--env-file"),
308            "Error message should indicate missing required --env-file: {error_message}"
309        );
310    }
311
312    #[test]
313    fn it_should_parse_working_dir_global_option_with_create_environment() {
314        let args = vec![
315            "torrust-tracker-deployer",
316            "--working-dir",
317            "/tmp/workspace",
318            "create",
319            "environment",
320            "--env-file",
321            "config.json",
322        ];
323        let cli = Cli::try_parse_from(args).unwrap();
324
325        assert_eq!(
326            cli.global.working_dir,
327            std::path::PathBuf::from("/tmp/workspace")
328        );
329
330        match cli.command.unwrap() {
331            Commands::Create { action } => match action {
332                crate::presentation::cli::input::cli::CreateAction::Environment { env_file } => {
333                    assert_eq!(env_file, std::path::PathBuf::from("config.json"));
334                }
335                crate::presentation::cli::input::cli::CreateAction::Template { .. }
336                | crate::presentation::cli::input::cli::CreateAction::Schema { .. } => {
337                    panic!("Expected Environment action")
338                }
339            },
340            Commands::Destroy { .. }
341            | Commands::Provision { .. }
342            | Commands::Configure { .. }
343            | Commands::Test { .. }
344            | Commands::Register { .. }
345            | Commands::Release { .. }
346            | Commands::Run { .. }
347            | Commands::Show { .. }
348            | Commands::List
349            | Commands::Purge { .. }
350            | Commands::Validate { .. }
351            | Commands::Render { .. }
352            | Commands::Exists { .. }
353            | Commands::Docs { .. } => {
354                panic!("Expected Create command")
355            }
356        }
357    }
358
359    #[test]
360    fn it_should_use_default_working_dir_when_not_specified() {
361        let args = vec![
362            "torrust-tracker-deployer",
363            "create",
364            "environment",
365            "-f",
366            "config.json",
367        ];
368        let cli = Cli::try_parse_from(args).unwrap();
369
370        assert_eq!(cli.global.working_dir, std::path::PathBuf::from("."));
371    }
372
373    #[test]
374    fn it_should_show_create_help() {
375        let args = vec!["torrust-tracker-deployer", "create", "--help"];
376        let result = Cli::try_parse_from(args);
377
378        assert!(result.is_err());
379        let error = result.unwrap_err();
380        assert_eq!(error.kind(), clap::error::ErrorKind::DisplayHelp);
381
382        let help_text = error.to_string();
383        assert!(
384            help_text.contains("environment") || help_text.contains("template"),
385            "Help text should mention subcommands: {help_text}"
386        );
387    }
388
389    #[test]
390    fn it_should_require_provider_for_create_template() {
391        let args = vec!["torrust-tracker-deployer", "create", "template"];
392        let result = Cli::try_parse_from(args);
393
394        assert!(result.is_err());
395        let error = result.unwrap_err();
396        let error_message = error.to_string();
397        assert!(
398            error_message.contains("required") || error_message.contains("--provider"),
399            "Error message should indicate missing required --provider: {error_message}"
400        );
401    }
402
403    #[test]
404    fn it_should_parse_create_template_with_provider() {
405        use crate::domain::provider::Provider;
406
407        let args = vec![
408            "torrust-tracker-deployer",
409            "create",
410            "template",
411            "--provider",
412            "lxd",
413        ];
414        let cli = Cli::try_parse_from(args).unwrap();
415
416        match cli.command.unwrap() {
417            Commands::Create { action } => match action {
418                crate::presentation::cli::input::cli::CreateAction::Template {
419                    output_path,
420                    provider,
421                } => {
422                    assert!(output_path.is_none());
423                    assert_eq!(provider, Provider::Lxd);
424                }
425                crate::presentation::cli::input::cli::CreateAction::Environment { .. }
426                | crate::presentation::cli::input::cli::CreateAction::Schema { .. } => {
427                    panic!("Expected Template action")
428                }
429            },
430            Commands::Destroy { .. }
431            | Commands::Provision { .. }
432            | Commands::Configure { .. }
433            | Commands::Test { .. }
434            | Commands::Register { .. }
435            | Commands::Release { .. }
436            | Commands::Run { .. }
437            | Commands::Show { .. }
438            | Commands::List
439            | Commands::Purge { .. }
440            | Commands::Validate { .. }
441            | Commands::Render { .. }
442            | Commands::Exists { .. }
443            | Commands::Docs { .. } => {
444                panic!("Expected Create command")
445            }
446        }
447    }
448
449    #[test]
450    fn it_should_parse_create_template_with_custom_path() {
451        let args = vec![
452            "torrust-tracker-deployer",
453            "create",
454            "template",
455            "./config/my-env.json",
456            "--provider",
457            "lxd",
458        ];
459        let cli = Cli::try_parse_from(args).unwrap();
460
461        match cli.command.unwrap() {
462            Commands::Create { action } => match action {
463                crate::presentation::cli::input::cli::CreateAction::Template {
464                    output_path,
465                    ..
466                } => {
467                    assert_eq!(
468                        output_path,
469                        Some(std::path::PathBuf::from("./config/my-env.json"))
470                    );
471                }
472                crate::presentation::cli::input::cli::CreateAction::Environment { .. }
473                | crate::presentation::cli::input::cli::CreateAction::Schema { .. } => {
474                    panic!("Expected Template action")
475                }
476            },
477            Commands::Destroy { .. }
478            | Commands::Provision { .. }
479            | Commands::Configure { .. }
480            | Commands::Test { .. }
481            | Commands::Register { .. }
482            | Commands::Release { .. }
483            | Commands::Run { .. }
484            | Commands::Show { .. }
485            | Commands::List
486            | Commands::Purge { .. }
487            | Commands::Validate { .. }
488            | Commands::Render { .. }
489            | Commands::Exists { .. }
490            | Commands::Docs { .. } => {
491                panic!("Expected Create command")
492            }
493        }
494    }
495
496    #[test]
497    fn it_should_parse_create_template_with_provider_lxd() {
498        use crate::domain::provider::Provider;
499
500        let args = vec![
501            "torrust-tracker-deployer",
502            "create",
503            "template",
504            "--provider",
505            "lxd",
506        ];
507        let cli = Cli::try_parse_from(args).unwrap();
508
509        match cli.command.unwrap() {
510            Commands::Create { action } => match action {
511                crate::presentation::cli::input::cli::CreateAction::Template {
512                    provider, ..
513                } => {
514                    assert_eq!(provider, Provider::Lxd);
515                }
516                crate::presentation::cli::input::cli::CreateAction::Environment { .. }
517                | crate::presentation::cli::input::cli::CreateAction::Schema { .. } => {
518                    panic!("Expected Template action")
519                }
520            },
521            _ => panic!("Expected Create command"),
522        }
523    }
524
525    #[test]
526    fn it_should_parse_create_template_with_provider_hetzner() {
527        use crate::domain::provider::Provider;
528
529        let args = vec![
530            "torrust-tracker-deployer",
531            "create",
532            "template",
533            "--provider",
534            "hetzner",
535        ];
536        let cli = Cli::try_parse_from(args).unwrap();
537
538        match cli.command.unwrap() {
539            Commands::Create { action } => match action {
540                crate::presentation::cli::input::cli::CreateAction::Template {
541                    provider, ..
542                } => {
543                    assert_eq!(provider, Provider::Hetzner);
544                }
545                crate::presentation::cli::input::cli::CreateAction::Environment { .. }
546                | crate::presentation::cli::input::cli::CreateAction::Schema { .. } => {
547                    panic!("Expected Template action")
548                }
549            },
550            _ => panic!("Expected Create command"),
551        }
552    }
553
554    #[test]
555    fn it_should_parse_create_template_with_short_provider_flag() {
556        use crate::domain::provider::Provider;
557
558        let args = vec![
559            "torrust-tracker-deployer",
560            "create",
561            "template",
562            "-p",
563            "hetzner",
564        ];
565        let cli = Cli::try_parse_from(args).unwrap();
566
567        match cli.command.unwrap() {
568            Commands::Create { action } => match action {
569                crate::presentation::cli::input::cli::CreateAction::Template {
570                    provider, ..
571                } => {
572                    assert_eq!(provider, Provider::Hetzner);
573                }
574                crate::presentation::cli::input::cli::CreateAction::Environment { .. }
575                | crate::presentation::cli::input::cli::CreateAction::Schema { .. } => {
576                    panic!("Expected Template action")
577                }
578            },
579            _ => panic!("Expected Create command"),
580        }
581    }
582
583    #[test]
584    fn it_should_parse_create_template_with_path_and_provider() {
585        use crate::domain::provider::Provider;
586
587        let args = vec![
588            "torrust-tracker-deployer",
589            "create",
590            "template",
591            "my-config.json",
592            "--provider",
593            "hetzner",
594        ];
595        let cli = Cli::try_parse_from(args).unwrap();
596
597        match cli.command.unwrap() {
598            Commands::Create { action } => match action {
599                crate::presentation::cli::input::cli::CreateAction::Template {
600                    output_path,
601                    provider,
602                } => {
603                    assert_eq!(
604                        output_path,
605                        Some(std::path::PathBuf::from("my-config.json"))
606                    );
607                    assert_eq!(provider, Provider::Hetzner);
608                }
609                crate::presentation::cli::input::cli::CreateAction::Environment { .. }
610                | crate::presentation::cli::input::cli::CreateAction::Schema { .. } => {
611                    panic!("Expected Template action")
612                }
613            },
614            _ => panic!("Expected Create command"),
615        }
616    }
617
618    #[test]
619    fn it_should_show_create_environment_help() {
620        let args = vec![
621            "torrust-tracker-deployer",
622            "create",
623            "environment",
624            "--help",
625        ];
626        let result = Cli::try_parse_from(args);
627
628        assert!(result.is_err());
629        let error = result.unwrap_err();
630        assert_eq!(error.kind(), clap::error::ErrorKind::DisplayHelp);
631
632        let help_text = error.to_string();
633        assert!(
634            help_text.contains("env-file") || help_text.contains("configuration"),
635            "Help text should mention env-file parameter"
636        );
637    }
638
639    #[test]
640    fn it_should_show_create_template_help() {
641        let args = vec!["torrust-tracker-deployer", "create", "template", "--help"];
642        let result = Cli::try_parse_from(args);
643
644        assert!(result.is_err());
645        let error = result.unwrap_err();
646        assert_eq!(error.kind(), clap::error::ErrorKind::DisplayHelp);
647
648        let help_text = error.to_string();
649        assert!(
650            help_text.contains("template") || help_text.contains("placeholder"),
651            "Help text should mention template generation"
652        );
653    }
654
655    #[test]
656    fn it_should_parse_register_subcommand() {
657        let args = vec![
658            "torrust-tracker-deployer",
659            "register",
660            "my-env",
661            "--instance-ip",
662            "192.168.1.100",
663        ];
664        let cli = Cli::try_parse_from(args).unwrap();
665
666        assert!(cli.command.is_some());
667        match cli.command.unwrap() {
668            Commands::Register {
669                environment,
670                instance_ip,
671                ssh_port: _,
672            } => {
673                assert_eq!(environment, "my-env");
674                assert_eq!(instance_ip, "192.168.1.100");
675            }
676            Commands::Create { .. }
677            | Commands::Destroy { .. }
678            | Commands::Provision { .. }
679            | Commands::Configure { .. }
680            | Commands::Test { .. }
681            | Commands::Release { .. }
682            | Commands::Run { .. }
683            | Commands::Show { .. }
684            | Commands::List
685            | Commands::Purge { .. }
686            | Commands::Validate { .. }
687            | Commands::Render { .. }
688            | Commands::Exists { .. }
689            | Commands::Docs { .. } => {
690                panic!("Expected Register command")
691            }
692        }
693    }
694
695    #[test]
696    fn it_should_require_instance_ip_for_register() {
697        let args = vec!["torrust-tracker-deployer", "register", "my-env"];
698        let result = Cli::try_parse_from(args);
699
700        assert!(result.is_err());
701        let error = result.unwrap_err();
702        let error_message = error.to_string();
703        assert!(
704            error_message.contains("required") || error_message.contains("--instance-ip"),
705            "Error message should indicate missing required --instance-ip: {error_message}"
706        );
707    }
708
709    #[test]
710    fn it_should_show_register_help() {
711        let args = vec!["torrust-tracker-deployer", "register", "--help"];
712        let result = Cli::try_parse_from(args);
713
714        assert!(result.is_err());
715        let error = result.unwrap_err();
716        assert_eq!(error.kind(), clap::error::ErrorKind::DisplayHelp);
717
718        let help_text = error.to_string();
719        assert!(
720            help_text.contains("instance-ip") || help_text.contains("existing"),
721            "Help text should mention instance-ip parameter"
722        );
723    }
724}