Skip to main content

torrust_tracker_deployer_lib/presentation/cli/controllers/test/
handler.rs

1//! Test Command Handler
2//!
3//! This module handles the test command execution at the presentation layer,
4//! including environment validation, repository initialization, and user interaction.
5
6use std::cell::RefCell;
7use std::sync::Arc;
8
9use parking_lot::ReentrantMutex;
10
11use crate::application::command_handlers::test::result::TestResult;
12use crate::application::command_handlers::TestCommandHandler;
13use crate::domain::environment::name::EnvironmentName;
14use crate::domain::environment::repository::EnvironmentRepository;
15use crate::presentation::cli::input::cli::OutputFormat;
16use crate::presentation::cli::views::commands::test::{JsonView, TestResultData, TextView};
17use crate::presentation::cli::views::progress::ProgressReporter;
18use crate::presentation::cli::views::Render;
19use crate::presentation::cli::views::UserOutput;
20
21use super::errors::TestSubcommandError;
22
23/// Steps in the test workflow
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25enum TestStep {
26    ValidateEnvironment,
27    CreateCommandHandler,
28    TestInfrastructure,
29}
30
31impl TestStep {
32    /// All steps in execution order
33    const ALL: &'static [Self] = &[
34        Self::ValidateEnvironment,
35        Self::CreateCommandHandler,
36        Self::TestInfrastructure,
37    ];
38
39    /// Total number of steps
40    const fn count() -> usize {
41        Self::ALL.len()
42    }
43
44    /// User-facing description for the step
45    fn description(self) -> &'static str {
46        match self {
47            Self::ValidateEnvironment => "Validating environment",
48            Self::CreateCommandHandler => "Creating command handler",
49            Self::TestInfrastructure => "Testing infrastructure",
50        }
51    }
52}
53
54/// Presentation layer controller for test command workflow
55///
56/// Coordinates user interaction, progress reporting, and input validation
57/// while delegating business logic to the application layer's `TestCommandHandler`.
58///
59/// ## Responsibilities
60///
61/// - Validate environment name format
62/// - Create and invoke the application layer `TestCommandHandler`
63/// - Report progress to the user (4 steps)
64/// - Format success/error messages with actionable guidance
65///
66/// ## Architecture
67///
68/// This controller **only orchestrates the workflow** - all validation logic
69/// is implemented in the `TestCommandHandler` at the application layer.
70///
71/// The `TestCommandHandler.execute()` method performs all infrastructure validation:
72/// - Cloud-init completion check
73/// - Docker installation verification
74/// - Docker Compose installation verification
75pub struct TestCommandController {
76    repository: Arc<dyn EnvironmentRepository>,
77    progress: ProgressReporter,
78}
79
80impl TestCommandController {
81    /// Create a new `TestCommandController` with dependencies
82    ///
83    /// # Arguments
84    ///
85    /// * `working_dir` - Working directory containing the data folder
86    /// * `repository` - Environment repository with Send + Sync bounds
87    /// * `user_output` - Shared output service for user feedback
88    #[allow(clippy::needless_pass_by_value)] // Arc parameters are moved to constructor for ownership
89    pub fn new(
90        repository: Arc<dyn EnvironmentRepository + Send + Sync>,
91        user_output: Arc<ReentrantMutex<RefCell<UserOutput>>>,
92    ) -> Self {
93        let progress = ProgressReporter::new(user_output, TestStep::count());
94
95        Self {
96            repository,
97            progress,
98        }
99    }
100
101    /// Execute the complete test workflow
102    ///
103    /// This method orchestrates the four-step workflow:
104    /// 1. Validate environment name
105    /// 2. Create command handler
106    /// 3. Execute validation workflow via application layer
107    /// 4. Complete workflow and display success message
108    ///
109    /// # Arguments
110    ///
111    /// * `environment_name` - Name of the environment to test
112    ///
113    /// # Errors
114    ///
115    /// Returns `TestSubcommandError` if any step fails
116    pub async fn execute(
117        &mut self,
118        environment_name: &str,
119        output_format: OutputFormat,
120    ) -> Result<(), TestSubcommandError> {
121        // 1. Validate environment name
122        let env_name = self.validate_environment_name(environment_name)?;
123
124        // 2. Create command handler
125        let handler = self.create_command_handler()?;
126
127        // 3. Execute validation workflow via application layer
128        let result = self.fixture_infrastructure(&handler, &env_name).await?;
129
130        // 4. Complete workflow with rendered output
131        self.complete_workflow(environment_name, &result, output_format)?;
132
133        Ok(())
134    }
135
136    /// Step 1: Validate environment name format
137    ///
138    /// # Errors
139    ///
140    /// Returns `TestSubcommandError::InvalidEnvironmentName` if validation fails
141    fn validate_environment_name(
142        &mut self,
143        name: &str,
144    ) -> Result<EnvironmentName, TestSubcommandError> {
145        self.progress
146            .start_step(TestStep::ValidateEnvironment.description())?;
147
148        let env_name = EnvironmentName::new(name.to_string()).map_err(|source| {
149            TestSubcommandError::InvalidEnvironmentName {
150                name: name.to_string(),
151                source,
152            }
153        })?;
154
155        self.progress
156            .complete_step(Some(&format!("Environment name validated: {name}")))?;
157
158        Ok(env_name)
159    }
160
161    /// Step 2: Create the application layer command handler
162    ///
163    /// # Errors
164    ///
165    /// Returns `TestSubcommandError::ProgressReportingFailed` if progress reporting fails
166    fn create_command_handler(&mut self) -> Result<TestCommandHandler, TestSubcommandError> {
167        self.progress
168            .start_step(TestStep::CreateCommandHandler.description())?;
169
170        let handler = TestCommandHandler::new(self.repository.clone());
171        self.progress.complete_step(None)?;
172
173        Ok(handler)
174    }
175
176    /// Step 3: Execute infrastructure validation tests
177    ///
178    /// Delegates all validation logic to the application layer `TestCommandHandler`.
179    /// The handler returns a structured `TestResult` containing DNS warnings
180    /// which are rendered here in the presentation layer.
181    ///
182    /// # Errors
183    ///
184    /// Returns `TestSubcommandError::ValidationFailed` if any validation check fails
185    async fn fixture_infrastructure(
186        &mut self,
187        handler: &TestCommandHandler,
188        env_name: &EnvironmentName,
189    ) -> Result<TestResult, TestSubcommandError> {
190        self.progress
191            .start_step(TestStep::TestInfrastructure.description())?;
192
193        let result = handler.execute(env_name).await.map_err(|source| {
194            TestSubcommandError::ValidationFailed {
195                name: env_name.to_string(),
196                source: Box::new(source),
197            }
198        })?;
199
200        // Render advisory DNS warnings from the test result
201        for warning in &result.dns_warnings {
202            self.progress
203                .output()
204                .lock()
205                .borrow_mut()
206                .warn(&format!("DNS check: {warning}"));
207        }
208
209        let step_message = if result.has_dns_warnings() {
210            "Infrastructure tests passed (with DNS warnings)"
211        } else {
212            "Infrastructure tests passed"
213        };
214
215        self.progress.complete_step(Some(step_message))?;
216
217        Ok(result)
218    }
219
220    /// Step 4: Complete workflow and display success message
221    ///
222    /// # Errors
223    ///
224    /// Returns `TestSubcommandError::ProgressReportingFailed` if progress reporting fails
225    fn complete_workflow(
226        &mut self,
227        environment_name: &str,
228        result: &TestResult,
229        output_format: OutputFormat,
230    ) -> Result<(), TestSubcommandError> {
231        let data = TestResultData::new(environment_name, result);
232
233        let output = match output_format {
234            OutputFormat::Text => TextView::render(&data)?,
235            OutputFormat::Json => JsonView::render(&data)?,
236        };
237
238        self.progress.result(&output)?;
239
240        Ok(())
241    }
242}