Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions src/presentation/commands/destroy.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use crate::presentation::user_output::{UserOutput, VerbosityLevel};
/// # Arguments
///
/// * `environment_name` - The name of the environment to destroy
/// * `working_dir` - Root directory for environment data storage
///
/// # Returns
///
Expand All @@ -44,14 +45,15 @@ use crate::presentation::user_output::{UserOutput, VerbosityLevel};
///
/// ```rust
/// use torrust_tracker_deployer_lib::presentation::commands::destroy;
/// use std::path::Path;
///
/// if let Err(e) = destroy::handle("test-env") {
/// if let Err(e) = destroy::handle("test-env", Path::new(".")) {
/// eprintln!("Destroy failed: {e}");
/// eprintln!("Help: {}", e.help());
/// }
/// ```
#[allow(clippy::result_large_err)] // Error contains detailed context for user guidance
pub fn handle(environment_name: &str) -> Result<(), DestroyError> {
pub fn handle(environment_name: &str, working_dir: &std::path::Path) -> Result<(), DestroyError> {
// Create user output with default stdout/stderr channels
let mut output = UserOutput::new(VerbosityLevel::Normal);

Expand All @@ -70,7 +72,7 @@ pub fn handle(environment_name: &str) -> Result<(), DestroyError> {

// Create repository for loading environment state
let repository_factory = RepositoryFactory::new(Duration::from_secs(30));
let repository = repository_factory.create(std::path::PathBuf::from("data"));
let repository = repository_factory.create(working_dir.to_path_buf());

// Create clock for timing information
let clock = std::sync::Arc::new(crate::shared::SystemClock);
Expand Down
2 changes: 1 addition & 1 deletion src/presentation/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ pub fn execute(command: Commands, working_dir: &std::path::Path) -> Result<(), C
Ok(())
}
Commands::Destroy { environment } => {
destroy::handle(&environment)?;
destroy::handle(&environment, working_dir)?;
Ok(())
} // Future commands will be added here:
//
Expand Down
210 changes: 210 additions & 0 deletions tests/e2e_destroy_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,210 @@
//! End-to-End Black Box Tests for Destroy Command
//!
//! This test suite provides true black-box testing of the destroy command
//! by running the production application as an external process. These tests
//! verify that the destroy command correctly handles the working directory
//! parameter, ensuring environments can be destroyed from custom locations.
//!
//! ## Test Approach
//!
//! - **Black Box**: Runs production binary as external process
//! - **Isolation**: Uses temporary directories for complete test isolation
//! - **Coverage**: Tests complete workflow from environment creation to destruction
//! - **Verification**: Validates environment is properly removed
//!
//! ## Test Scenarios
//!
//! 1. Default working directory: Destroy environment from current directory
//! 2. Custom working directory: Destroy environment from temporary directory
//! 3. Full lifecycle: Create → Destroy with custom working directory

mod support;

use support::{EnvironmentStateAssertions, ProcessRunner, TempWorkspace};

/// Helper function to create a test environment configuration
fn create_test_environment_config(env_name: &str) -> String {
// Use absolute paths to SSH keys to ensure they work regardless of current directory
let project_root = env!("CARGO_MANIFEST_DIR");
let private_key_path = format!("{project_root}/fixtures/testing_rsa");
let public_key_path = format!("{project_root}/fixtures/testing_rsa.pub");

serde_json::json!({
"environment": {
"name": env_name
},
"ssh_credentials": {
"private_key_path": private_key_path,
"public_key_path": public_key_path,
"username": "torrust",
"port": 22
}
})
.to_string()
}

#[test]
fn it_should_destroy_environment_with_default_working_directory() {
// Arrange: Create temporary workspace
let temp_workspace = TempWorkspace::new().expect("Failed to create temp workspace");

// Create environment configuration file
let config = create_test_environment_config("test-destroy-default");
temp_workspace
.write_config_file("environment.json", &config)
.expect("Failed to write config file");

// Create environment in default location
let create_result = ProcessRunner::new()
.working_dir(temp_workspace.path())
.run_create_command("./environment.json")
.expect("Failed to run create command");

assert!(
create_result.success(),
"Create command failed: {}",
create_result.stderr()
);

// Verify environment was created
let env_assertions = EnvironmentStateAssertions::new(temp_workspace.path());
env_assertions.assert_environment_exists("test-destroy-default");

// Act: Destroy environment using destroy command
let destroy_result = ProcessRunner::new()
.working_dir(temp_workspace.path())
.run_destroy_command("test-destroy-default")
.expect("Failed to run destroy command");

// Assert: Verify command succeeded
assert!(
destroy_result.success(),
"Destroy command failed with exit code: {:?}\nstderr: {}",
destroy_result.exit_code(),
destroy_result.stderr()
);

// Assert: Verify environment was transitioned to Destroyed state
let env_assertions = EnvironmentStateAssertions::new(temp_workspace.path());
env_assertions.assert_environment_exists("test-destroy-default");
env_assertions.assert_environment_state_is("test-destroy-default", "Destroyed");
}

#[test]
fn it_should_destroy_environment_with_custom_working_directory() {
// Arrange: Create temporary workspace
let temp_workspace = TempWorkspace::new().expect("Failed to create temp workspace");

// Create environment configuration file
let config = create_test_environment_config("test-destroy-custom");
temp_workspace
.write_config_file("environment.json", &config)
.expect("Failed to write config file");

// Create environment in custom location
let create_result = ProcessRunner::new()
.working_dir(temp_workspace.path())
.run_create_command("./environment.json")
.expect("Failed to run create command");

assert!(
create_result.success(),
"Create command failed: {}",
create_result.stderr()
);

// Verify environment was created in custom location
let env_assertions = EnvironmentStateAssertions::new(temp_workspace.path());
env_assertions.assert_environment_exists("test-destroy-custom");

// Act: Destroy environment using same working directory
let destroy_result = ProcessRunner::new()
.working_dir(temp_workspace.path())
.run_destroy_command("test-destroy-custom")
.expect("Failed to run destroy command");

// Assert: Verify command succeeded
assert!(
destroy_result.success(),
"Destroy command failed with exit code: {:?}\nstderr: {}",
destroy_result.exit_code(),
destroy_result.stderr()
);

// Assert: Verify environment was transitioned to Destroyed state in custom location
let env_assertions = EnvironmentStateAssertions::new(temp_workspace.path());
env_assertions.assert_environment_exists("test-destroy-custom");
env_assertions.assert_environment_state_is("test-destroy-custom", "Destroyed");
}

#[test]
fn it_should_fail_when_environment_not_found_in_working_directory() {
// Arrange: Create temporary workspace
let temp_workspace = TempWorkspace::new().expect("Failed to create temp workspace");

// Act: Try to destroy non-existent environment
let destroy_result = ProcessRunner::new()
.working_dir(temp_workspace.path())
.run_destroy_command("nonexistent-environment")
.expect("Failed to run destroy command");

// Assert: Command should fail
assert!(
!destroy_result.success(),
"Command should have failed when environment doesn't exist"
);

// Verify error message mentions environment not found
let stderr = destroy_result.stderr();
assert!(
stderr.contains("not found") || stderr.contains("does not exist"),
"Error message should mention environment not found, got: {stderr}"
);
}

#[test]
fn it_should_complete_full_lifecycle_with_custom_working_directory() {
// Arrange: Create temporary workspace
let temp_workspace = TempWorkspace::new().expect("Failed to create temp workspace");

// Create environment configuration file
let config = create_test_environment_config("test-lifecycle");
temp_workspace
.write_config_file("environment.json", &config)
.expect("Failed to write config file");

// Act: Create environment in custom location
let create_result = ProcessRunner::new()
.working_dir(temp_workspace.path())
.run_create_command("./environment.json")
.expect("Failed to run create command");

assert!(
create_result.success(),
"Create command failed: {}",
create_result.stderr()
);

// Verify environment exists
let env_assertions = EnvironmentStateAssertions::new(temp_workspace.path());
env_assertions.assert_environment_exists("test-lifecycle");
env_assertions.assert_environment_state_is("test-lifecycle", "Created");

// Act: Destroy environment
let destroy_result = ProcessRunner::new()
.working_dir(temp_workspace.path())
.run_destroy_command("test-lifecycle")
.expect("Failed to run destroy command");

// Assert: Both commands succeed
assert!(
destroy_result.success(),
"Destroy command failed: {}",
destroy_result.stderr()
);

// Assert: Environment is transitioned to Destroyed state
let env_assertions = EnvironmentStateAssertions::new(temp_workspace.path());
env_assertions.assert_environment_exists("test-lifecycle");
env_assertions.assert_environment_state_is("test-lifecycle", "Destroyed");
}
1 change: 1 addition & 0 deletions tests/support/assertions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ impl EnvironmentStateAssertions {
/// # Panics
///
/// Panics if the data directory or environment JSON file doesn't exist.
#[allow(dead_code)]
pub fn assert_data_directory_structure(&self, env_name: &str) {
let data_dir = self.workspace_path.join(env_name);
assert!(
Expand Down
32 changes: 32 additions & 0 deletions tests/support/process_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,38 @@ impl ProcessRunner {

Ok(ProcessResult::new(output))
}

/// Run the destroy command with the production binary
///
/// This method runs `cargo run -- destroy <environment_name>` with
/// optional working directory for the application itself via `--working-dir`.
///
/// # Errors
///
/// Returns an error if the command fails to execute.
#[allow(dead_code)]
pub fn run_destroy_command(&self, environment_name: &str) -> Result<ProcessResult> {
let mut cmd = Command::new("cargo");

if let Some(working_dir) = &self.working_dir {
// Build command with working directory
cmd.args([
"run",
"--",
"destroy",
environment_name,
"--working-dir",
working_dir.to_str().unwrap(),
]);
} else {
// No working directory, use relative paths
cmd.args(["run", "--", "destroy", environment_name]);
}

let output = cmd.output().context("Failed to execute destroy command")?;

Ok(ProcessResult::new(output))
}
}

impl Default for ProcessRunner {
Expand Down
1 change: 1 addition & 0 deletions tests/support/temp_workspace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ impl TempWorkspace {
/// # Errors
///
/// Returns an error if the file cannot be written.
#[allow(dead_code)]
pub fn write_file(&self, filename: &str, content: &str) -> Result<()> {
let file_path = self.temp_dir.path().join(filename);
fs::write(file_path, content)?;
Expand Down
Loading