- Interactive deploy command with 8-step walkthrough: framework → provider → token → SSH → server → inference → tailscale → discord - .env file generation from walkthrough config - DeploymentConfig struct with framework-aware defaults - Inference API client with validation for Venice, OpenRouter, OpenAI, Anthropic - Hetzner Cloud provider: token validation, SSH key listing - DotEnv parser/writer with schema validation - Destroy command with confirmation prompt - Validation subcommand for checking existing .env files - All tests passing, go vet clean
258 lines
No EOL
6.6 KiB
Go
258 lines
No EOL
6.6 KiB
Go
package destroy
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func TestListResourcesFromState(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
want []Resource
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "empty state",
|
|
content: `{}`,
|
|
want: []Resource{},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "state with resources",
|
|
content: `{"resources":[{"address":"hcloud_server.main","type":"hcloud_server","name":"main"},{"address":"hcloud_volume.data","type":"hcloud_volume","name":"data"}]}`,
|
|
want: []Resource{
|
|
{Address: "hcloud_server.main", Type: "hcloud_server", Name: "main"},
|
|
{Address: "hcloud_volume.data", Type: "hcloud_volume", Name: "data"},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "state with module resources",
|
|
content: `{"resources":[{"address":"hcloud_server.main","type":"hcloud_server","name":"main","module":"module.agent"},{"address":"null_resource.provisioner","type":"null_resource","name":"provisioner"}]}`,
|
|
want: []Resource{
|
|
{Address: "module.agent.hcloud_server.main", Type: "hcloud_server", Name: "main"},
|
|
{Address: "null_resource.provisioner", Type: "null_resource", Name: "provisioner"},
|
|
},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create temp file
|
|
tmpDir := t.TempDir()
|
|
statePath := filepath.Join(tmpDir, "terraform.tfstate")
|
|
if err := os.WriteFile(statePath, []byte(tt.content), 0644); err != nil {
|
|
t.Fatalf("failed to write state file: %v", err)
|
|
}
|
|
|
|
got, err := listResourcesFromState(statePath)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("listResourcesFromState() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if len(got) != len(tt.want) {
|
|
t.Errorf("listResourcesFromState() got %d resources, want %d", len(got), len(tt.want))
|
|
return
|
|
}
|
|
for i, r := range got {
|
|
if r.Address != tt.want[i].Address {
|
|
t.Errorf("resource[%d].Address = %s, want %s", i, r.Address, tt.want[i].Address)
|
|
}
|
|
if r.Type != tt.want[i].Type {
|
|
t.Errorf("resource[%d].Type = %s, want %s", i, r.Type, tt.want[i].Type)
|
|
}
|
|
if r.Name != tt.want[i].Name {
|
|
t.Errorf("resource[%d].Name = %s, want %s", i, r.Name, tt.want[i].Name)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestListResourcesFromStateNonExistent(t *testing.T) {
|
|
_, err := listResourcesFromState("/nonexistent/path/state")
|
|
if err == nil {
|
|
t.Error("expected error for non-existent file")
|
|
}
|
|
}
|
|
|
|
func TestLoadEnvFile(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
content string
|
|
want []string
|
|
wantErr bool
|
|
}{
|
|
{
|
|
name: "simple key-value",
|
|
content: "KEY=value\nOTHER=123",
|
|
want: []string{"KEY=value", "OTHER=123"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "quoted values",
|
|
content: `KEY="quoted value"` + "\n" + `OTHER='single quoted'`,
|
|
want: []string{"KEY=quoted value", "OTHER=single quoted"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "comments and blank lines",
|
|
content: "# comment\n\nKEY=value\n# another comment\n",
|
|
want: []string{"KEY=value"},
|
|
wantErr: false,
|
|
},
|
|
{
|
|
name: "empty file",
|
|
content: "",
|
|
want: []string{},
|
|
wantErr: false,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
// Create temp file
|
|
tmpFile := filepath.Join(t.TempDir(), ".env")
|
|
if err := os.WriteFile(tmpFile, []byte(tt.content), 0644); err != nil {
|
|
t.Fatalf("failed to write env file: %v", err)
|
|
}
|
|
|
|
got, err := loadEnvFile(tmpFile)
|
|
if (err != nil) != tt.wantErr {
|
|
t.Errorf("loadEnvFile() error = %v, wantErr %v", err, tt.wantErr)
|
|
return
|
|
}
|
|
if len(got) != len(tt.want) {
|
|
t.Errorf("loadEnvFile() got %d entries, want %d", len(got), len(tt.want))
|
|
return
|
|
}
|
|
for i, v := range got {
|
|
if v != tt.want[i] {
|
|
t.Errorf("env[%d] = %s, want %s", i, v, tt.want[i])
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFileExists(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Test existing file
|
|
existingFile := filepath.Join(tmpDir, "exists")
|
|
if err := os.WriteFile(existingFile, []byte("test"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !fileExists(existingFile) {
|
|
t.Error("fileExists() returned false for existing file")
|
|
}
|
|
|
|
// Test non-existent file
|
|
if fileExists(filepath.Join(tmpDir, "nonexistent")) {
|
|
t.Error("fileExists() returned true for non-existent file")
|
|
}
|
|
|
|
// Test directory (should return false)
|
|
if fileExists(tmpDir) {
|
|
t.Error("fileExists() returned true for directory")
|
|
}
|
|
}
|
|
|
|
func TestDirExists(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Test existing directory
|
|
if !dirExists(tmpDir) {
|
|
t.Error("dirExists() returned false for existing directory")
|
|
}
|
|
|
|
// Test non-existent directory
|
|
if dirExists(filepath.Join(tmpDir, "nonexistent")) {
|
|
t.Error("dirExists() returned true for non-existent directory")
|
|
}
|
|
|
|
// Test file (should return false)
|
|
existingFile := filepath.Join(tmpDir, "file")
|
|
if err := os.WriteFile(existingFile, []byte("test"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if dirExists(existingFile) {
|
|
t.Error("dirExists() returned true for file")
|
|
}
|
|
}
|
|
|
|
func TestCleanupStateFiles(t *testing.T) {
|
|
tmpDir := t.TempDir()
|
|
|
|
// Create state files
|
|
stateFiles := []string{
|
|
"terraform.tfstate",
|
|
"terraform.tfstate.backup",
|
|
".terraform.lock.hcl",
|
|
}
|
|
for _, f := range stateFiles {
|
|
if err := os.WriteFile(filepath.Join(tmpDir, f), []byte("{}"), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
// Create .terraform directory
|
|
tfDir := filepath.Join(tmpDir, ".terraform")
|
|
if err := os.MkdirAll(tfDir, 0755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Run cleanup
|
|
cleanupStateFiles(tmpDir)
|
|
|
|
// Verify files are deleted
|
|
for _, f := range stateFiles {
|
|
if fileExists(filepath.Join(tmpDir, f)) {
|
|
t.Errorf("state file %s was not deleted", f)
|
|
}
|
|
}
|
|
if dirExists(tfDir) {
|
|
t.Error(".terraform directory was not deleted")
|
|
}
|
|
}
|
|
|
|
func TestResourceJSONMarshal(t *testing.T) {
|
|
// Verify Resource struct can be marshaled/unmarshaled if needed
|
|
res := Resource{
|
|
Address: "hcloud_server.main",
|
|
Type: "hcloud_server",
|
|
Name: "main",
|
|
}
|
|
|
|
data, err := json.Marshal(res)
|
|
if err != nil {
|
|
t.Fatalf("failed to marshal Resource: %v", err)
|
|
}
|
|
|
|
var got Resource
|
|
if err := json.Unmarshal(data, &got); err != nil {
|
|
t.Fatalf("failed to unmarshal Resource: %v", err)
|
|
}
|
|
|
|
if got.Address != res.Address || got.Type != res.Type || got.Name != res.Name {
|
|
t.Errorf("marshal/unmarshal roundtrip failed: got %+v, want %+v", got, res)
|
|
}
|
|
}
|
|
|
|
func TestOptionsDefaults(t *testing.T) {
|
|
// Test that Options struct can be created with defaults
|
|
opts := &Options{}
|
|
if opts.AutoApprove != false {
|
|
t.Error("default AutoApprove should be false")
|
|
}
|
|
if opts.WorkDir != "" {
|
|
t.Error("default WorkDir should be empty")
|
|
}
|
|
if opts.KeepState != false {
|
|
t.Error("default KeepState should be false")
|
|
}
|
|
} |