22//!
33//! **Purpose**: Smoke test for running Torrust Tracker services
44//!
5- //! This handler validates that a deployed Tracker application is running and accessible.
6- //! The command is designed for post-deployment verification - checking that services
7- //! respond correctly to requests, not validating infrastructure components .
5+ //! This handler validates that a deployed Tracker application is running and accessible
6+ //! from external clients. The command performs comprehensive end-to-end verification
7+ //! including service status, health checks, and external accessibility validation .
88//!
9- //! **Current Implementation Status**: Work in Progress / Temporary Scaffolding
9+ //! ## Validation Strategy
1010//!
11- //! The current validation steps (cloud-init, Docker, Docker Compose) are **temporary
12- //! scaffolding** that exist only because the complete deployment workflow is not yet
13- //! implemented. These steps will be **removed** when the full deployment is implemented
14- //! and replaced with actual smoke tests.
11+ //! The test command validates deployed services through:
1512//!
16- //! **Target Implementation** (when `Running` state is implemented):
13+ //! 1. **Docker Compose Service Status** - Verifies containers are running
14+ //! 2. **External Health Checks** - Tests service accessibility from outside the VM:
15+ //! - Tracker API health endpoint (required): `http://<vm-ip>:<api-port>/api/health_check`
16+ //! - HTTP Tracker health endpoint (optional): `http://<vm-ip>:<tracker-port>/api/health_check`
1717//!
18- //! - Make HTTP requests to publicly exposed Tracker services
19- //! - Verify services respond correctly (health checks, basic API calls)
20- //! - Confirm deployment is production-ready from end-user perspective
18+ //! ## Why External-Only Validation?
19+ //!
20+ //! We perform external accessibility checks (from test runner to VM) rather than
21+ //! internal checks (via SSH to localhost) because:
22+ //! - External checks are a superset of internal checks
23+ //! - If services are accessible externally, they must be running internally
24+ //! - External checks validate firewall configuration automatically
25+ //! - Simpler test implementation reduces maintenance burden
26+ //!
27+ //! ## Port Configuration
28+ //!
29+ //! The test command extracts tracker ports from the environment's tracker configuration:
30+ //! - HTTP API port from `environment.context.user_inputs.tracker.http_api.bind_address`
31+ //! - HTTP Tracker port from `environment.context.user_inputs.tracker.http_trackers[0].bind_address`
2132//!
2233//! For rationale and alternatives, see:
2334//! - `docs/decisions/test-command-as-smoke-test.md` - Architectural decision record
@@ -28,33 +39,29 @@ use tracing::{info, instrument};
2839
2940use super :: errors:: TestCommandHandlerError ;
3041use crate :: adapters:: ssh:: SshConfig ;
31- use crate :: application:: steps:: {
32- ValidateCloudInitCompletionStep , ValidateDockerComposeInstallationStep ,
33- ValidateDockerInstallationStep ,
34- } ;
3542use crate :: domain:: environment:: repository:: { EnvironmentRepository , TypedEnvironmentRepository } ;
3643use crate :: domain:: EnvironmentName ;
44+ use crate :: infrastructure:: remote_actions:: { RemoteAction , RunningServicesValidator } ;
3745
3846/// `TestCommandHandler` orchestrates smoke testing for running Torrust Tracker services
3947///
4048/// **Purpose**: Post-deployment smoke test to verify the application is running and accessible
4149///
42- /// **Current Status**: Work in Progress - Current implementation is temporary scaffolding
50+ /// This handler validates that deployed services are operational and accessible from
51+ /// external clients by performing comprehensive health checks on the Tracker API and
52+ /// HTTP Tracker endpoints.
4353///
44- /// The current validation steps are **placeholders** until the complete deployment workflow
45- /// is implemented with the `Running` state. See module documentation for details.
54+ /// ## Validation Steps
4655///
47- /// ## Current Validation Steps (Temporary)
56+ /// 1. **Service Status** - Verifies Docker Compose services are running via SSH
57+ /// 2. **Tracker API Health** (required) - Tests external accessibility of HTTP API
58+ /// 3. **HTTP Tracker Health** (optional) - Tests external accessibility of HTTP tracker
4859///
49- /// 1. Validate cloud-init completion
50- /// 2. Validate Docker installation
51- /// 3. Validate Docker Compose installation
60+ /// ## Port Discovery
5261///
53- /// ## Target Validation Steps (Future)
54- ///
55- /// 1. HTTP health check to Tracker service
56- /// 2. Basic API request verification
57- /// 3. Metrics endpoint validation
62+ /// The handler extracts tracker ports from the environment's tracker configuration:
63+ /// - HTTP API port from `tracker.http_api.bind_address`
64+ /// - HTTP Tracker port from `tracker.http_trackers[0].bind_address`
5865///
5966/// ## Design Rationale
6067///
@@ -80,6 +87,9 @@ impl TestCommandHandler {
8087
8188 /// Execute the complete testing and validation workflow
8289 ///
90+ /// Validates that the Torrust Tracker services are running and accessible by
91+ /// performing external health checks on the deployed services.
92+ ///
8393 /// # Arguments
8494 ///
8595 /// * `env_name` - The name of the environment to test
@@ -89,10 +99,11 @@ impl TestCommandHandler {
8999 /// Returns an error if:
90100 /// * Environment not found
91101 /// * Environment does not have an instance IP set
92- /// * Any validation step fails:
93- /// - Cloud-init completion validation fails
94- /// - Docker installation validation fails
95- /// - Docker Compose installation validation fails
102+ /// * Tracker configuration is invalid or missing required ports
103+ /// * Running services validation fails:
104+ /// - Services are not running
105+ /// - Health check endpoints are not accessible
106+ /// - Firewall rules block external access
96107 #[ instrument(
97108 name = "test_command" ,
98109 skip_all,
@@ -111,31 +122,54 @@ impl TestCommandHandler {
111122 environment_name : env_name. to_string ( ) ,
112123 } ) ?;
113124
125+ // Extract tracker ports from configuration
126+ let tracker_config = any_env. tracker_config ( ) ;
127+
128+ // Get HTTP API port from bind_address (e.g., "0.0.0.0:1212" -> 1212)
129+ let tracker_api_port =
130+ Self :: extract_port_from_bind_address ( & tracker_config. http_api . bind_address )
131+ . ok_or_else ( || TestCommandHandlerError :: InvalidTrackerConfiguration {
132+ message : format ! (
133+ "Invalid HTTP API bind_address: {}. Expected format: 'host:port'" ,
134+ tracker_config. http_api. bind_address
135+ ) ,
136+ } ) ?;
137+
138+ // Get HTTP Tracker port from first HTTP tracker (optional)
139+ let http_tracker_port = tracker_config
140+ . http_trackers
141+ . first ( )
142+ . and_then ( |tracker| Self :: extract_port_from_bind_address ( & tracker. bind_address ) ) ;
143+
114144 let ssh_config =
115145 SshConfig :: with_default_port ( any_env. ssh_credentials ( ) . clone ( ) , instance_ip) ;
116146
117- ValidateCloudInitCompletionStep :: new ( ssh_config . clone ( ) )
118- . execute ( )
119- . await ? ;
147+ // Validate running services with external accessibility checks
148+ let services_validator =
149+ RunningServicesValidator :: new ( ssh_config , tracker_api_port , http_tracker_port ) ;
120150
121- ValidateDockerInstallationStep :: new ( ssh_config. clone ( ) )
122- . execute ( )
123- . await ?;
124-
125- ValidateDockerComposeInstallationStep :: new ( ssh_config)
126- . execute ( )
127- . await ?;
151+ services_validator. execute ( & instance_ip) . await ?;
128152
129153 info ! (
130154 command = "test" ,
131155 environment = %env_name,
132156 instance_ip = ?instance_ip,
133- "Infrastructure testing workflow completed successfully"
157+ tracker_api_port = tracker_api_port,
158+ http_tracker_port = ?http_tracker_port,
159+ "Service testing workflow completed successfully"
134160 ) ;
135161
136162 Ok ( ( ) )
137163 }
138164
165+ /// Extract port number from bind_address string (e.g., "0.0.0.0:1212" -> Some(1212))
166+ fn extract_port_from_bind_address ( bind_address : & str ) -> Option < u16 > {
167+ bind_address
168+ . split ( ':' )
169+ . nth ( 1 )
170+ . and_then ( |port_str| port_str. parse :: < u16 > ( ) . ok ( ) )
171+ }
172+
139173 /// Load environment from storage
140174 ///
141175 /// # Errors
0 commit comments