Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
169 changes: 169 additions & 0 deletions tests/e2e_create_command.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
//! End-to-End Black Box Tests for Create Command
//!
//! This test suite provides true black-box testing of the create command
//! by running the production application as an external process. Unlike
//! other E2E tests that mock infrastructure components, these tests exercise
//! the complete application workflow from configuration file to persisted
//! environment state.
//!
//! ## Test Approach
//!
//! - **Black Box**: Runs production binary as external process
//! - **Isolation**: Uses temporary directories for complete test isolation
//! - **Coverage**: Tests complete workflow from config file to persistence
//! - **Verification**: Validates environment state in data directory
//!
//! ## Test Scenarios
//!
//! 1. Happy path: Create environment from valid config file
//! 2. Invalid config: Graceful failure with validation errors
//! 3. Missing config file: Appropriate error when file not found
//! 4. Duplicate detection: Error when environment already exists

mod support;

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

#[test]
fn it_should_create_environment_from_config_file_black_box() {
// 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-environment");
temp_workspace
.write_config_file("environment.json", &config)
.expect("Failed to write config file");

// Act: Run production application as external process
let result = ProcessRunner::new()
.working_dir(temp_workspace.path())
.run_create_command("./environment.json")
.expect("Failed to run create command");

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

// Assert: Verify environment state was persisted
let env_assertions = EnvironmentStateAssertions::new(temp_workspace.path());
env_assertions.assert_environment_exists("test-environment");
env_assertions.assert_environment_state_is("test-environment", "Created");
env_assertions.assert_data_directory_structure("test-environment");
// Note: traces directory is created on-demand, not during environment creation
}

#[test]
fn it_should_fail_gracefully_with_invalid_config() {
let temp_workspace = TempWorkspace::new().expect("Failed to create temp workspace");

// Create invalid configuration (missing required fields)
let invalid_config = r#"{"invalid": "config"}"#;
temp_workspace
.write_file("invalid.json", invalid_config)
.expect("Failed to write invalid config");

// Run command and expect failure
let result = ProcessRunner::new()
.working_dir(temp_workspace.path())
.run_create_command("./invalid.json")
.expect("Failed to run create command");

// Assert command failed with helpful error message
assert!(
!result.success(),
"Command should have failed with invalid config"
);

// Verify error message mentions configuration validation
let stderr = result.stderr();
assert!(
stderr.contains("missing field") || stderr.contains("Configuration"),
"Error message should mention configuration issues, got: {stderr}"
);
}

#[test]
fn it_should_fail_when_config_file_not_found() {
let temp_workspace = TempWorkspace::new().expect("Failed to create temp workspace");

// Run command with non-existent config file
let result = ProcessRunner::new()
.working_dir(temp_workspace.path())
.run_create_command("./nonexistent.json")
.expect("Failed to run create command");

// Assert command failed with file not found error
assert!(
!result.success(),
"Command should have failed with missing file"
);

// Verify error message mentions file not found
let stderr = result.stderr();
assert!(
stderr.contains("not found") || stderr.contains("No such file"),
"Error message should mention file not found, got: {stderr}"
);
}

#[test]
fn it_should_fail_when_environment_already_exists() {
let temp_workspace = TempWorkspace::new().expect("Failed to create temp workspace");

let config = create_test_environment_config("duplicate-env");
temp_workspace
.write_config_file("config.json", &config)
.expect("Failed to write config");

// Create environment first time
let result1 = ProcessRunner::new()
.working_dir(temp_workspace.path())
.run_create_command("./config.json")
.expect("Failed to run create command");

assert!(
result1.success(),
"First create should succeed, stderr: {}",
result1.stderr()
);

// Try to create same environment again
let result2 = ProcessRunner::new()
.working_dir(temp_workspace.path())
.run_create_command("./config.json")
.expect("Failed to run create command");

// Assert second create failed
assert!(
!result2.success(),
"Second create should fail with duplicate environment"
);

// Verify error message mentions duplicate or already exists
let stderr = result2.stderr();
assert!(
stderr.contains("Already Exists")
|| stderr.contains("already exists")
|| stderr.contains("AlreadyExists"),
"Error message should mention environment already exists, got: {stderr}"
);
}

/// Helper function to create a test environment configuration
fn create_test_environment_config(env_name: &str) -> String {
serde_json::json!({
"environment": {
"name": env_name
},
"ssh_credentials": {
"private_key_path": "fixtures/testing_rsa",
"public_key_path": "fixtures/testing_rsa.pub"
}
})
.to_string()
}
129 changes: 129 additions & 0 deletions tests/support/assertions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
//! Environment State Assertions
//!
//! Provides assertion utilities for verifying environment state after
//! command execution in black-box tests.

use anyhow::{Context, Result};
use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};

/// Assertions for verifying environment state after command execution
///
/// This struct provides methods to verify that the environment was created
/// correctly and that all expected files and directories exist with the
/// correct structure and content.
pub struct EnvironmentStateAssertions {
workspace_path: PathBuf,
}

impl EnvironmentStateAssertions {
/// Create a new assertions helper for the given workspace
#[must_use]
pub fn new<P: AsRef<Path>>(workspace_path: P) -> Self {
Self {
workspace_path: workspace_path.as_ref().to_path_buf(),
}
}

/// Assert that the environment exists
///
/// Verifies that the environment state file exists at the expected location.
///
/// # Panics
///
/// Panics if the environment file does not exist.
pub fn assert_environment_exists(&self, env_name: &str) {
let env_file_path = self.environment_json_path(env_name);
assert!(
env_file_path.exists(),
"Environment file should exist at: {}",
env_file_path.display()
);
}

/// Assert that the environment is in the expected state
///
/// Verifies that the environment state matches the expected state string.
/// The state is determined by the top-level key in the environment JSON.
///
/// # Panics
///
/// Panics if the environment JSON cannot be read or if the state doesn't match.
pub fn assert_environment_state_is(&self, env_name: &str, expected_state: &str) {
let env_data = self
.read_environment_json(env_name)
.expect("Failed to read environment JSON");

// Parse the environment state structure
let state_key = env_data
.as_object()
.expect("Environment JSON should be an object")
.keys()
.next()
.expect("Environment should have a state key");

assert_eq!(
state_key, expected_state,
"Environment state should be '{expected_state}', but was '{state_key}'"
);
}

/// Assert that the data directory structure exists
///
/// Verifies that the environment's data directory and required files exist.
///
/// # Panics
///
/// Panics if the data directory or environment JSON file doesn't exist.
pub fn assert_data_directory_structure(&self, env_name: &str) {
let data_dir = self.workspace_path.join(env_name);
assert!(
data_dir.exists(),
"Data directory should exist at: {}",
data_dir.display()
);

let env_json = data_dir.join("environment.json");
assert!(
env_json.exists(),
"Environment JSON should exist at: {}",
env_json.display()
);
}

/// Assert that the trace directory exists
///
/// Verifies that the environment's traces directory exists for observability.
///
/// # Panics
///
/// Panics if the traces directory doesn't exist.
#[allow(dead_code)]
pub fn assert_trace_directory_exists(&self, env_name: &str) {
let traces_dir = self.workspace_path.join(env_name).join("traces");

assert!(
traces_dir.exists(),
"Traces directory should exist at: {}",
traces_dir.display()
);
}

fn environment_json_path(&self, env_name: &str) -> PathBuf {
self.workspace_path.join(env_name).join("environment.json")
}

fn read_environment_json(&self, env_name: &str) -> Result<Value> {
let env_file_path = self.environment_json_path(env_name);
let content = fs::read_to_string(&env_file_path).context(format!(
"Failed to read environment file: {}",
env_file_path.display()
))?;

let json: Value =
serde_json::from_str(&content).context("Failed to parse environment JSON")?;

Ok(json)
}
}
13 changes: 13 additions & 0 deletions tests/support/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
//! Test Support Infrastructure
//!
//! This module provides reusable test utilities for black-box E2E testing.
//! It includes temporary workspace management, external process execution,
//! and environment state assertions.

mod assertions;
mod process_runner;
mod temp_workspace;

pub use assertions::EnvironmentStateAssertions;
pub use process_runner::ProcessRunner;
pub use temp_workspace::TempWorkspace;
Loading