torrust_tracker_deployer_lib/presentation/cli/input/cli/
mod.rs1use clap::Parser;
8
9pub 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#[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)] pub struct Cli {
29 #[command(flatten)]
31 pub global: GlobalArgs,
32
33 #[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 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 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 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 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 assert!(result.is_err());
206 let error = result.unwrap_err();
207 assert_eq!(error.kind(), clap::error::ErrorKind::DisplayHelp);
208
209 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}