go-ansible/cmd/ansible/ansible_test.go

372 lines
11 KiB
Go
Raw Normal View History

package ansiblecmd
2026-04-01 21:12:05 +00:00
import (
"os"
"path/filepath"
2026-04-01 21:12:05 +00:00
"testing"
"dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
2026-04-01 21:12:05 +00:00
)
func TestExtraVars_Good_RepeatableAndCommaSeparated(t *testing.T) {
opts := core.NewOptions(
core.Option{Key: "extra-vars", Value: "version=1.2.3,env=prod"},
core.Option{Key: "extra-vars", Value: "region=us-east-1"},
core.Option{Key: "extra-vars", Value: []string{"build=42"}},
)
vars, err := extraVars(opts)
require.NoError(t, err)
2026-04-01 21:12:05 +00:00
assert.Equal(t, map[string]any{
2026-04-01 21:12:05 +00:00
"version": "1.2.3",
"env": "prod",
"region": "us-east-1",
2026-04-03 11:44:56 +00:00
"build": 42,
2026-04-01 21:12:05 +00:00
}, vars)
}
2026-04-02 00:27:36 +00:00
func TestExtraVars_Good_UsesShortAlias(t *testing.T) {
opts := core.NewOptions(
core.Option{Key: "e", Value: "version=1.2.3,env=prod"},
)
vars, err := extraVars(opts)
require.NoError(t, err)
2026-04-02 00:27:36 +00:00
assert.Equal(t, map[string]any{
2026-04-02 00:27:36 +00:00
"version": "1.2.3",
"env": "prod",
}, vars)
}
func TestExtraVars_Good_TrimsWhitespaceAroundPairs(t *testing.T) {
opts := core.NewOptions(
core.Option{Key: "extra-vars", Value: " version = 1.2.3 , env = prod , empty = "},
)
vars, err := extraVars(opts)
require.NoError(t, err)
assert.Equal(t, map[string]any{
"version": "1.2.3",
"env": "prod",
"empty": "",
}, vars)
}
2026-04-01 21:12:05 +00:00
func TestExtraVars_Good_IgnoresMalformedPairs(t *testing.T) {
opts := core.NewOptions(
core.Option{Key: "extra-vars", Value: "missing_equals,keep=this"},
core.Option{Key: "extra-vars", Value: "also_bad="},
)
vars, err := extraVars(opts)
require.NoError(t, err)
2026-04-01 21:12:05 +00:00
assert.Equal(t, map[string]any{
2026-04-01 21:12:05 +00:00
"keep": "this",
"also_bad": "",
}, vars)
}
2026-04-01 23:24:17 +00:00
2026-04-03 11:44:56 +00:00
func TestExtraVars_Good_ParsesYAMLScalarsInKeyValuePairs(t *testing.T) {
opts := core.NewOptions(
core.Option{Key: "extra-vars", Value: "enabled=true,count=42,threshold=3.5"},
)
vars, err := extraVars(opts)
require.NoError(t, err)
assert.Equal(t, map[string]any{
"enabled": true,
"count": 42,
"threshold": 3.5,
}, vars)
}
func TestSplitCommaSeparatedOption_Good_TrimsWhitespace(t *testing.T) {
assert.Equal(t, []string{"deploy", "setup", "smoke"}, splitCommaSeparatedOption(" deploy, setup ,smoke "))
}
func TestExtraVars_Good_SupportsStructuredYAMLAndJSON(t *testing.T) {
opts := core.NewOptions(
core.Option{Key: "extra-vars", Value: "app:\n port: 8080\n debug: true"},
core.Option{Key: "extra-vars", Value: `{"image":"nginx:latest","replicas":3}`},
)
vars, err := extraVars(opts)
require.NoError(t, err)
assert.Equal(t, map[string]any{
"app": map[string]any{
"port": int(8080),
"debug": true,
},
"image": "nginx:latest",
"replicas": int(3),
}, vars)
}
func TestExtraVars_Good_LoadsFileReferences(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "vars.yml")
require.NoError(t, os.WriteFile(path, []byte("deploy_env: prod\nrelease: 42\n"), 0644))
opts := core.NewOptions(
core.Option{Key: "extra-vars", Value: "@" + path},
)
vars, err := extraVars(opts)
require.NoError(t, err)
assert.Equal(t, map[string]any{
"deploy_env": "prod",
"release": int(42),
}, vars)
}
func TestExtraVars_Bad_MissingFile(t *testing.T) {
opts := core.NewOptions(
core.Option{Key: "extra-vars", Value: "@/definitely/missing/vars.yml"},
)
_, err := extraVars(opts)
require.Error(t, err)
assert.Contains(t, err.Error(), "read extra vars file")
}
2026-04-02 00:27:36 +00:00
func TestFirstString_Good_PrefersFirstNonEmptyKey(t *testing.T) {
opts := core.NewOptions(
core.Option{Key: "inventory", Value: ""},
core.Option{Key: "i", Value: "/tmp/inventory.yml"},
)
assert.Equal(t, "/tmp/inventory.yml", firstStringOption(opts, "inventory", "i"))
2026-04-02 00:27:36 +00:00
}
func TestFirstBool_Good_UsesAlias(t *testing.T) {
opts := core.NewOptions(
core.Option{Key: "v", Value: true},
)
assert.True(t, firstBoolOption(opts, "verbose", "v"))
2026-04-02 00:27:36 +00:00
}
func TestVerbosityLevel_Good_CountsStackedShortFlags(t *testing.T) {
opts := core.NewOptions()
assert.Equal(t, 3, verbosityLevel(opts, []string{"-vvv"}))
}
func TestVerbosityLevel_Good_CountsLongForm(t *testing.T) {
opts := core.NewOptions()
assert.Equal(t, 1, verbosityLevel(opts, []string{"--verbose"}))
}
func TestVerbosityLevel_Good_PreservesExplicitNumericLevel(t *testing.T) {
opts := core.NewOptions(core.Option{Key: "verbose", Value: 2})
assert.Equal(t, 2, verbosityLevel(opts, nil))
}
2026-04-03 11:24:10 +00:00
func TestBuildPlaybookCommandSettings_Good_AppliesFlags(t *testing.T) {
dir := t.TempDir()
playbookPath := filepath.Join(dir, "site.yml")
require.NoError(t, os.WriteFile(playbookPath, []byte("- hosts: all\n tasks: []\n"), 0644))
opts := core.NewOptions(
core.Option{Key: "_arg", Value: playbookPath},
core.Option{Key: "limit", Value: "web1"},
core.Option{Key: "tags", Value: "deploy,setup"},
core.Option{Key: "skip-tags", Value: "slow"},
core.Option{Key: "extra-vars", Value: "version=1.2.3"},
core.Option{Key: "check", Value: true},
core.Option{Key: "diff", Value: true},
)
settings, err := buildPlaybookCommandSettings(opts, []string{"-vvv"})
require.NoError(t, err)
assert.Equal(t, playbookPath, settings.playbookPath)
assert.Equal(t, dir, settings.basePath)
assert.Equal(t, "web1", settings.limit)
assert.Equal(t, []string{"deploy", "setup"}, settings.tags)
assert.Equal(t, []string{"slow"}, settings.skipTags)
assert.Equal(t, 3, settings.verbose)
assert.True(t, settings.checkMode)
assert.True(t, settings.diff)
assert.Equal(t, map[string]any{"version": "1.2.3"}, settings.extraVars)
}
func TestBuildPlaybookCommandSettings_Good_MergesRepeatedListFlags(t *testing.T) {
dir := t.TempDir()
playbookPath := filepath.Join(dir, "site.yml")
require.NoError(t, os.WriteFile(playbookPath, []byte("- hosts: all\n tasks: []\n"), 0644))
opts := core.NewOptions(
core.Option{Key: "_arg", Value: playbookPath},
core.Option{Key: "limit", Value: "web1"},
core.Option{Key: "limit", Value: []string{"web2"}},
core.Option{Key: "tags", Value: "deploy,setup"},
core.Option{Key: "tags", Value: []string{"smoke"}},
core.Option{Key: "skip-tags", Value: "slow"},
core.Option{Key: "skip-tags", Value: []string{"flaky,experimental"}},
)
settings, err := buildPlaybookCommandSettings(opts, nil)
require.NoError(t, err)
assert.Equal(t, "web1,web2", settings.limit)
assert.Equal(t, []string{"deploy", "setup", "smoke"}, settings.tags)
assert.Equal(t, []string{"slow", "flaky", "experimental"}, settings.skipTags)
}
2026-04-03 11:24:10 +00:00
func TestBuildPlaybookCommandSettings_Bad_MissingPlaybook(t *testing.T) {
_, err := buildPlaybookCommandSettings(core.NewOptions(), nil)
require.Error(t, err)
assert.Contains(t, err.Error(), "usage: ansible <playbook>")
}
func TestDiffOutputLines_Good_IncludesPathAndBeforeAfter(t *testing.T) {
lines := diffOutputLines(map[string]any{
"path": "/etc/nginx/conf.d/app.conf",
"before": "server_name=old.example.com;",
"after": "server_name=web01.example.com;",
})
assert.Equal(t, []string{
"diff:",
"path: /etc/nginx/conf.d/app.conf",
"- server_name=old.example.com;",
"+ server_name=web01.example.com;",
}, lines)
}
2026-04-01 23:24:17 +00:00
func TestTestKeyFile_Good_PrefersExplicitKey(t *testing.T) {
opts := core.NewOptions(
core.Option{Key: "key", Value: "/tmp/id_ed25519"},
core.Option{Key: "i", Value: "/tmp/ignored"},
)
assert.Equal(t, "/tmp/id_ed25519", resolveSSHTestKeyFile(opts))
2026-04-01 23:24:17 +00:00
}
func TestTestKeyFile_Good_FallsBackToShortAlias(t *testing.T) {
opts := core.NewOptions(
core.Option{Key: "i", Value: "/tmp/id_ed25519"},
)
assert.Equal(t, "/tmp/id_ed25519", resolveSSHTestKeyFile(opts))
2026-04-01 23:24:17 +00:00
}
2026-04-02 00:27:36 +00:00
func TestFirstString_Good_ResolvesShortUserAlias(t *testing.T) {
opts := core.NewOptions(
core.Option{Key: "u", Value: "deploy"},
)
cfgUser := firstStringOption(opts, "user", "u")
2026-04-02 00:27:36 +00:00
assert.Equal(t, "deploy", cfgUser)
}
func TestRegister_Good_RegistersAnsibleCommands(t *testing.T) {
app := core.New()
Register(app)
ansible := app.Command("ansible")
require.True(t, ansible.OK)
ansibleCmd := ansible.Value.(*core.Command)
assert.Equal(t, "ansible", ansibleCmd.Path)
assert.Equal(t, "ansible", ansibleCmd.Name)
assert.Equal(t, "Run Ansible playbooks natively (no Python required)", ansibleCmd.Description)
require.NotNil(t, ansibleCmd.Action)
test := app.Command("ansible/test")
require.True(t, test.OK)
testCmd := test.Value.(*core.Command)
assert.Equal(t, "ansible/test", testCmd.Path)
assert.Equal(t, "test", testCmd.Name)
assert.Equal(t, "Test SSH connectivity to a host", testCmd.Description)
require.NotNil(t, testCmd.Action)
paths := app.Commands()
assert.Contains(t, paths, "ansible")
assert.Contains(t, paths, "ansible/test")
}
func TestRegister_Good_ExposesExpectedFlags(t *testing.T) {
app := core.New()
Register(app)
ansibleCmd := app.Command("ansible").Value.(*core.Command)
assert.True(t, ansibleCmd.Flags.Has("inventory"))
assert.True(t, ansibleCmd.Flags.Has("i"))
assert.True(t, ansibleCmd.Flags.Has("limit"))
assert.True(t, ansibleCmd.Flags.Has("l"))
assert.True(t, ansibleCmd.Flags.Has("tags"))
assert.True(t, ansibleCmd.Flags.Has("t"))
assert.True(t, ansibleCmd.Flags.Has("skip-tags"))
assert.True(t, ansibleCmd.Flags.Has("extra-vars"))
assert.True(t, ansibleCmd.Flags.Has("e"))
assert.True(t, ansibleCmd.Flags.Has("verbose"))
assert.True(t, ansibleCmd.Flags.Has("v"))
assert.True(t, ansibleCmd.Flags.Has("check"))
assert.True(t, ansibleCmd.Flags.Has("diff"))
assert.Equal(t, "", ansibleCmd.Flags.String("inventory"))
assert.Equal(t, "", ansibleCmd.Flags.String("i"))
assert.Equal(t, "", ansibleCmd.Flags.String("limit"))
assert.Equal(t, "", ansibleCmd.Flags.String("l"))
assert.Equal(t, "", ansibleCmd.Flags.String("tags"))
assert.Equal(t, "", ansibleCmd.Flags.String("t"))
assert.Equal(t, "", ansibleCmd.Flags.String("skip-tags"))
assert.Equal(t, "", ansibleCmd.Flags.String("extra-vars"))
assert.Equal(t, "", ansibleCmd.Flags.String("e"))
assert.Equal(t, 0, ansibleCmd.Flags.Int("verbose"))
assert.False(t, ansibleCmd.Flags.Bool("v"))
assert.False(t, ansibleCmd.Flags.Bool("check"))
assert.False(t, ansibleCmd.Flags.Bool("diff"))
testCmd := app.Command("ansible/test").Value.(*core.Command)
assert.True(t, testCmd.Flags.Has("user"))
assert.True(t, testCmd.Flags.Has("u"))
assert.True(t, testCmd.Flags.Has("password"))
assert.True(t, testCmd.Flags.Has("key"))
assert.True(t, testCmd.Flags.Has("i"))
assert.True(t, testCmd.Flags.Has("port"))
assert.Equal(t, "root", testCmd.Flags.String("user"))
assert.Equal(t, "root", testCmd.Flags.String("u"))
assert.Equal(t, "", testCmd.Flags.String("password"))
assert.Equal(t, "", testCmd.Flags.String("key"))
assert.Equal(t, "", testCmd.Flags.String("i"))
assert.Equal(t, 22, testCmd.Flags.Int("port"))
}
func TestRunAnsible_Bad_MissingPlaybook(t *testing.T) {
result := runPlaybookCommand(core.NewOptions())
require.False(t, result.OK)
err, ok := result.Value.(error)
require.True(t, ok)
assert.Contains(t, err.Error(), "usage: ansible <playbook>")
}
func TestRunAnsibleTest_Bad_MissingHost(t *testing.T) {
result := runSSHTestCommand(core.NewOptions())
require.False(t, result.OK)
err, ok := result.Value.(error)
require.True(t, ok)
assert.Contains(t, err.Error(), "usage: ansible test <host>")
}