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
218 changes: 218 additions & 0 deletions src/domain/config/environment_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,100 @@ impl EnvironmentCreationConfig {

Ok((environment_name, ssh_credentials, ssh_port))
}

/// Creates a template instance with placeholder values
///
/// This method generates a configuration template with placeholder values
/// that users can replace with their actual configuration. The template
/// structure matches the `EnvironmentCreationConfig` exactly, ensuring
/// type safety and automatic synchronization with struct changes.
///
/// # Examples
///
/// ```rust
/// use torrust_tracker_deployer_lib::domain::config::EnvironmentCreationConfig;
///
/// let template = EnvironmentCreationConfig::template();
/// assert_eq!(template.environment.name, "REPLACE_WITH_ENVIRONMENT_NAME");
/// ```
#[must_use]
pub fn template() -> Self {
Self {
environment: EnvironmentSection {
name: "REPLACE_WITH_ENVIRONMENT_NAME".to_string(),
},
ssh_credentials: SshCredentialsConfig {
private_key_path: "REPLACE_WITH_SSH_PRIVATE_KEY_PATH".to_string(),
public_key_path: "REPLACE_WITH_SSH_PUBLIC_KEY_PATH".to_string(),
username: "torrust".to_string(), // default value
port: 22, // default value
},
}
}

/// Generates a configuration template file at the specified path
///
/// This method creates a JSON configuration file with placeholder values
/// that users can edit. The file is formatted with pretty-printing for
/// better readability.
///
/// # Arguments
///
/// * `path` - Path where the template file should be created
///
/// # Returns
///
/// * `Ok(())` - Template file created successfully
/// * `Err(CreateConfigError)` - File creation or serialization failed
///
/// # Errors
///
/// Returns an error if:
/// - Parent directory cannot be created
/// - Template serialization fails (unlikely - indicates a bug)
/// - File cannot be written due to permissions or I/O errors
///
/// # Examples
///
/// ```rust,no_run
/// use torrust_tracker_deployer_lib::domain::config::EnvironmentCreationConfig;
/// use std::path::Path;
///
/// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
/// EnvironmentCreationConfig::generate_template_file(
/// Path::new("./environment-config.json")
/// ).await?;
/// # Ok(())
/// # }
/// ```
pub async fn generate_template_file(path: &std::path::Path) -> Result<(), CreateConfigError> {
// Create template instance with placeholders
let template = Self::template();

// Serialize to pretty-printed JSON
let json = serde_json::to_string_pretty(&template)
.map_err(|source| CreateConfigError::TemplateSerializationFailed { source })?;

// Create parent directories if they don't exist
if let Some(parent) = path.parent() {
tokio::fs::create_dir_all(parent).await.map_err(|source| {
CreateConfigError::TemplateDirectoryCreationFailed {
path: parent.to_path_buf(),
source,
}
})?;
}

// Write template to file
tokio::fs::write(path, json).await.map_err(|source| {
CreateConfigError::TemplateFileWriteFailed {
path: path.to_path_buf(),
source,
}
})?;

Ok(())
}
}

#[cfg(test)]
Expand Down Expand Up @@ -419,4 +513,128 @@ mod tests {

assert_eq!(original, deserialized);
}

#[test]
fn test_template_has_placeholder_values() {
let template = EnvironmentCreationConfig::template();

assert_eq!(template.environment.name, "REPLACE_WITH_ENVIRONMENT_NAME");
assert_eq!(
template.ssh_credentials.private_key_path,
"REPLACE_WITH_SSH_PRIVATE_KEY_PATH"
);
assert_eq!(
template.ssh_credentials.public_key_path,
"REPLACE_WITH_SSH_PUBLIC_KEY_PATH"
);
assert_eq!(template.ssh_credentials.username, "torrust");
assert_eq!(template.ssh_credentials.port, 22);
}

#[test]
fn test_template_serializes_to_valid_json() {
let template = EnvironmentCreationConfig::template();
let json = serde_json::to_string_pretty(&template).unwrap();

// Verify it can be deserialized back
let deserialized: EnvironmentCreationConfig = serde_json::from_str(&json).unwrap();
assert_eq!(template, deserialized);
}

#[test]
fn test_template_structure_matches_config() {
let template = EnvironmentCreationConfig::template();

// Verify template has same structure as regular config
let regular_config = EnvironmentCreationConfig::new(
EnvironmentSection {
name: "test".to_string(),
},
SshCredentialsConfig::new(
"path1".to_string(),
"path2".to_string(),
"user".to_string(),
22,
),
);

// Both should serialize to same structure (different values)
let template_json = serde_json::to_value(&template).unwrap();
let config_json = serde_json::to_value(&regular_config).unwrap();

// Check structure matches
assert!(template_json.is_object());
assert!(config_json.is_object());

let template_obj = template_json.as_object().unwrap();
let config_obj = config_json.as_object().unwrap();

assert_eq!(template_obj.keys().len(), config_obj.keys().len());
assert!(template_obj.contains_key("environment"));
assert!(template_obj.contains_key("ssh_credentials"));
}

#[tokio::test]
async fn test_generate_template_file() {
use tempfile::TempDir;

let temp_dir = TempDir::new().unwrap();
let template_path = temp_dir.path().join("config.json");

let result = EnvironmentCreationConfig::generate_template_file(&template_path).await;
assert!(result.is_ok());

// Verify file exists
assert!(template_path.exists());

// Verify content is valid JSON
let content = std::fs::read_to_string(&template_path).unwrap();
let parsed: EnvironmentCreationConfig = serde_json::from_str(&content).unwrap();

// Verify placeholders are present
assert_eq!(parsed.environment.name, "REPLACE_WITH_ENVIRONMENT_NAME");
assert_eq!(
parsed.ssh_credentials.private_key_path,
"REPLACE_WITH_SSH_PRIVATE_KEY_PATH"
);
}

#[tokio::test]
async fn test_generate_template_file_creates_parent_directories() {
use tempfile::TempDir;

let temp_dir = TempDir::new().unwrap();
let nested_path = temp_dir
.path()
.join("configs")
.join("env")
.join("test.json");

let result = EnvironmentCreationConfig::generate_template_file(&nested_path).await;
assert!(result.is_ok());

// Verify nested directories were created
assert!(nested_path.exists());
assert!(nested_path.parent().unwrap().exists());
}

#[tokio::test]
async fn test_generate_template_file_overwrites_existing() {
use tempfile::TempDir;

let temp_dir = TempDir::new().unwrap();
let template_path = temp_dir.path().join("config.json");

// Create initial file
std::fs::write(&template_path, "old content").unwrap();

// Generate template should overwrite
let result = EnvironmentCreationConfig::generate_template_file(&template_path).await;
assert!(result.is_ok());

// Verify content was replaced
let content = std::fs::read_to_string(&template_path).unwrap();
assert!(content.contains("REPLACE_WITH_ENVIRONMENT_NAME"));
assert!(!content.contains("old content"));
}
}
108 changes: 108 additions & 0 deletions src/domain/config/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,29 @@ pub enum CreateConfigError {
/// Invalid SSH port (must be 1-65535)
#[error("Invalid SSH port: {port} (must be between 1 and 65535)")]
InvalidPort { port: u16 },

/// Failed to serialize configuration template to JSON
#[error("Failed to serialize configuration template to JSON")]
TemplateSerializationFailed {
#[source]
source: serde_json::Error,
},

/// Failed to create parent directory for template file
#[error("Failed to create directory: {path}")]
TemplateDirectoryCreationFailed {
path: PathBuf,
#[source]
source: std::io::Error,
},

/// Failed to write template file
#[error("Failed to write template file: {path}")]
TemplateFileWriteFailed {
path: PathBuf,
#[source]
source: std::io::Error,
},
}

impl CreateConfigError {
Expand All @@ -59,6 +82,7 @@ impl CreateConfigError {
/// assert!(help.contains("Check that the file path is correct"));
/// ```
#[must_use]
#[allow(clippy::too_many_lines)]
pub fn help(&self) -> &'static str {
match self {
Self::InvalidEnvironmentName(_) => {
Expand Down Expand Up @@ -129,6 +153,51 @@ impl CreateConfigError {
\n\
Fix: Update the SSH port in your configuration to a valid port number (1-65535)."
}
Self::TemplateSerializationFailed { .. } => {
"Template serialization failed.\n\
\n\
This indicates an internal error in template generation.\n\
\n\
Common causes:\n\
- Software bug in template generation logic\n\
- Invalid data structure for JSON serialization\n\
\n\
Fix:\n\
1. Report this issue with full error details\n\
2. Check for application updates\n\
\n\
This is likely a software bug that needs to be reported."
}
Self::TemplateDirectoryCreationFailed { .. } => {
"Failed to create directory for template file.\n\
\n\
Common causes:\n\
- Insufficient permissions to create directory\n\
- No disk space available\n\
- A file exists with the same name as the directory\n\
- Path length exceeds system limits\n\
\n\
Fix:\n\
1. Check write permissions for the parent directory\n\
2. Verify disk space is available: df -h\n\
3. Ensure no file exists with the same name as the directory\n\
4. Try using a shorter path"
}
Self::TemplateFileWriteFailed { .. } => {
"Failed to write template file.\n\
\n\
Common causes:\n\
- Insufficient permissions to write file\n\
- No disk space available\n\
- File is open in another application\n\
- Antivirus software blocking file creation\n\
\n\
Fix:\n\
1. Check write permissions for the target file and directory\n\
2. Verify disk space is available: df -h\n\
3. Ensure the file is not open in another application\n\
4. Check if antivirus software is blocking file creation"
}
}
}
}
Expand Down Expand Up @@ -214,4 +283,43 @@ mod tests {
);
}
}

#[test]
fn test_template_serialization_failed_error() {
// Simulate serialization error (hard to create naturally)
let json_error = serde_json::from_str::<serde_json::Value>("invalid").unwrap_err();
let error = CreateConfigError::TemplateSerializationFailed { source: json_error };

assert!(error
.to_string()
.contains("serialize configuration template"));
assert!(error.help().contains("internal error"));
assert!(error.help().contains("Report this issue"));
}

#[test]
fn test_template_directory_creation_failed_error() {
let error = CreateConfigError::TemplateDirectoryCreationFailed {
path: PathBuf::from("/test/path"),
source: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "test"),
};

assert!(error.to_string().contains("Failed to create directory"));
assert!(error.to_string().contains("/test/path"));
assert!(error.help().contains("permissions"));
assert!(error.help().contains("df -h"));
}

#[test]
fn test_template_file_write_failed_error() {
let error = CreateConfigError::TemplateFileWriteFailed {
path: PathBuf::from("/test/file.json"),
source: std::io::Error::new(std::io::ErrorKind::PermissionDenied, "test"),
};

assert!(error.to_string().contains("Failed to write template file"));
assert!(error.to_string().contains("/test/file.json"));
assert!(error.help().contains("permissions"));
assert!(error.help().contains("disk space"));
}
}