use terraform_wrapper::commands::apply::ApplyCommand;
use terraform_wrapper::commands::destroy::DestroyCommand;
use terraform_wrapper::commands::fmt::FmtCommand;
use terraform_wrapper::commands::init::InitCommand;
use terraform_wrapper::commands::output::{OutputCommand, OutputResult};
use terraform_wrapper::commands::plan::PlanCommand;
use terraform_wrapper::commands::show::{ShowCommand, ShowResult};
use terraform_wrapper::commands::state::StateCommand;
use terraform_wrapper::commands::validate::ValidateCommand;
use terraform_wrapper::commands::workspace::WorkspaceCommand;
use terraform_wrapper::{Terraform, TerraformCommand};
fn setup_terraform(dir: &std::path::Path) -> Option<Terraform> {
Terraform::builder().working_dir(dir).build().ok()
}
fn write_null_config(dir: &std::path::Path) {
let main_tf = r#"
terraform {
required_providers {
null = {
source = "hashicorp/null"
version = "~> 3.0"
}
}
}
resource "null_resource" "example" {
triggers = {
value = var.trigger_value
}
}
variable "trigger_value" {
default = "hello"
}
output "trigger" {
value = var.trigger_value
}
output "id" {
value = null_resource.example.id
}
"#;
std::fs::write(dir.join("main.tf"), main_tf).unwrap();
}
#[tokio::test]
async fn init_plan_apply_output_destroy() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let Some(tf) = setup_terraform(dir) else {
eprintln!("terraform not found, skipping test");
return;
};
write_null_config(dir);
let init_output = InitCommand::new().execute(&tf).await.unwrap();
assert!(init_output.success);
let plan_output = PlanCommand::new()
.out("tfplan")
.detailed_exitcode()
.execute(&tf)
.await
.unwrap();
assert_eq!(plan_output.exit_code, 2);
let apply_output = ApplyCommand::new()
.plan_file("tfplan")
.execute(&tf)
.await
.unwrap();
assert!(apply_output.success);
let result = OutputCommand::new().json().execute(&tf).await.unwrap();
match result {
OutputResult::Json(ref outputs) => {
assert!(outputs.contains_key("trigger"));
assert!(outputs.contains_key("id"));
assert_eq!(
outputs["trigger"].value,
serde_json::Value::String("hello".into())
);
}
_ => panic!("expected Json variant"),
}
let result = OutputCommand::new()
.name("trigger")
.raw()
.execute(&tf)
.await
.unwrap();
match result {
OutputResult::Raw(ref value) => {
assert_eq!(value, "hello");
}
_ => panic!("expected Raw variant"),
}
let plan_output = PlanCommand::new().execute(&tf).await.unwrap();
assert_eq!(plan_output.exit_code, 0);
let destroy_output = DestroyCommand::new()
.auto_approve()
.execute(&tf)
.await
.unwrap();
assert!(destroy_output.success);
}
#[tokio::test]
async fn init_with_upgrade() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let Some(tf) = setup_terraform(dir) else {
eprintln!("terraform not found, skipping test");
return;
};
write_null_config(dir);
InitCommand::new().execute(&tf).await.unwrap();
let output = InitCommand::new().upgrade().execute(&tf).await.unwrap();
assert!(output.success);
}
#[tokio::test]
async fn apply_with_var_override() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let Some(tf) = setup_terraform(dir) else {
eprintln!("terraform not found, skipping test");
return;
};
write_null_config(dir);
InitCommand::new().execute(&tf).await.unwrap();
ApplyCommand::new()
.auto_approve()
.var("trigger_value", "custom")
.execute(&tf)
.await
.unwrap();
let result = OutputCommand::new()
.name("trigger")
.raw()
.execute(&tf)
.await
.unwrap();
match result {
OutputResult::Raw(ref value) => assert_eq!(value, "custom"),
_ => panic!("expected Raw variant"),
}
DestroyCommand::new()
.auto_approve()
.execute(&tf)
.await
.unwrap();
}
#[tokio::test]
async fn validate_valid_config() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let Some(tf) = setup_terraform(dir) else {
eprintln!("terraform not found, skipping test");
return;
};
write_null_config(dir);
InitCommand::new().execute(&tf).await.unwrap();
let result = ValidateCommand::new().execute(&tf).await.unwrap();
assert!(result.valid);
assert_eq!(result.error_count, 0);
}
#[tokio::test]
async fn validate_invalid_config() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let Some(tf) = setup_terraform(dir) else {
eprintln!("terraform not found, skipping test");
return;
};
let bad_tf = r#"
output "bad" {
value = nonexistent_resource.foo.id
}
"#;
std::fs::write(dir.join("main.tf"), bad_tf).unwrap();
let result = ValidateCommand::new().execute(&tf).await.unwrap();
assert!(!result.valid);
assert!(result.error_count > 0);
assert!(!result.diagnostics.is_empty());
}
#[tokio::test]
async fn show_current_state() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let Some(tf) = setup_terraform(dir) else {
eprintln!("terraform not found, skipping test");
return;
};
write_null_config(dir);
InitCommand::new().execute(&tf).await.unwrap();
ApplyCommand::new()
.auto_approve()
.execute(&tf)
.await
.unwrap();
let result = ShowCommand::new().execute(&tf).await.unwrap();
match result {
ShowResult::State(state) => {
assert_eq!(state.format_version, "1.0");
assert!(!state.terraform_version.is_empty());
assert_eq!(state.values.root_module.resources.len(), 1);
assert_eq!(
state.values.root_module.resources[0].address,
"null_resource.example"
);
assert!(state.values.outputs.contains_key("trigger"));
}
_ => panic!("expected State variant"),
}
DestroyCommand::new()
.auto_approve()
.execute(&tf)
.await
.unwrap();
}
#[tokio::test]
async fn show_saved_plan() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let Some(tf) = setup_terraform(dir) else {
eprintln!("terraform not found, skipping test");
return;
};
write_null_config(dir);
InitCommand::new().execute(&tf).await.unwrap();
PlanCommand::new().out("tfplan").execute(&tf).await.unwrap();
let result = ShowCommand::new()
.plan_file("tfplan")
.execute(&tf)
.await
.unwrap();
match result {
ShowResult::Plan(plan) => {
assert!(!plan.terraform_version.is_empty());
assert_eq!(plan.resource_changes.len(), 1);
assert_eq!(plan.resource_changes[0].address, "null_resource.example");
assert_eq!(plan.resource_changes[0].change.actions, vec!["create"]);
assert!(plan.applyable);
}
_ => panic!("expected Plan variant"),
}
}
#[tokio::test]
async fn fmt_check_formatted() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let Some(tf) = setup_terraform(dir) else {
eprintln!("terraform not found, skipping test");
return;
};
write_null_config(dir);
let output = FmtCommand::new().check().execute(&tf).await.unwrap();
assert_eq!(output.exit_code, 0);
}
#[tokio::test]
async fn fmt_check_unformatted() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let Some(tf) = setup_terraform(dir) else {
eprintln!("terraform not found, skipping test");
return;
};
let ugly_tf = "resource\"null_resource\"\"x\"{\n}\n";
std::fs::write(dir.join("main.tf"), ugly_tf).unwrap();
let output = FmtCommand::new().check().execute(&tf).await.unwrap();
assert_eq!(output.exit_code, 3);
}
#[tokio::test]
async fn workspace_lifecycle() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let Some(tf) = setup_terraform(dir) else {
eprintln!("terraform not found, skipping test");
return;
};
write_null_config(dir);
InitCommand::new().execute(&tf).await.unwrap();
let output = WorkspaceCommand::show().execute(&tf).await.unwrap();
assert_eq!(output.stdout.trim(), "default");
WorkspaceCommand::new_workspace("test-ws")
.execute(&tf)
.await
.unwrap();
let output = WorkspaceCommand::show().execute(&tf).await.unwrap();
assert_eq!(output.stdout.trim(), "test-ws");
let output = WorkspaceCommand::list().execute(&tf).await.unwrap();
assert!(output.stdout.contains("default"));
assert!(output.stdout.contains("test-ws"));
WorkspaceCommand::select("default")
.execute(&tf)
.await
.unwrap();
WorkspaceCommand::delete("test-ws")
.execute(&tf)
.await
.unwrap();
}
#[tokio::test]
async fn state_list_and_show() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let Some(tf) = setup_terraform(dir) else {
eprintln!("terraform not found, skipping test");
return;
};
write_null_config(dir);
InitCommand::new().execute(&tf).await.unwrap();
ApplyCommand::new()
.auto_approve()
.execute(&tf)
.await
.unwrap();
let output = StateCommand::list().execute(&tf).await.unwrap();
assert!(output.stdout.contains("null_resource.example"));
let output = StateCommand::show("null_resource.example")
.execute(&tf)
.await
.unwrap();
assert!(output.stdout.contains("null_resource.example"));
DestroyCommand::new()
.auto_approve()
.execute(&tf)
.await
.unwrap();
}
#[tokio::test]
async fn timeout_triggers_error() {
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let tf = match Terraform::builder()
.working_dir(dir)
.timeout(std::time::Duration::from_nanos(1))
.build()
{
Ok(tf) => tf,
Err(_) => {
eprintln!("terraform not found, skipping test");
return;
}
};
write_null_config(dir);
let result = InitCommand::new().execute(&tf).await;
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
matches!(err, terraform_wrapper::Error::Timeout { .. }),
"expected Timeout error, got: {err:?}"
);
}
#[tokio::test]
async fn with_working_dir_override() {
let tmp1 = tempfile::tempdir().unwrap();
let tmp2 = tempfile::tempdir().unwrap();
let Some(tf) = setup_terraform(tmp1.path()) else {
eprintln!("terraform not found, skipping test");
return;
};
write_null_config(tmp2.path());
let tf2 = tf.with_working_dir(tmp2.path());
let output = InitCommand::new().execute(&tf2).await.unwrap();
assert!(output.success);
let result = ValidateCommand::new().no_json().execute(&tf).await;
assert!(result.is_err()); }
#[tokio::test]
async fn streaming_apply() {
use terraform_wrapper::streaming::{JsonLogLine, stream_terraform};
let tmp = tempfile::tempdir().unwrap();
let dir = tmp.path();
let Some(tf) = setup_terraform(dir) else {
eprintln!("terraform not found, skipping test");
return;
};
write_null_config(dir);
InitCommand::new().execute(&tf).await.unwrap();
let mut events: Vec<JsonLogLine> = Vec::new();
let result = stream_terraform(
&tf,
ApplyCommand::new().auto_approve().json(),
&[0],
|line| {
events.push(line);
},
)
.await
.unwrap();
assert!(result.success);
assert!(!events.is_empty());
let types: Vec<&str> = events.iter().map(|e| e.log_type.as_str()).collect();
assert!(types.contains(&"version"));
assert!(types.contains(&"apply_complete"));
assert!(types.contains(&"change_summary"));
DestroyCommand::new()
.auto_approve()
.execute(&tf)
.await
.unwrap();
}
#[cfg(feature = "config")]
#[tokio::test]
async fn config_builder_lifecycle() {
use terraform_wrapper::config::TerraformConfig;
let config = TerraformConfig::new()
.required_provider("null", "hashicorp/null", "~> 3.0")
.resource(
"null_resource",
"example",
serde_json::json!({ "triggers": { "v": "1" } }),
)
.output(
"id",
serde_json::json!({ "value": "${null_resource.example.id}" }),
);
let dir = config.write_to_tempdir().unwrap();
let tf = match Terraform::builder().working_dir(dir.path()).build() {
Ok(tf) => tf,
Err(_) => {
eprintln!("terraform not found, skipping test");
return;
}
};
InitCommand::new().execute(&tf).await.unwrap();
ApplyCommand::new()
.auto_approve()
.execute(&tf)
.await
.unwrap();
let result = OutputCommand::new()
.name("id")
.raw()
.execute(&tf)
.await
.unwrap();
match result {
OutputResult::Raw(ref id) => assert!(!id.is_empty()),
_ => panic!("expected Raw variant"),
}
DestroyCommand::new()
.auto_approve()
.execute(&tf)
.await
.unwrap();
}