Skip to main content

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

1//! Configure Command Handler
2//!
3//! This module handles the configure 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::ConfigureCommandHandler;
12use crate::domain::environment::name::EnvironmentName;
13use crate::domain::environment::repository::EnvironmentRepository;
14use crate::domain::environment::state::Configured;
15use crate::domain::environment::Environment;
16use crate::presentation::cli::input::cli::OutputFormat;
17use crate::presentation::cli::views::commands::configure::{
18    ConfigureDetailsData, JsonView, TextView,
19};
20use crate::presentation::cli::views::progress::ProgressReporter;
21use crate::presentation::cli::views::progress::VerboseProgressListener;
22use crate::presentation::cli::views::Render;
23use crate::presentation::cli::views::UserOutput;
24use crate::shared::clock::Clock;
25
26use super::errors::ConfigureSubcommandError;
27
28/// Steps in the configure workflow
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30enum ConfigureStep {
31    ValidateEnvironment,
32    CreateCommandHandler,
33    ConfigureInfrastructure,
34}
35
36impl ConfigureStep {
37    /// All steps in execution order
38    const ALL: &'static [Self] = &[
39        Self::ValidateEnvironment,
40        Self::CreateCommandHandler,
41        Self::ConfigureInfrastructure,
42    ];
43
44    /// Total number of steps
45    const fn count() -> usize {
46        Self::ALL.len()
47    }
48
49    /// User-facing description for the step
50    fn description(self) -> &'static str {
51        match self {
52            Self::ValidateEnvironment => "Validating environment",
53            Self::CreateCommandHandler => "Creating command handler",
54            Self::ConfigureInfrastructure => "Configuring infrastructure",
55        }
56    }
57}
58
59/// Presentation layer controller for configure command workflow
60///
61/// Coordinates user interaction, progress reporting, and input validation
62/// before delegating to the application layer `ConfigureCommandHandler`.
63///
64/// # Responsibilities
65///
66/// - Validate user input (environment name format)
67/// - Show progress updates to the user
68/// - Format success/error messages for display
69/// - Delegate business logic to application layer
70///
71/// # Architecture
72///
73/// This controller sits in the presentation layer and handles all user-facing
74/// concerns. It delegates actual business logic to the application layer's
75/// `ConfigureCommandHandler`, maintaining clear separation of concerns.
76pub struct ConfigureCommandController {
77    repository: Arc<dyn EnvironmentRepository + Send + Sync>,
78    clock: Arc<dyn Clock>,
79    progress: ProgressReporter,
80}
81
82impl ConfigureCommandController {
83    /// Create a new configure command controller
84    ///
85    /// Creates a `ConfigureCommandController` with direct service injection.
86    /// This follows the single container architecture pattern.
87    #[allow(clippy::needless_pass_by_value)] // Constructor takes ownership of Arc parameters
88    pub fn new(
89        repository: Arc<dyn EnvironmentRepository + Send + Sync>,
90        clock: Arc<dyn Clock>,
91        user_output: Arc<ReentrantMutex<RefCell<UserOutput>>>,
92    ) -> Self {
93        let progress = ProgressReporter::new(user_output, ConfigureStep::count());
94
95        Self {
96            repository,
97            clock,
98            progress,
99        }
100    }
101
102    /// Execute the complete configure workflow
103    ///
104    /// Orchestrates all steps of the configure command:
105    /// 1. Validate environment name
106    /// 2. Load and validate environment state
107    /// 3. Create command handler
108    /// 4. Configure infrastructure
109    /// 5. Display results (in specified format)
110    /// 6. Complete with success message
111    ///
112    /// # Arguments
113    ///
114    /// * `environment_name` - The name of the environment to configure
115    /// * `output_format` - The output format (Text or Json)
116    ///
117    /// # Errors
118    ///
119    /// Returns an error if:
120    /// - Environment name is invalid (format validation fails)
121    /// - Environment cannot be loaded from repository
122    /// - Environment is not in "Provisioned" state
123    /// - Infrastructure configuration fails
124    /// - Progress reporting encounters a poisoned mutex
125    ///
126    /// # Returns
127    ///
128    /// Returns `Ok(Environment<Configured>)` on success, or a `ConfigureSubcommandError` if any step fails.
129    #[allow(clippy::result_large_err)]
130    pub fn execute(
131        &mut self,
132        environment_name: &str,
133        output_format: OutputFormat,
134    ) -> Result<Environment<Configured>, ConfigureSubcommandError> {
135        let env_name = self.validate_environment_name(environment_name)?;
136
137        let handler = self.create_command_handler()?;
138
139        let configured = self.configure_infrastructure(&handler, &env_name)?;
140
141        self.complete_workflow(environment_name)?;
142
143        self.display_configure_results(&configured, output_format)?;
144
145        Ok(configured)
146    }
147
148    /// Validate the environment name format
149    ///
150    /// Shows progress to user and validates that the environment name
151    /// meets domain requirements (1-63 chars, alphanumeric + hyphens).
152    #[allow(clippy::result_large_err)]
153    fn validate_environment_name(
154        &mut self,
155        name: &str,
156    ) -> Result<EnvironmentName, ConfigureSubcommandError> {
157        self.progress
158            .start_step(ConfigureStep::ValidateEnvironment.description())?;
159
160        let env_name = EnvironmentName::new(name.to_string()).map_err(|source| {
161            ConfigureSubcommandError::InvalidEnvironmentName {
162                name: name.to_string(),
163                source,
164            }
165        })?;
166
167        self.progress
168            .complete_step(Some(&format!("Environment name validated: {name}")))?;
169
170        Ok(env_name)
171    }
172
173    /// Create application layer command handler
174    ///
175    /// Creates the application layer command handler with all required
176    /// dependencies (repository, clock).
177    #[allow(clippy::result_large_err)]
178    fn create_command_handler(
179        &mut self,
180    ) -> Result<ConfigureCommandHandler, ConfigureSubcommandError> {
181        self.progress
182            .start_step(ConfigureStep::CreateCommandHandler.description())?;
183
184        let handler = ConfigureCommandHandler::new(self.clock.clone(), self.repository.clone());
185        self.progress.complete_step(None)?;
186
187        Ok(handler)
188    }
189
190    /// Execute infrastructure configuration via application layer
191    ///
192    /// Delegates to the application layer `ConfigureCommandHandler` to
193    /// orchestrate the actual infrastructure configuration workflow.
194    ///
195    /// The application layer handles:
196    /// - Loading the environment from repository
197    /// - Validating the environment state (must be Provisioned)
198    /// - Complete configuration workflow
199    /// - State transitions and persistence
200    #[allow(clippy::result_large_err)]
201    fn configure_infrastructure(
202        &mut self,
203        handler: &ConfigureCommandHandler,
204        env_name: &EnvironmentName,
205    ) -> Result<Environment<Configured>, ConfigureSubcommandError> {
206        self.progress
207            .start_step(ConfigureStep::ConfigureInfrastructure.description())?;
208
209        // Create the listener for verbose progress reporting.
210        // The VerboseProgressListener translates step events into
211        // user-facing detail messages via UserOutput's verbosity filter.
212        let listener = VerboseProgressListener::new(self.progress.output().clone());
213
214        let configured = handler
215            .execute(env_name, Some(&listener))
216            .map_err(
217                |source| ConfigureSubcommandError::ConfigureOperationFailed {
218                    name: env_name.to_string(),
219                    source: Box::new(source),
220                },
221            )?;
222
223        self.progress
224            .complete_step(Some("Infrastructure configured"))?;
225        Ok(configured)
226    }
227
228    /// Complete the workflow with success message
229    ///
230    /// Shows final success message to the user with workflow summary.
231    #[allow(clippy::result_large_err)]
232    fn complete_workflow(&mut self, name: &str) -> Result<(), ConfigureSubcommandError> {
233        self.progress
234            .complete(&format!("Environment '{name}' configured successfully"))?;
235        Ok(())
236    }
237
238    /// Display configure results in the specified format
239    ///
240    /// Uses the Strategy Pattern to render configure details in either
241    /// human-readable text or machine-readable JSON format.
242    ///
243    /// # Arguments
244    ///
245    /// * `configured` - The configured environment to display
246    /// * `output_format` - The output format (Text or Json)
247    ///
248    /// # Errors
249    ///
250    /// Returns an error if:
251    /// - Progress reporting encounters a poisoned mutex
252    ///
253    /// # Note
254    ///
255    /// JSON serialization errors are propagated as `ConfigureSubcommandError::OutputFormatting`.
256    #[allow(clippy::result_large_err)]
257    fn display_configure_results(
258        &mut self,
259        configured: &Environment<Configured>,
260        output_format: OutputFormat,
261    ) -> Result<(), ConfigureSubcommandError> {
262        self.progress.blank_line()?;
263        let details = ConfigureDetailsData::from(configured);
264        let output = match output_format {
265            OutputFormat::Text => TextView::render(&details)?,
266            OutputFormat::Json => JsonView::render(&details)?,
267        };
268        self.progress.result(&output)?;
269        Ok(())
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use crate::presentation::cli::controllers::constants::DEFAULT_LOCK_TIMEOUT;
277    use crate::presentation::cli::views::testing::TestUserOutput;
278    use crate::presentation::cli::views::VerbosityLevel;
279    use crate::shared::SystemClock;
280
281    /// Create test dependencies for configure command handler tests
282    ///
283    /// Returns the common dependencies needed for testing `handle_configure_command`:
284    /// - `user_output`: `ReentrantMutex`-wrapped `UserOutput` for thread-safe access
285    /// - `repository`: Environment repository for persistence
286    /// - `clock`: System clock for timing operations
287    #[allow(clippy::type_complexity)] // Test helper with complex but clear types
288    fn create_test_dependencies(
289        temp_dir: &tempfile::TempDir,
290    ) -> (
291        Arc<ReentrantMutex<RefCell<UserOutput>>>,
292        Arc<dyn EnvironmentRepository + Send + Sync>,
293        Arc<dyn Clock>,
294    ) {
295        use crate::infrastructure::persistence::file_repository_factory::FileRepositoryFactory;
296        let (user_output, _, _) =
297            TestUserOutput::new(VerbosityLevel::Normal).into_reentrant_wrapped();
298        let data_dir = temp_dir.path().join("data");
299        let file_repository_factory = FileRepositoryFactory::new(DEFAULT_LOCK_TIMEOUT);
300        let repository = file_repository_factory.create(data_dir);
301        let clock = Arc::new(SystemClock);
302
303        (user_output, repository, clock)
304    }
305
306    #[tokio::test]
307    async fn it_should_return_error_for_invalid_environment_name() {
308        use tempfile::TempDir;
309
310        let temp_dir = TempDir::new().unwrap();
311
312        let (user_output, repository, clock) = create_test_dependencies(&temp_dir);
313
314        // Test with invalid environment name (contains underscore)
315        let result = ConfigureCommandController::new(repository, clock, user_output.clone())
316            .execute("invalid_name", OutputFormat::Text);
317
318        assert!(result.is_err());
319        match result.unwrap_err() {
320            ConfigureSubcommandError::InvalidEnvironmentName { name, .. } => {
321                assert_eq!(name, "invalid_name");
322            }
323            other => panic!("Expected InvalidEnvironmentName, got: {other:?}"),
324        }
325    }
326
327    #[tokio::test]
328    async fn it_should_return_error_for_empty_environment_name() {
329        use tempfile::TempDir;
330
331        let temp_dir = TempDir::new().unwrap();
332
333        let (user_output, repository, clock) = create_test_dependencies(&temp_dir);
334
335        let result = ConfigureCommandController::new(repository, clock, user_output.clone())
336            .execute("", OutputFormat::Text);
337
338        assert!(result.is_err());
339        match result.unwrap_err() {
340            ConfigureSubcommandError::InvalidEnvironmentName { name, .. } => {
341                assert_eq!(name, "");
342            }
343            other => panic!("Expected InvalidEnvironmentName, got: {other:?}"),
344        }
345    }
346
347    #[tokio::test]
348    async fn it_should_return_error_for_nonexistent_environment() {
349        use tempfile::TempDir;
350
351        let temp_dir = TempDir::new().unwrap();
352
353        let (user_output, repository, clock) = create_test_dependencies(&temp_dir);
354
355        // Try to configure an environment that doesn't exist
356        let result = ConfigureCommandController::new(repository, clock, user_output.clone())
357            .execute("nonexistent-env", OutputFormat::Text);
358
359        assert!(result.is_err());
360        // After refactoring, repository NotFound error is wrapped in ConfigureOperationFailed
361        match result.unwrap_err() {
362            ConfigureSubcommandError::ConfigureOperationFailed { name, .. } => {
363                assert_eq!(name, "nonexistent-env");
364            }
365            other => panic!("Expected ConfigureOperationFailed, got: {other:?}"),
366        }
367    }
368
369    #[tokio::test]
370    async fn it_should_accept_valid_environment_name() {
371        use std::fs;
372        use tempfile::TempDir;
373
374        let temp_dir = TempDir::new().unwrap();
375        let working_dir = temp_dir.path();
376
377        let (user_output, repository, clock) = create_test_dependencies(&temp_dir);
378
379        // Create a mock environment directory to test validation
380        let env_dir = working_dir.join("test-env");
381        fs::create_dir_all(&env_dir).unwrap();
382
383        // Valid environment name should pass validation, but will fail
384        // at configure operation since we don't have a real environment setup
385        let result = ConfigureCommandController::new(repository, clock, user_output.clone())
386            .execute("test-env", OutputFormat::Text);
387
388        // Should fail at operation, not at name validation
389        if let Err(ConfigureSubcommandError::InvalidEnvironmentName { .. }) = result {
390            panic!("Should not fail at name validation for 'test-env'");
391        }
392        // Expected - valid name but operation fails or other errors acceptable in test context
393    }
394}