chore: polish ax v0.8.0 compliance

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-03-26 16:39:59 +00:00
parent 4f33c15d6c
commit f127ac2fcb
19 changed files with 984 additions and 729 deletions

View file

@ -5,6 +5,11 @@ import (
)
// Register registers the 'ansible' command and all subcommands on the given Core instance.
//
// Example:
//
// var app core.Core
// Register(&app)
func Register(c *core.Core) {
c.Command("ansible", core.Command{
Description: "Run Ansible playbooks natively (no Python required)",

View file

@ -280,6 +280,7 @@ func join(sep string, parts []string) string { return corexJo
func lower(s string) string { return corexLower(s) }
func replaceAll(s, old, new string) string { return corexReplaceAll(s, old, new) }
func replaceN(s, old, new string, n int) string { return corexReplaceN(s, old, new, n) }
func trimSpace(s string) string { return corexTrimSpace(s) }
func trimCutset(s, cutset string) string { return corexTrimCutset(s, cutset) }
func repeat(s string, count int) string { return corexRepeat(s, count) }
func fields(s string) []string { return corexFields(s) }

View file

@ -13,6 +13,10 @@ import (
)
// Executor runs Ansible playbooks.
//
// Example:
//
// exec := NewExecutor("/workspace/playbooks")
type Executor struct {
parser *Parser
inventory *Inventory
@ -40,6 +44,10 @@ type Executor struct {
}
// NewExecutor creates a new playbook executor.
//
// Example:
//
// exec := NewExecutor("/workspace/playbooks")
func NewExecutor(basePath string) *Executor {
return &Executor{
parser: NewParser(basePath),
@ -53,6 +61,10 @@ func NewExecutor(basePath string) *Executor {
}
// SetInventory loads inventory from a file.
//
// Example:
//
// err := exec.SetInventory("/workspace/inventory.yml")
func (e *Executor) SetInventory(path string) error {
inv, err := e.parser.ParseInventory(path)
if err != nil {
@ -63,11 +75,19 @@ func (e *Executor) SetInventory(path string) error {
}
// SetInventoryDirect sets inventory directly.
//
// Example:
//
// exec.SetInventoryDirect(&Inventory{All: &InventoryGroup{}})
func (e *Executor) SetInventoryDirect(inv *Inventory) {
e.inventory = inv
}
// SetVar sets a variable.
//
// Example:
//
// exec.SetVar("env", "prod")
func (e *Executor) SetVar(key string, value any) {
e.mu.Lock()
defer e.mu.Unlock()
@ -75,6 +95,10 @@ func (e *Executor) SetVar(key string, value any) {
}
// Run executes a playbook.
//
// Example:
//
// err := exec.Run(context.Background(), "/workspace/playbooks/site.yml")
func (e *Executor) Run(ctx context.Context, playbookPath string) error {
plays, err := e.parser.ParsePlaybook(playbookPath)
if err != nil {
@ -956,6 +980,10 @@ func (e *Executor) handleNotify(notify any) {
}
// Close closes all SSH connections.
//
// Example:
//
// exec.Close()
func (e *Executor) Close() {
e.mu.Lock()
defer e.mu.Unlock()
@ -967,6 +995,10 @@ func (e *Executor) Close() {
}
// TemplateFile processes a template file.
//
// Example:
//
// content, err := exec.TemplateFile("/workspace/templates/app.conf.j2", "web1", &Task{})
func (e *Executor) TemplateFile(src, host string, task *Task) (string, error) {
content, err := coreio.Local.Read(src)
if err != nil {

View file

@ -1,8 +1,6 @@
package ansible
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@ -15,7 +13,7 @@ import (
// --- moduleDebug ---
func TestModuleDebug_Good_Message(t *testing.T) {
func TestExecutorExtra_ModuleDebug_Good_Message(t *testing.T) {
e := NewExecutor("/tmp")
result, err := e.moduleDebug(map[string]any{"msg": "Hello world"})
@ -24,7 +22,7 @@ func TestModuleDebug_Good_Message(t *testing.T) {
assert.Equal(t, "Hello world", result.Msg)
}
func TestModuleDebug_Good_Var(t *testing.T) {
func TestExecutorExtra_ModuleDebug_Good_Var(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["my_version"] = "1.2.3"
@ -34,7 +32,7 @@ func TestModuleDebug_Good_Var(t *testing.T) {
assert.Contains(t, result.Msg, "1.2.3")
}
func TestModuleDebug_Good_EmptyArgs(t *testing.T) {
func TestExecutorExtra_ModuleDebug_Good_EmptyArgs(t *testing.T) {
e := NewExecutor("/tmp")
result, err := e.moduleDebug(map[string]any{})
@ -44,7 +42,7 @@ func TestModuleDebug_Good_EmptyArgs(t *testing.T) {
// --- moduleFail ---
func TestModuleFail_Good_DefaultMessage(t *testing.T) {
func TestExecutorExtra_ModuleFail_Good_DefaultMessage(t *testing.T) {
e := NewExecutor("/tmp")
result, err := e.moduleFail(map[string]any{})
@ -53,7 +51,7 @@ func TestModuleFail_Good_DefaultMessage(t *testing.T) {
assert.Equal(t, "Failed as requested", result.Msg)
}
func TestModuleFail_Good_CustomMessage(t *testing.T) {
func TestExecutorExtra_ModuleFail_Good_CustomMessage(t *testing.T) {
e := NewExecutor("/tmp")
result, err := e.moduleFail(map[string]any{"msg": "deployment blocked"})
@ -64,7 +62,7 @@ func TestModuleFail_Good_CustomMessage(t *testing.T) {
// --- moduleAssert ---
func TestModuleAssert_Good_PassingAssertion(t *testing.T) {
func TestExecutorExtra_ModuleAssert_Good_PassingAssertion(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["enabled"] = true
@ -75,7 +73,7 @@ func TestModuleAssert_Good_PassingAssertion(t *testing.T) {
assert.Equal(t, "All assertions passed", result.Msg)
}
func TestModuleAssert_Bad_FailingAssertion(t *testing.T) {
func TestExecutorExtra_ModuleAssert_Bad_FailingAssertion(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["enabled"] = false
@ -86,14 +84,14 @@ func TestModuleAssert_Bad_FailingAssertion(t *testing.T) {
assert.Contains(t, result.Msg, "Assertion failed")
}
func TestModuleAssert_Bad_MissingThat(t *testing.T) {
func TestExecutorExtra_ModuleAssert_Bad_MissingThat(t *testing.T) {
e := NewExecutor("/tmp")
_, err := e.moduleAssert(map[string]any{}, "host1")
assert.Error(t, err)
}
func TestModuleAssert_Good_CustomFailMsg(t *testing.T) {
func TestExecutorExtra_ModuleAssert_Good_CustomFailMsg(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["ready"] = false
@ -107,7 +105,7 @@ func TestModuleAssert_Good_CustomFailMsg(t *testing.T) {
assert.Equal(t, "Service not ready", result.Msg)
}
func TestModuleAssert_Good_MultipleConditions(t *testing.T) {
func TestExecutorExtra_ModuleAssert_Good_MultipleConditions(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["enabled"] = true
e.vars["count"] = 5
@ -122,7 +120,7 @@ func TestModuleAssert_Good_MultipleConditions(t *testing.T) {
// --- moduleSetFact ---
func TestModuleSetFact_Good(t *testing.T) {
func TestExecutorExtra_ModuleSetFact_Good(t *testing.T) {
e := NewExecutor("/tmp")
result, err := e.moduleSetFact(map[string]any{
@ -136,7 +134,7 @@ func TestModuleSetFact_Good(t *testing.T) {
assert.Equal(t, "production", e.vars["deploy_env"])
}
func TestModuleSetFact_Good_SkipsCacheable(t *testing.T) {
func TestExecutorExtra_ModuleSetFact_Good_SkipsCacheable(t *testing.T) {
e := NewExecutor("/tmp")
e.moduleSetFact(map[string]any{
@ -151,7 +149,7 @@ func TestModuleSetFact_Good_SkipsCacheable(t *testing.T) {
// --- moduleIncludeVars ---
func TestModuleIncludeVars_Good_WithFile(t *testing.T) {
func TestExecutorExtra_ModuleIncludeVars_Good_WithFile(t *testing.T) {
e := NewExecutor("/tmp")
result, err := e.moduleIncludeVars(map[string]any{"file": "vars/main.yml"})
@ -159,7 +157,7 @@ func TestModuleIncludeVars_Good_WithFile(t *testing.T) {
assert.Contains(t, result.Msg, "vars/main.yml")
}
func TestModuleIncludeVars_Good_WithRawParams(t *testing.T) {
func TestExecutorExtra_ModuleIncludeVars_Good_WithRawParams(t *testing.T) {
e := NewExecutor("/tmp")
result, err := e.moduleIncludeVars(map[string]any{"_raw_params": "defaults.yml"})
@ -167,7 +165,7 @@ func TestModuleIncludeVars_Good_WithRawParams(t *testing.T) {
assert.Contains(t, result.Msg, "defaults.yml")
}
func TestModuleIncludeVars_Good_Empty(t *testing.T) {
func TestExecutorExtra_ModuleIncludeVars_Good_Empty(t *testing.T) {
e := NewExecutor("/tmp")
result, err := e.moduleIncludeVars(map[string]any{})
@ -177,7 +175,7 @@ func TestModuleIncludeVars_Good_Empty(t *testing.T) {
// --- moduleMeta ---
func TestModuleMeta_Good(t *testing.T) {
func TestExecutorExtra_ModuleMeta_Good(t *testing.T) {
e := NewExecutor("/tmp")
result, err := e.moduleMeta(map[string]any{"_raw_params": "flush_handlers"})
@ -189,22 +187,21 @@ func TestModuleMeta_Good(t *testing.T) {
// Tests for handleLookup (0% coverage)
// ============================================================
func TestHandleLookup_Good_EnvVar(t *testing.T) {
func TestExecutorExtra_HandleLookup_Good_EnvVar(t *testing.T) {
e := NewExecutor("/tmp")
os.Setenv("TEST_ANSIBLE_LOOKUP", "found_it")
defer os.Unsetenv("TEST_ANSIBLE_LOOKUP")
t.Setenv("TEST_ANSIBLE_LOOKUP", "found_it")
result := e.handleLookup("lookup('env', 'TEST_ANSIBLE_LOOKUP')")
assert.Equal(t, "found_it", result)
}
func TestHandleLookup_Good_EnvVarMissing(t *testing.T) {
func TestExecutorExtra_HandleLookup_Good_EnvVarMissing(t *testing.T) {
e := NewExecutor("/tmp")
result := e.handleLookup("lookup('env', 'NONEXISTENT_VAR_12345')")
assert.Equal(t, "", result)
}
func TestHandleLookup_Bad_InvalidSyntax(t *testing.T) {
func TestExecutorExtra_HandleLookup_Bad_InvalidSyntax(t *testing.T) {
e := NewExecutor("/tmp")
result := e.handleLookup("lookup(invalid)")
assert.Equal(t, "", result)
@ -214,9 +211,9 @@ func TestHandleLookup_Bad_InvalidSyntax(t *testing.T) {
// Tests for SetInventory (0% coverage)
// ============================================================
func TestSetInventory_Good(t *testing.T) {
func TestExecutorExtra_SetInventory_Good(t *testing.T) {
dir := t.TempDir()
invPath := filepath.Join(dir, "inventory.yml")
invPath := joinPath(dir, "inventory.yml")
yaml := `all:
hosts:
web1:
@ -224,7 +221,7 @@ func TestSetInventory_Good(t *testing.T) {
web2:
ansible_host: 10.0.0.2
`
require.NoError(t, os.WriteFile(invPath, []byte(yaml), 0644))
require.NoError(t, writeTestFile(invPath, []byte(yaml), 0644))
e := NewExecutor(dir)
err := e.SetInventory(invPath)
@ -234,7 +231,7 @@ func TestSetInventory_Good(t *testing.T) {
assert.Len(t, e.inventory.All.Hosts, 2)
}
func TestSetInventory_Bad_FileNotFound(t *testing.T) {
func TestExecutorExtra_SetInventory_Bad_FileNotFound(t *testing.T) {
e := NewExecutor("/tmp")
err := e.SetInventory("/nonexistent/inventory.yml")
assert.Error(t, err)
@ -244,9 +241,9 @@ func TestSetInventory_Bad_FileNotFound(t *testing.T) {
// Tests for iterator functions (0% coverage)
// ============================================================
func TestParsePlaybookIter_Good(t *testing.T) {
func TestExecutorExtra_ParsePlaybookIter_Good(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "playbook.yml")
path := joinPath(dir, "playbook.yml")
yaml := `- name: First play
hosts: all
tasks:
@ -259,7 +256,7 @@ func TestParsePlaybookIter_Good(t *testing.T) {
- debug:
msg: world
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
iter, err := p.ParsePlaybookIter(path)
@ -274,14 +271,14 @@ func TestParsePlaybookIter_Good(t *testing.T) {
assert.Equal(t, "Second play", plays[1].Name)
}
func TestParsePlaybookIter_Bad_InvalidFile(t *testing.T) {
func TestExecutorExtra_ParsePlaybookIter_Bad_InvalidFile(t *testing.T) {
_, err := NewParser("/tmp").ParsePlaybookIter("/nonexistent.yml")
assert.Error(t, err)
}
func TestParseTasksIter_Good(t *testing.T) {
func TestExecutorExtra_ParseTasksIter_Good(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "tasks.yml")
path := joinPath(dir, "tasks.yml")
yaml := `- name: Task one
debug:
msg: first
@ -290,7 +287,7 @@ func TestParseTasksIter_Good(t *testing.T) {
debug:
msg: second
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
iter, err := p.ParseTasksIter(path)
@ -304,12 +301,12 @@ func TestParseTasksIter_Good(t *testing.T) {
assert.Equal(t, "Task one", tasks[0].Name)
}
func TestParseTasksIter_Bad_InvalidFile(t *testing.T) {
func TestExecutorExtra_ParseTasksIter_Bad_InvalidFile(t *testing.T) {
_, err := NewParser("/tmp").ParseTasksIter("/nonexistent.yml")
assert.Error(t, err)
}
func TestGetHostsIter_Good(t *testing.T) {
func TestExecutorExtra_GetHostsIter_Good(t *testing.T) {
inv := &Inventory{
All: &InventoryGroup{
Hosts: map[string]*Host{
@ -327,7 +324,7 @@ func TestGetHostsIter_Good(t *testing.T) {
assert.Len(t, hosts, 3)
}
func TestAllHostsIter_Good(t *testing.T) {
func TestExecutorExtra_AllHostsIter_Good(t *testing.T) {
group := &InventoryGroup{
Hosts: map[string]*Host{
"alpha": {},
@ -353,7 +350,7 @@ func TestAllHostsIter_Good(t *testing.T) {
assert.Equal(t, "gamma", hosts[2])
}
func TestAllHostsIter_Good_NilGroup(t *testing.T) {
func TestExecutorExtra_AllHostsIter_Good_NilGroup(t *testing.T) {
var count int
for range AllHostsIter(nil) {
count++
@ -365,7 +362,7 @@ func TestAllHostsIter_Good_NilGroup(t *testing.T) {
// Tests for resolveExpr with registered vars (additional coverage)
// ============================================================
func TestResolveExpr_Good_RegisteredVarFields(t *testing.T) {
func TestExecutorExtra_ResolveExpr_Good_RegisteredVarFields(t *testing.T) {
e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{
"cmd_result": {
@ -384,7 +381,7 @@ func TestResolveExpr_Good_RegisteredVarFields(t *testing.T) {
assert.Equal(t, "false", e.resolveExpr("cmd_result.failed", "host1", nil))
}
func TestResolveExpr_Good_TaskVars(t *testing.T) {
func TestExecutorExtra_ResolveExpr_Good_TaskVars(t *testing.T) {
e := NewExecutor("/tmp")
task := &Task{
Vars: map[string]any{"local_var": "local_value"},
@ -394,7 +391,7 @@ func TestResolveExpr_Good_TaskVars(t *testing.T) {
assert.Equal(t, "local_value", result)
}
func TestResolveExpr_Good_HostVars(t *testing.T) {
func TestExecutorExtra_ResolveExpr_Good_HostVars(t *testing.T) {
e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{
@ -408,7 +405,7 @@ func TestResolveExpr_Good_HostVars(t *testing.T) {
assert.Equal(t, "10.0.0.1", result)
}
func TestResolveExpr_Good_Facts(t *testing.T) {
func TestExecutorExtra_ResolveExpr_Good_Facts(t *testing.T) {
e := NewExecutor("/tmp")
e.facts["host1"] = &Facts{
Hostname: "web01",
@ -429,25 +426,25 @@ func TestResolveExpr_Good_Facts(t *testing.T) {
// --- applyFilter additional coverage ---
func TestApplyFilter_Good_B64Decode(t *testing.T) {
func TestExecutorExtra_ApplyFilter_Good_B64Decode(t *testing.T) {
e := NewExecutor("/tmp")
// b64decode is a no-op stub currently
assert.Equal(t, "hello", e.applyFilter("hello", "b64decode"))
}
func TestApplyFilter_Good_UnknownFilter(t *testing.T) {
func TestExecutorExtra_ApplyFilter_Good_UnknownFilter(t *testing.T) {
e := NewExecutor("/tmp")
assert.Equal(t, "value", e.applyFilter("value", "unknown_filter"))
}
// --- evalCondition with default filter ---
func TestEvalCondition_Good_DefaultFilter(t *testing.T) {
func TestExecutorExtra_EvalCondition_Good_DefaultFilter(t *testing.T) {
e := NewExecutor("/tmp")
assert.True(t, e.evalCondition("myvar | default('fallback')", "host1"))
}
func TestEvalCondition_Good_UndefinedCheck(t *testing.T) {
func TestExecutorExtra_EvalCondition_Good_UndefinedCheck(t *testing.T) {
e := NewExecutor("/tmp")
assert.True(t, e.evalCondition("missing_var is not defined", "host1"))
assert.True(t, e.evalCondition("missing_var is undefined", "host1"))
@ -455,7 +452,7 @@ func TestEvalCondition_Good_UndefinedCheck(t *testing.T) {
// --- resolveExpr with filter pipe ---
func TestResolveExpr_Good_WithFilter(t *testing.T) {
func TestExecutorExtra_ResolveExpr_Good_WithFilter(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["raw_value"] = " trimmed "

View file

@ -8,7 +8,7 @@ import (
// --- NewExecutor ---
func TestNewExecutor_Good(t *testing.T) {
func TestExecutor_NewExecutor_Good(t *testing.T) {
e := NewExecutor("/some/path")
assert.NotNil(t, e)
@ -23,7 +23,7 @@ func TestNewExecutor_Good(t *testing.T) {
// --- SetVar ---
func TestSetVar_Good(t *testing.T) {
func TestExecutor_SetVar_Good(t *testing.T) {
e := NewExecutor("/tmp")
e.SetVar("foo", "bar")
e.SetVar("count", 42)
@ -34,7 +34,7 @@ func TestSetVar_Good(t *testing.T) {
// --- SetInventoryDirect ---
func TestSetInventoryDirect_Good(t *testing.T) {
func TestExecutor_SetInventoryDirect_Good(t *testing.T) {
e := NewExecutor("/tmp")
inv := &Inventory{
All: &InventoryGroup{
@ -50,7 +50,7 @@ func TestSetInventoryDirect_Good(t *testing.T) {
// --- getHosts ---
func TestGetHosts_Executor_Good_WithInventory(t *testing.T) {
func TestExecutor_GetHosts_Good_WithInventory(t *testing.T) {
e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{
@ -65,7 +65,7 @@ func TestGetHosts_Executor_Good_WithInventory(t *testing.T) {
assert.Len(t, hosts, 2)
}
func TestGetHosts_Executor_Good_Localhost(t *testing.T) {
func TestExecutor_GetHosts_Good_Localhost(t *testing.T) {
e := NewExecutor("/tmp")
// No inventory set
@ -73,14 +73,14 @@ func TestGetHosts_Executor_Good_Localhost(t *testing.T) {
assert.Equal(t, []string{"localhost"}, hosts)
}
func TestGetHosts_Executor_Good_NoInventory(t *testing.T) {
func TestExecutor_GetHosts_Good_NoInventory(t *testing.T) {
e := NewExecutor("/tmp")
hosts := e.getHosts("webservers")
assert.Nil(t, hosts)
}
func TestGetHosts_Executor_Good_WithLimit(t *testing.T) {
func TestExecutor_GetHosts_Good_WithLimit(t *testing.T) {
e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{
@ -100,14 +100,14 @@ func TestGetHosts_Executor_Good_WithLimit(t *testing.T) {
// --- matchesTags ---
func TestMatchesTags_Good_NoTagsFilter(t *testing.T) {
func TestExecutor_MatchesTags_Good_NoTagsFilter(t *testing.T) {
e := NewExecutor("/tmp")
assert.True(t, e.matchesTags(nil))
assert.True(t, e.matchesTags([]string{"any", "tags"}))
}
func TestMatchesTags_Good_IncludeTag(t *testing.T) {
func TestExecutor_MatchesTags_Good_IncludeTag(t *testing.T) {
e := NewExecutor("/tmp")
e.Tags = []string{"deploy"}
@ -116,7 +116,7 @@ func TestMatchesTags_Good_IncludeTag(t *testing.T) {
assert.False(t, e.matchesTags([]string{"other"}))
}
func TestMatchesTags_Good_SkipTag(t *testing.T) {
func TestExecutor_MatchesTags_Good_SkipTag(t *testing.T) {
e := NewExecutor("/tmp")
e.SkipTags = []string{"slow"}
@ -125,14 +125,14 @@ func TestMatchesTags_Good_SkipTag(t *testing.T) {
assert.False(t, e.matchesTags([]string{"fast", "slow"}))
}
func TestMatchesTags_Good_AllTag(t *testing.T) {
func TestExecutor_MatchesTags_Good_AllTag(t *testing.T) {
e := NewExecutor("/tmp")
e.Tags = []string{"all"}
assert.True(t, e.matchesTags([]string{"anything"}))
}
func TestMatchesTags_Good_NoTaskTags(t *testing.T) {
func TestExecutor_MatchesTags_Good_NoTaskTags(t *testing.T) {
e := NewExecutor("/tmp")
e.Tags = []string{"deploy"}
@ -143,14 +143,14 @@ func TestMatchesTags_Good_NoTaskTags(t *testing.T) {
// --- handleNotify ---
func TestHandleNotify_Good_String(t *testing.T) {
func TestExecutor_HandleNotify_Good_String(t *testing.T) {
e := NewExecutor("/tmp")
e.handleNotify("restart nginx")
assert.True(t, e.notified["restart nginx"])
}
func TestHandleNotify_Good_StringList(t *testing.T) {
func TestExecutor_HandleNotify_Good_StringList(t *testing.T) {
e := NewExecutor("/tmp")
e.handleNotify([]string{"restart nginx", "reload config"})
@ -158,7 +158,7 @@ func TestHandleNotify_Good_StringList(t *testing.T) {
assert.True(t, e.notified["reload config"])
}
func TestHandleNotify_Good_AnyList(t *testing.T) {
func TestExecutor_HandleNotify_Good_AnyList(t *testing.T) {
e := NewExecutor("/tmp")
e.handleNotify([]any{"restart nginx", "reload config"})
@ -168,47 +168,47 @@ func TestHandleNotify_Good_AnyList(t *testing.T) {
// --- normalizeConditions ---
func TestNormalizeConditions_Good_String(t *testing.T) {
func TestExecutor_NormalizeConditions_Good_String(t *testing.T) {
result := normalizeConditions("my_var is defined")
assert.Equal(t, []string{"my_var is defined"}, result)
}
func TestNormalizeConditions_Good_StringSlice(t *testing.T) {
func TestExecutor_NormalizeConditions_Good_StringSlice(t *testing.T) {
result := normalizeConditions([]string{"cond1", "cond2"})
assert.Equal(t, []string{"cond1", "cond2"}, result)
}
func TestNormalizeConditions_Good_AnySlice(t *testing.T) {
func TestExecutor_NormalizeConditions_Good_AnySlice(t *testing.T) {
result := normalizeConditions([]any{"cond1", "cond2"})
assert.Equal(t, []string{"cond1", "cond2"}, result)
}
func TestNormalizeConditions_Good_Nil(t *testing.T) {
func TestExecutor_NormalizeConditions_Good_Nil(t *testing.T) {
result := normalizeConditions(nil)
assert.Nil(t, result)
}
// --- evaluateWhen ---
func TestEvaluateWhen_Good_TrueLiteral(t *testing.T) {
func TestExecutor_EvaluateWhen_Good_TrueLiteral(t *testing.T) {
e := NewExecutor("/tmp")
assert.True(t, e.evaluateWhen("true", "host1", nil))
assert.True(t, e.evaluateWhen("True", "host1", nil))
}
func TestEvaluateWhen_Good_FalseLiteral(t *testing.T) {
func TestExecutor_EvaluateWhen_Good_FalseLiteral(t *testing.T) {
e := NewExecutor("/tmp")
assert.False(t, e.evaluateWhen("false", "host1", nil))
assert.False(t, e.evaluateWhen("False", "host1", nil))
}
func TestEvaluateWhen_Good_Negation(t *testing.T) {
func TestExecutor_EvaluateWhen_Good_Negation(t *testing.T) {
e := NewExecutor("/tmp")
assert.False(t, e.evaluateWhen("not true", "host1", nil))
assert.True(t, e.evaluateWhen("not false", "host1", nil))
}
func TestEvaluateWhen_Good_RegisteredVarDefined(t *testing.T) {
func TestExecutor_EvaluateWhen_Good_RegisteredVarDefined(t *testing.T) {
e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{
"myresult": {Changed: true, Failed: false},
@ -220,7 +220,7 @@ func TestEvaluateWhen_Good_RegisteredVarDefined(t *testing.T) {
assert.True(t, e.evaluateWhen("nonexistent is not defined", "host1", nil))
}
func TestEvaluateWhen_Good_RegisteredVarStatus(t *testing.T) {
func TestExecutor_EvaluateWhen_Good_RegisteredVarStatus(t *testing.T) {
e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{
"success_result": {Changed: true, Failed: false},
@ -235,7 +235,7 @@ func TestEvaluateWhen_Good_RegisteredVarStatus(t *testing.T) {
assert.True(t, e.evaluateWhen("skipped_result is skipped", "host1", nil))
}
func TestEvaluateWhen_Good_VarTruthy(t *testing.T) {
func TestExecutor_EvaluateWhen_Good_VarTruthy(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["enabled"] = true
e.vars["disabled"] = false
@ -252,7 +252,7 @@ func TestEvaluateWhen_Good_VarTruthy(t *testing.T) {
assert.False(t, e.evalCondition("zero", "host1"))
}
func TestEvaluateWhen_Good_MultipleConditions(t *testing.T) {
func TestExecutor_EvaluateWhen_Good_MultipleConditions(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["enabled"] = true
@ -263,7 +263,7 @@ func TestEvaluateWhen_Good_MultipleConditions(t *testing.T) {
// --- templateString ---
func TestTemplateString_Good_SimpleVar(t *testing.T) {
func TestExecutor_TemplateString_Good_SimpleVar(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["name"] = "world"
@ -271,7 +271,7 @@ func TestTemplateString_Good_SimpleVar(t *testing.T) {
assert.Equal(t, "hello world", result)
}
func TestTemplateString_Good_MultVars(t *testing.T) {
func TestExecutor_TemplateString_Good_MultVars(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["host"] = "example.com"
e.vars["port"] = 8080
@ -280,13 +280,13 @@ func TestTemplateString_Good_MultVars(t *testing.T) {
assert.Equal(t, "http://example.com:8080", result)
}
func TestTemplateString_Good_Unresolved(t *testing.T) {
func TestExecutor_TemplateString_Good_Unresolved(t *testing.T) {
e := NewExecutor("/tmp")
result := e.templateString("{{ undefined_var }}", "", nil)
assert.Equal(t, "{{ undefined_var }}", result)
}
func TestTemplateString_Good_NoTemplate(t *testing.T) {
func TestExecutor_TemplateString_Good_NoTemplate(t *testing.T) {
e := NewExecutor("/tmp")
result := e.templateString("plain string", "", nil)
assert.Equal(t, "plain string", result)
@ -294,14 +294,14 @@ func TestTemplateString_Good_NoTemplate(t *testing.T) {
// --- applyFilter ---
func TestApplyFilter_Good_Default(t *testing.T) {
func TestExecutor_ApplyFilter_Good_Default(t *testing.T) {
e := NewExecutor("/tmp")
assert.Equal(t, "hello", e.applyFilter("hello", "default('fallback')"))
assert.Equal(t, "fallback", e.applyFilter("", "default('fallback')"))
}
func TestApplyFilter_Good_Bool(t *testing.T) {
func TestExecutor_ApplyFilter_Good_Bool(t *testing.T) {
e := NewExecutor("/tmp")
assert.Equal(t, "true", e.applyFilter("true", "bool"))
@ -312,26 +312,26 @@ func TestApplyFilter_Good_Bool(t *testing.T) {
assert.Equal(t, "false", e.applyFilter("anything", "bool"))
}
func TestApplyFilter_Good_Trim(t *testing.T) {
func TestExecutor_ApplyFilter_Good_Trim(t *testing.T) {
e := NewExecutor("/tmp")
assert.Equal(t, "hello", e.applyFilter(" hello ", "trim"))
}
// --- resolveLoop ---
func TestResolveLoop_Good_SliceAny(t *testing.T) {
func TestExecutor_ResolveLoop_Good_SliceAny(t *testing.T) {
e := NewExecutor("/tmp")
items := e.resolveLoop([]any{"a", "b", "c"}, "host1")
assert.Len(t, items, 3)
}
func TestResolveLoop_Good_SliceString(t *testing.T) {
func TestExecutor_ResolveLoop_Good_SliceString(t *testing.T) {
e := NewExecutor("/tmp")
items := e.resolveLoop([]string{"a", "b", "c"}, "host1")
assert.Len(t, items, 3)
}
func TestResolveLoop_Good_Nil(t *testing.T) {
func TestExecutor_ResolveLoop_Good_Nil(t *testing.T) {
e := NewExecutor("/tmp")
items := e.resolveLoop(nil, "host1")
assert.Nil(t, items)
@ -339,14 +339,14 @@ func TestResolveLoop_Good_Nil(t *testing.T) {
// --- templateArgs ---
func TestTemplateArgs_Good(t *testing.T) {
func TestExecutor_TemplateArgs_Good(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["myvar"] = "resolved"
args := map[string]any{
"plain": "no template",
"plain": "no template",
"templated": "{{ myvar }}",
"number": 42,
"number": 42,
}
result := e.templateArgs(args, "host1", nil)
@ -355,7 +355,7 @@ func TestTemplateArgs_Good(t *testing.T) {
assert.Equal(t, 42, result["number"])
}
func TestTemplateArgs_Good_NestedMap(t *testing.T) {
func TestExecutor_TemplateArgs_Good_NestedMap(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["port"] = "8080"
@ -370,7 +370,7 @@ func TestTemplateArgs_Good_NestedMap(t *testing.T) {
assert.Equal(t, "8080", nested["port"])
}
func TestTemplateArgs_Good_ArrayValues(t *testing.T) {
func TestExecutor_TemplateArgs_Good_ArrayValues(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["pkg"] = "nginx"
@ -386,7 +386,7 @@ func TestTemplateArgs_Good_ArrayValues(t *testing.T) {
// --- Helper functions ---
func TestGetStringArg_Good(t *testing.T) {
func TestExecutor_GetStringArg_Good(t *testing.T) {
args := map[string]any{
"name": "value",
"number": 42,
@ -397,7 +397,7 @@ func TestGetStringArg_Good(t *testing.T) {
assert.Equal(t, "default", getStringArg(args, "missing", "default"))
}
func TestGetBoolArg_Good(t *testing.T) {
func TestExecutor_GetBoolArg_Good(t *testing.T) {
args := map[string]any{
"enabled": true,
"disabled": false,
@ -419,7 +419,7 @@ func TestGetBoolArg_Good(t *testing.T) {
// --- Close ---
func TestClose_Good_EmptyClients(t *testing.T) {
func TestExecutor_Close_Good_EmptyClients(t *testing.T) {
e := NewExecutor("/tmp")
// Should not panic with no clients
e.Close()

View file

@ -2,20 +2,23 @@ package ansible
import (
"context"
"fmt"
"io"
"os"
"path/filepath"
"io/fs"
"regexp"
"strconv"
"strings"
"sync"
core "dappco.re/go/core"
)
// --- Mock SSH Client ---
// MockSSHClient simulates an SSHClient for testing module logic
// without requiring real SSH connections.
//
// Example:
//
// mock := NewMockSSHClient()
type MockSSHClient struct {
mu sync.Mutex
@ -59,10 +62,15 @@ type executedCommand struct {
type uploadRecord struct {
Content []byte
Remote string
Mode os.FileMode
Mode fs.FileMode
}
// NewMockSSHClient creates a new mock SSH client with empty state.
//
// Example:
//
// mock := NewMockSSHClient()
// mock.expectCommand("echo ok", "ok", "", 0)
func NewMockSSHClient() *MockSSHClient {
return &MockSSHClient{
files: make(map[string][]byte),
@ -70,6 +78,14 @@ func NewMockSSHClient() *MockSSHClient {
}
}
func mockError(op, msg string) error {
return core.E(op, msg, nil)
}
func mockWrap(op, msg string, err error) error {
return core.E(op, msg, err)
}
// expectCommand registers a command pattern with a pre-configured response.
// The pattern is a regular expression matched against the full command string.
func (m *MockSSHClient) expectCommand(pattern, stdout, stderr string, rc int) {
@ -109,6 +125,10 @@ func (m *MockSSHClient) addStat(path string, info map[string]any) {
// Run simulates executing a command. It matches against registered
// expectations in order (last match wins) and records the execution.
//
// Example:
//
// stdout, stderr, rc, err := mock.Run(context.Background(), "echo ok")
func (m *MockSSHClient) Run(_ context.Context, cmd string) (string, string, int, error) {
m.mu.Lock()
defer m.mu.Unlock()
@ -128,6 +148,10 @@ func (m *MockSSHClient) Run(_ context.Context, cmd string) (string, string, int,
}
// RunScript simulates executing a script via heredoc.
//
// Example:
//
// stdout, stderr, rc, err := mock.RunScript(context.Background(), "echo ok")
func (m *MockSSHClient) RunScript(_ context.Context, script string) (string, string, int, error) {
m.mu.Lock()
defer m.mu.Unlock()
@ -146,13 +170,17 @@ func (m *MockSSHClient) RunScript(_ context.Context, script string) (string, str
}
// Upload simulates uploading content to the remote filesystem.
func (m *MockSSHClient) Upload(_ context.Context, local io.Reader, remote string, mode os.FileMode) error {
//
// Example:
//
// err := mock.Upload(context.Background(), newReader("hello"), "/tmp/hello.txt", 0644)
func (m *MockSSHClient) Upload(_ context.Context, local io.Reader, remote string, mode fs.FileMode) error {
m.mu.Lock()
defer m.mu.Unlock()
content, err := io.ReadAll(local)
if err != nil {
return fmt.Errorf("mock upload read: %w", err)
return mockWrap("MockSSHClient.Upload", "mock upload read", err)
}
m.uploads = append(m.uploads, uploadRecord{
@ -165,18 +193,26 @@ func (m *MockSSHClient) Upload(_ context.Context, local io.Reader, remote string
}
// Download simulates downloading content from the remote filesystem.
//
// Example:
//
// data, err := mock.Download(context.Background(), "/tmp/hello.txt")
func (m *MockSSHClient) Download(_ context.Context, remote string) ([]byte, error) {
m.mu.Lock()
defer m.mu.Unlock()
content, ok := m.files[remote]
if !ok {
return nil, fmt.Errorf("file not found: %s", remote)
return nil, mockError("MockSSHClient.Download", sprintf("file not found: %s", remote))
}
return content, nil
}
// FileExists checks if a path exists in the simulated filesystem.
//
// Example:
//
// ok, err := mock.FileExists(context.Background(), "/tmp/hello.txt")
func (m *MockSSHClient) FileExists(_ context.Context, path string) (bool, error) {
m.mu.Lock()
defer m.mu.Unlock()
@ -187,6 +223,10 @@ func (m *MockSSHClient) FileExists(_ context.Context, path string) (bool, error)
// Stat returns stat info from the pre-configured map, or constructs
// a basic result from the file existence in the simulated filesystem.
//
// Example:
//
// info, err := mock.Stat(context.Background(), "/tmp/hello.txt")
func (m *MockSSHClient) Stat(_ context.Context, path string) (map[string]any, error) {
m.mu.Lock()
defer m.mu.Unlock()
@ -204,6 +244,10 @@ func (m *MockSSHClient) Stat(_ context.Context, path string) (map[string]any, er
}
// SetBecome records become state changes.
//
// Example:
//
// mock.SetBecome(true, "root", "")
func (m *MockSSHClient) SetBecome(become bool, user, password string) {
m.mu.Lock()
defer m.mu.Unlock()
@ -217,6 +261,10 @@ func (m *MockSSHClient) SetBecome(become bool, user, password string) {
}
// Close is a no-op for the mock.
//
// Example:
//
// _ = mock.Close()
func (m *MockSSHClient) Close() error {
return nil
}
@ -426,7 +474,7 @@ func executeModuleWithMock(e *Executor, mock *MockSSHClient, host string, task *
return moduleDockerComposeWithClient(e, mock, args)
default:
return nil, fmt.Errorf("mock dispatch: unsupported module %s", module)
return nil, mockError("executeModuleWithMock", sprintf("unsupported module %s", module))
}
}
@ -444,11 +492,11 @@ func moduleShellWithClient(_ *Executor, client sshRunner, args map[string]any) (
cmd = getStringArg(args, "cmd", "")
}
if cmd == "" {
return nil, fmt.Errorf("shell: no command specified")
return nil, mockError("moduleShellWithClient", "shell: no command specified")
}
if chdir := getStringArg(args, "chdir", ""); chdir != "" {
cmd = fmt.Sprintf("cd %q && %s", chdir, cmd)
cmd = sprintf("cd %q && %s", chdir, cmd)
}
stdout, stderr, rc, err := client.RunScript(context.Background(), cmd)
@ -471,11 +519,11 @@ func moduleCommandWithClient(_ *Executor, client sshRunner, args map[string]any)
cmd = getStringArg(args, "cmd", "")
}
if cmd == "" {
return nil, fmt.Errorf("command: no command specified")
return nil, mockError("moduleCommandWithClient", "command: no command specified")
}
if chdir := getStringArg(args, "chdir", ""); chdir != "" {
cmd = fmt.Sprintf("cd %q && %s", chdir, cmd)
cmd = sprintf("cd %q && %s", chdir, cmd)
}
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
@ -495,7 +543,7 @@ func moduleCommandWithClient(_ *Executor, client sshRunner, args map[string]any)
func moduleRawWithClient(_ *Executor, client sshRunner, args map[string]any) (*TaskResult, error) {
cmd := getStringArg(args, "_raw_params", "")
if cmd == "" {
return nil, fmt.Errorf("raw: no command specified")
return nil, mockError("moduleRawWithClient", "raw: no command specified")
}
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
@ -514,12 +562,12 @@ func moduleRawWithClient(_ *Executor, client sshRunner, args map[string]any) (*T
func moduleScriptWithClient(_ *Executor, client sshRunner, args map[string]any) (*TaskResult, error) {
script := getStringArg(args, "_raw_params", "")
if script == "" {
return nil, fmt.Errorf("script: no script specified")
return nil, mockError("moduleScriptWithClient", "script: no script specified")
}
content, err := os.ReadFile(script)
content, err := readTestFile(script)
if err != nil {
return nil, fmt.Errorf("read script: %w", err)
return nil, mockWrap("moduleScriptWithClient", "read script", err)
}
stdout, stderr, rc, err := client.RunScript(context.Background(), string(content))
@ -541,7 +589,7 @@ func moduleScriptWithClient(_ *Executor, client sshRunner, args map[string]any)
type sshFileRunner interface {
sshRunner
Upload(ctx context.Context, local io.Reader, remote string, mode os.FileMode) error
Upload(ctx context.Context, local io.Reader, remote string, mode fs.FileMode) error
Stat(ctx context.Context, path string) (map[string]any, error)
FileExists(ctx context.Context, path string) (bool, error)
}
@ -551,72 +599,72 @@ type sshFileRunner interface {
func moduleCopyWithClient(e *Executor, client sshFileRunner, args map[string]any, host string, task *Task) (*TaskResult, error) {
dest := getStringArg(args, "dest", "")
if dest == "" {
return nil, fmt.Errorf("copy: dest required")
return nil, mockError("moduleCopyWithClient", "copy: dest required")
}
var content []byte
var err error
if src := getStringArg(args, "src", ""); src != "" {
content, err = os.ReadFile(src)
content, err = readTestFile(src)
if err != nil {
return nil, fmt.Errorf("read src: %w", err)
return nil, mockWrap("moduleCopyWithClient", "read src", err)
}
} else if c := getStringArg(args, "content", ""); c != "" {
content = []byte(c)
} else {
return nil, fmt.Errorf("copy: src or content required")
return nil, mockError("moduleCopyWithClient", "copy: src or content required")
}
mode := os.FileMode(0644)
mode := fs.FileMode(0644)
if m := getStringArg(args, "mode", ""); m != "" {
if parsed, parseErr := strconv.ParseInt(m, 8, 32); parseErr == nil {
mode = os.FileMode(parsed)
mode = fs.FileMode(parsed)
}
}
err = client.Upload(context.Background(), strings.NewReader(string(content)), dest, mode)
err = client.Upload(context.Background(), newReader(string(content)), dest, mode)
if err != nil {
return nil, err
}
// Handle owner/group (best-effort, errors ignored)
if owner := getStringArg(args, "owner", ""); owner != "" {
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("chown %s %q", owner, dest))
_, _, _, _ = client.Run(context.Background(), sprintf("chown %s %q", owner, dest))
}
if group := getStringArg(args, "group", ""); group != "" {
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("chgrp %s %q", group, dest))
_, _, _, _ = client.Run(context.Background(), sprintf("chgrp %s %q", group, dest))
}
return &TaskResult{Changed: true, Msg: fmt.Sprintf("copied to %s", dest)}, nil
return &TaskResult{Changed: true, Msg: sprintf("copied to %s", dest)}, nil
}
func moduleTemplateWithClient(e *Executor, client sshFileRunner, args map[string]any, host string, task *Task) (*TaskResult, error) {
src := getStringArg(args, "src", "")
dest := getStringArg(args, "dest", "")
if src == "" || dest == "" {
return nil, fmt.Errorf("template: src and dest required")
return nil, mockError("moduleTemplateWithClient", "template: src and dest required")
}
// Process template
content, err := e.TemplateFile(src, host, task)
if err != nil {
return nil, fmt.Errorf("template: %w", err)
return nil, mockWrap("moduleTemplateWithClient", "template", err)
}
mode := os.FileMode(0644)
mode := fs.FileMode(0644)
if m := getStringArg(args, "mode", ""); m != "" {
if parsed, parseErr := strconv.ParseInt(m, 8, 32); parseErr == nil {
mode = os.FileMode(parsed)
mode = fs.FileMode(parsed)
}
}
err = client.Upload(context.Background(), strings.NewReader(content), dest, mode)
err = client.Upload(context.Background(), newReader(content), dest, mode)
if err != nil {
return nil, err
}
return &TaskResult{Changed: true, Msg: fmt.Sprintf("templated to %s", dest)}, nil
return &TaskResult{Changed: true, Msg: sprintf("templated to %s", dest)}, nil
}
func moduleFileWithClient(_ *Executor, client sshFileRunner, args map[string]any) (*TaskResult, error) {
@ -625,7 +673,7 @@ func moduleFileWithClient(_ *Executor, client sshFileRunner, args map[string]any
path = getStringArg(args, "dest", "")
}
if path == "" {
return nil, fmt.Errorf("file: path required")
return nil, mockError("moduleFileWithClient", "file: path required")
}
state := getStringArg(args, "state", "file")
@ -633,21 +681,21 @@ func moduleFileWithClient(_ *Executor, client sshFileRunner, args map[string]any
switch state {
case "directory":
mode := getStringArg(args, "mode", "0755")
cmd := fmt.Sprintf("mkdir -p %q && chmod %s %q", path, mode, path)
cmd := sprintf("mkdir -p %q && chmod %s %q", path, mode, path)
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil || rc != 0 {
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
}
case "absent":
cmd := fmt.Sprintf("rm -rf %q", path)
cmd := sprintf("rm -rf %q", path)
_, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil || rc != 0 {
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
}
case "touch":
cmd := fmt.Sprintf("touch %q", path)
cmd := sprintf("touch %q", path)
_, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil || rc != 0 {
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
@ -656,9 +704,9 @@ func moduleFileWithClient(_ *Executor, client sshFileRunner, args map[string]any
case "link":
src := getStringArg(args, "src", "")
if src == "" {
return nil, fmt.Errorf("file: src required for link state")
return nil, mockError("moduleFileWithClient", "file: src required for link state")
}
cmd := fmt.Sprintf("ln -sf %q %q", src, path)
cmd := sprintf("ln -sf %q %q", src, path)
_, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil || rc != 0 {
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
@ -667,20 +715,20 @@ func moduleFileWithClient(_ *Executor, client sshFileRunner, args map[string]any
case "file":
// Ensure file exists and set permissions
if mode := getStringArg(args, "mode", ""); mode != "" {
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("chmod %s %q", mode, path))
_, _, _, _ = client.Run(context.Background(), sprintf("chmod %s %q", mode, path))
}
}
// Handle owner/group (best-effort, errors ignored)
if owner := getStringArg(args, "owner", ""); owner != "" {
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("chown %s %q", owner, path))
_, _, _, _ = client.Run(context.Background(), sprintf("chown %s %q", owner, path))
}
if group := getStringArg(args, "group", ""); group != "" {
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("chgrp %s %q", group, path))
_, _, _, _ = client.Run(context.Background(), sprintf("chgrp %s %q", group, path))
}
if recurse := getBoolArg(args, "recurse", false); recurse {
if owner := getStringArg(args, "owner", ""); owner != "" {
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("chown -R %s %q", owner, path))
_, _, _, _ = client.Run(context.Background(), sprintf("chown -R %s %q", owner, path))
}
}
@ -693,7 +741,7 @@ func moduleLineinfileWithClient(_ *Executor, client sshRunner, args map[string]a
path = getStringArg(args, "dest", "")
}
if path == "" {
return nil, fmt.Errorf("lineinfile: path required")
return nil, mockError("moduleLineinfileWithClient", "lineinfile: path required")
}
line := getStringArg(args, "line", "")
@ -702,7 +750,7 @@ func moduleLineinfileWithClient(_ *Executor, client sshRunner, args map[string]a
if state == "absent" {
if regexpArg != "" {
cmd := fmt.Sprintf("sed -i '/%s/d' %q", regexpArg, path)
cmd := sprintf("sed -i '/%s/d' %q", regexpArg, path)
_, stderr, rc, _ := client.Run(context.Background(), cmd)
if rc != 0 {
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
@ -712,17 +760,17 @@ func moduleLineinfileWithClient(_ *Executor, client sshRunner, args map[string]a
// state == present
if regexpArg != "" {
// Replace line matching regexp
escapedLine := strings.ReplaceAll(line, "/", "\\/")
cmd := fmt.Sprintf("sed -i 's/%s/%s/' %q", regexpArg, escapedLine, path)
escapedLine := replaceAll(line, "/", "\\/")
cmd := sprintf("sed -i 's/%s/%s/' %q", regexpArg, escapedLine, path)
_, _, rc, _ := client.Run(context.Background(), cmd)
if rc != 0 {
// Line not found, append
cmd = fmt.Sprintf("echo %q >> %q", line, path)
cmd = sprintf("echo %q >> %q", line, path)
_, _, _, _ = client.Run(context.Background(), cmd)
}
} else if line != "" {
// Ensure line is present
cmd := fmt.Sprintf("grep -qxF %q %q || echo %q >> %q", line, path, line, path)
cmd := sprintf("grep -qxF %q %q || echo %q >> %q", line, path, line, path)
_, _, _, _ = client.Run(context.Background(), cmd)
}
}
@ -736,7 +784,7 @@ func moduleBlockinfileWithClient(_ *Executor, client sshFileRunner, args map[str
path = getStringArg(args, "dest", "")
}
if path == "" {
return nil, fmt.Errorf("blockinfile: path required")
return nil, mockError("moduleBlockinfileWithClient", "blockinfile: path required")
}
block := getStringArg(args, "block", "")
@ -744,14 +792,14 @@ func moduleBlockinfileWithClient(_ *Executor, client sshFileRunner, args map[str
state := getStringArg(args, "state", "present")
create := getBoolArg(args, "create", false)
beginMarker := strings.Replace(marker, "{mark}", "BEGIN", 1)
endMarker := strings.Replace(marker, "{mark}", "END", 1)
beginMarker := replaceN(marker, "{mark}", "BEGIN", 1)
endMarker := replaceN(marker, "{mark}", "END", 1)
if state == "absent" {
// Remove block
cmd := fmt.Sprintf("sed -i '/%s/,/%s/d' %q",
strings.ReplaceAll(beginMarker, "/", "\\/"),
strings.ReplaceAll(endMarker, "/", "\\/"),
cmd := sprintf("sed -i '/%s/,/%s/d' %q",
replaceAll(beginMarker, "/", "\\/"),
replaceAll(endMarker, "/", "\\/"),
path)
_, _, _, _ = client.Run(context.Background(), cmd)
return &TaskResult{Changed: true}, nil
@ -759,20 +807,20 @@ func moduleBlockinfileWithClient(_ *Executor, client sshFileRunner, args map[str
// Create file if needed (best-effort)
if create {
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("touch %q", path))
_, _, _, _ = client.Run(context.Background(), sprintf("touch %q", path))
}
// Remove existing block and add new one
escapedBlock := strings.ReplaceAll(block, "'", "'\\''")
cmd := fmt.Sprintf(`
escapedBlock := replaceAll(block, "'", "'\\''")
cmd := sprintf(`
sed -i '/%s/,/%s/d' %q 2>/dev/null || true
cat >> %q << 'BLOCK_EOF'
%s
%s
%s
BLOCK_EOF
`, strings.ReplaceAll(beginMarker, "/", "\\/"),
strings.ReplaceAll(endMarker, "/", "\\/"),
`, replaceAll(beginMarker, "/", "\\/"),
replaceAll(endMarker, "/", "\\/"),
path, path, beginMarker, escapedBlock, endMarker)
stdout, stderr, rc, err := client.RunScript(context.Background(), cmd)
@ -786,7 +834,7 @@ BLOCK_EOF
func moduleStatWithClient(_ *Executor, client sshFileRunner, args map[string]any) (*TaskResult, error) {
path := getStringArg(args, "path", "")
if path == "" {
return nil, fmt.Errorf("stat: path required")
return nil, mockError("moduleStatWithClient", "stat: path required")
}
stat, err := client.Stat(context.Background(), path)
@ -808,7 +856,7 @@ func moduleServiceWithClient(_ *Executor, client sshRunner, args map[string]any)
enabled := args["enabled"]
if name == "" {
return nil, fmt.Errorf("service: name required")
return nil, mockError("moduleServiceWithClient", "service: name required")
}
var cmds []string
@ -816,21 +864,21 @@ func moduleServiceWithClient(_ *Executor, client sshRunner, args map[string]any)
if state != "" {
switch state {
case "started":
cmds = append(cmds, fmt.Sprintf("systemctl start %s", name))
cmds = append(cmds, sprintf("systemctl start %s", name))
case "stopped":
cmds = append(cmds, fmt.Sprintf("systemctl stop %s", name))
cmds = append(cmds, sprintf("systemctl stop %s", name))
case "restarted":
cmds = append(cmds, fmt.Sprintf("systemctl restart %s", name))
cmds = append(cmds, sprintf("systemctl restart %s", name))
case "reloaded":
cmds = append(cmds, fmt.Sprintf("systemctl reload %s", name))
cmds = append(cmds, sprintf("systemctl reload %s", name))
}
}
if enabled != nil {
if getBoolArg(args, "enabled", false) {
cmds = append(cmds, fmt.Sprintf("systemctl enable %s", name))
cmds = append(cmds, sprintf("systemctl enable %s", name))
} else {
cmds = append(cmds, fmt.Sprintf("systemctl disable %s", name))
cmds = append(cmds, sprintf("systemctl disable %s", name))
}
}
@ -868,12 +916,12 @@ func moduleAptWithClient(_ *Executor, client sshRunner, args map[string]any) (*T
switch state {
case "present", "installed":
if name != "" {
cmd = fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq %s", name)
cmd = sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq %s", name)
}
case "absent", "removed":
cmd = fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get remove -y -qq %s", name)
cmd = sprintf("DEBIAN_FRONTEND=noninteractive apt-get remove -y -qq %s", name)
case "latest":
cmd = fmt.Sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --only-upgrade %s", name)
cmd = sprintf("DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --only-upgrade %s", name)
}
if cmd == "" {
@ -895,20 +943,20 @@ func moduleAptKeyWithClient(_ *Executor, client sshRunner, args map[string]any)
if state == "absent" {
if keyring != "" {
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("rm -f %q", keyring))
_, _, _, _ = client.Run(context.Background(), sprintf("rm -f %q", keyring))
}
return &TaskResult{Changed: true}, nil
}
if url == "" {
return nil, fmt.Errorf("apt_key: url required")
return nil, mockError("moduleAptKeyWithClient", "apt_key: url required")
}
var cmd string
if keyring != "" {
cmd = fmt.Sprintf("curl -fsSL %q | gpg --dearmor -o %q", url, keyring)
cmd = sprintf("curl -fsSL %q | gpg --dearmor -o %q", url, keyring)
} else {
cmd = fmt.Sprintf("curl -fsSL %q | apt-key add -", url)
cmd = sprintf("curl -fsSL %q | apt-key add -", url)
}
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
@ -925,23 +973,23 @@ func moduleAptRepositoryWithClient(_ *Executor, client sshRunner, args map[strin
state := getStringArg(args, "state", "present")
if repo == "" {
return nil, fmt.Errorf("apt_repository: repo required")
return nil, mockError("moduleAptRepositoryWithClient", "apt_repository: repo required")
}
if filename == "" {
filename = strings.ReplaceAll(repo, " ", "-")
filename = strings.ReplaceAll(filename, "/", "-")
filename = strings.ReplaceAll(filename, ":", "")
filename = replaceAll(repo, " ", "-")
filename = replaceAll(filename, "/", "-")
filename = replaceAll(filename, ":", "")
}
path := fmt.Sprintf("/etc/apt/sources.list.d/%s.list", filename)
path := sprintf("/etc/apt/sources.list.d/%s.list", filename)
if state == "absent" {
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("rm -f %q", path))
_, _, _, _ = client.Run(context.Background(), sprintf("rm -f %q", path))
return &TaskResult{Changed: true}, nil
}
cmd := fmt.Sprintf("echo %q > %q", repo, path)
cmd := sprintf("echo %q > %q", repo, path)
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil || rc != 0 {
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
@ -956,9 +1004,9 @@ func moduleAptRepositoryWithClient(_ *Executor, client sshRunner, args map[strin
func modulePackageWithClient(e *Executor, client sshRunner, args map[string]any) (*TaskResult, error) {
stdout, _, _, _ := client.Run(context.Background(), "which apt-get yum dnf 2>/dev/null | head -1")
stdout = strings.TrimSpace(stdout)
stdout = trimSpace(stdout)
if strings.Contains(stdout, "apt") {
if contains(stdout, "apt") {
return moduleAptWithClient(e, client, args)
}
@ -973,11 +1021,11 @@ func modulePipWithClient(_ *Executor, client sshRunner, args map[string]any) (*T
var cmd string
switch state {
case "present", "installed":
cmd = fmt.Sprintf("%s install %s", executable, name)
cmd = sprintf("%s install %s", executable, name)
case "absent", "removed":
cmd = fmt.Sprintf("%s uninstall -y %s", executable, name)
cmd = sprintf("%s uninstall -y %s", executable, name)
case "latest":
cmd = fmt.Sprintf("%s install --upgrade %s", executable, name)
cmd = sprintf("%s install --upgrade %s", executable, name)
}
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
@ -995,11 +1043,11 @@ func moduleUserWithClient(_ *Executor, client sshRunner, args map[string]any) (*
state := getStringArg(args, "state", "present")
if name == "" {
return nil, fmt.Errorf("user: name required")
return nil, mockError("moduleUserWithClient", "user: name required")
}
if state == "absent" {
cmd := fmt.Sprintf("userdel -r %s 2>/dev/null || true", name)
cmd := sprintf("userdel -r %s 2>/dev/null || true", name)
_, _, _, _ = client.Run(context.Background(), cmd)
return &TaskResult{Changed: true}, nil
}
@ -1030,12 +1078,12 @@ func moduleUserWithClient(_ *Executor, client sshRunner, args map[string]any) (*
}
// Try usermod first, then useradd
optsStr := strings.Join(opts, " ")
optsStr := joinStrings(opts, " ")
var cmd string
if optsStr == "" {
cmd = fmt.Sprintf("id %s >/dev/null 2>&1 || useradd %s", name, name)
cmd = sprintf("id %s >/dev/null 2>&1 || useradd %s", name, name)
} else {
cmd = fmt.Sprintf("id %s >/dev/null 2>&1 && usermod %s %s || useradd %s %s",
cmd = sprintf("id %s >/dev/null 2>&1 && usermod %s %s || useradd %s %s",
name, optsStr, name, optsStr, name)
}
@ -1052,11 +1100,11 @@ func moduleGroupWithClient(_ *Executor, client sshRunner, args map[string]any) (
state := getStringArg(args, "state", "present")
if name == "" {
return nil, fmt.Errorf("group: name required")
return nil, mockError("moduleGroupWithClient", "group: name required")
}
if state == "absent" {
cmd := fmt.Sprintf("groupdel %s 2>/dev/null || true", name)
cmd := sprintf("groupdel %s 2>/dev/null || true", name)
_, _, _, _ = client.Run(context.Background(), cmd)
return &TaskResult{Changed: true}, nil
}
@ -1069,8 +1117,8 @@ func moduleGroupWithClient(_ *Executor, client sshRunner, args map[string]any) (
opts = append(opts, "-r")
}
cmd := fmt.Sprintf("getent group %s >/dev/null 2>&1 || groupadd %s %s",
name, strings.Join(opts, " "), name)
cmd := sprintf("getent group %s >/dev/null 2>&1 || groupadd %s %s",
name, joinStrings(opts, " "), name)
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil || rc != 0 {
@ -1097,7 +1145,7 @@ func moduleCronWithClient(_ *Executor, client sshRunner, args map[string]any) (*
if state == "absent" {
if name != "" {
// Remove by name (comment marker)
cmd := fmt.Sprintf("crontab -u %s -l 2>/dev/null | grep -v '# %s' | grep -v '%s' | crontab -u %s -",
cmd := sprintf("crontab -u %s -l 2>/dev/null | grep -v '# %s' | grep -v '%s' | crontab -u %s -",
user, name, job, user)
_, _, _, _ = client.Run(context.Background(), cmd)
}
@ -1105,11 +1153,11 @@ func moduleCronWithClient(_ *Executor, client sshRunner, args map[string]any) (*
}
// Build cron entry
schedule := fmt.Sprintf("%s %s %s %s %s", minute, hour, day, month, weekday)
entry := fmt.Sprintf("%s %s # %s", schedule, job, name)
schedule := sprintf("%s %s %s %s %s", minute, hour, day, month, weekday)
entry := sprintf("%s %s # %s", schedule, job, name)
// Add to crontab
cmd := fmt.Sprintf("(crontab -u %s -l 2>/dev/null | grep -v '# %s' ; echo %q) | crontab -u %s -",
cmd := sprintf("(crontab -u %s -l 2>/dev/null | grep -v '# %s' ; echo %q) | crontab -u %s -",
user, name, entry, user)
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil || rc != 0 {
@ -1127,15 +1175,15 @@ func moduleAuthorizedKeyWithClient(_ *Executor, client sshRunner, args map[strin
state := getStringArg(args, "state", "present")
if user == "" || key == "" {
return nil, fmt.Errorf("authorized_key: user and key required")
return nil, mockError("moduleAuthorizedKeyWithClient", "authorized_key: user and key required")
}
// Get user's home directory
stdout, _, _, err := client.Run(context.Background(), fmt.Sprintf("getent passwd %s | cut -d: -f6", user))
stdout, _, _, err := client.Run(context.Background(), sprintf("getent passwd %s | cut -d: -f6", user))
if err != nil {
return nil, fmt.Errorf("get home dir: %w", err)
return nil, mockWrap("moduleAuthorizedKeyWithClient", "get home dir", err)
}
home := strings.TrimSpace(stdout)
home := trimSpace(stdout)
if home == "" {
home = "/root"
if user != "root" {
@ -1143,22 +1191,22 @@ func moduleAuthorizedKeyWithClient(_ *Executor, client sshRunner, args map[strin
}
}
authKeysPath := filepath.Join(home, ".ssh", "authorized_keys")
authKeysPath := joinPath(home, ".ssh", "authorized_keys")
if state == "absent" {
// Remove key
escapedKey := strings.ReplaceAll(key, "/", "\\/")
cmd := fmt.Sprintf("sed -i '/%s/d' %q 2>/dev/null || true", escapedKey[:40], authKeysPath)
escapedKey := replaceAll(key, "/", "\\/")
cmd := sprintf("sed -i '/%s/d' %q 2>/dev/null || true", escapedKey[:40], authKeysPath)
_, _, _, _ = client.Run(context.Background(), cmd)
return &TaskResult{Changed: true}, nil
}
// Ensure .ssh directory exists (best-effort)
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("mkdir -p %q && chmod 700 %q && chown %s:%s %q",
filepath.Dir(authKeysPath), filepath.Dir(authKeysPath), user, user, filepath.Dir(authKeysPath)))
_, _, _, _ = client.Run(context.Background(), sprintf("mkdir -p %q && chmod 700 %q && chown %s:%s %q",
pathDir(authKeysPath), pathDir(authKeysPath), user, user, pathDir(authKeysPath)))
// Add key if not present
cmd := fmt.Sprintf("grep -qF %q %q 2>/dev/null || echo %q >> %q",
cmd := sprintf("grep -qF %q %q 2>/dev/null || echo %q >> %q",
key[:40], authKeysPath, key, authKeysPath)
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil || rc != 0 {
@ -1166,7 +1214,7 @@ func moduleAuthorizedKeyWithClient(_ *Executor, client sshRunner, args map[strin
}
// Fix permissions (best-effort)
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("chmod 600 %q && chown %s:%s %q",
_, _, _, _ = client.Run(context.Background(), sprintf("chmod 600 %q && chown %s:%s %q",
authKeysPath, user, user, authKeysPath))
return &TaskResult{Changed: true}, nil
@ -1180,7 +1228,7 @@ func moduleGitWithClient(_ *Executor, client sshFileRunner, args map[string]any)
version := getStringArg(args, "version", "HEAD")
if repo == "" || dest == "" {
return nil, fmt.Errorf("git: repo and dest required")
return nil, mockError("moduleGitWithClient", "git: repo and dest required")
}
// Check if dest exists
@ -1188,9 +1236,9 @@ func moduleGitWithClient(_ *Executor, client sshFileRunner, args map[string]any)
var cmd string
if exists {
cmd = fmt.Sprintf("cd %q && git fetch --all && git checkout --force %q", dest, version)
cmd = sprintf("cd %q && git fetch --all && git checkout --force %q", dest, version)
} else {
cmd = fmt.Sprintf("git clone %q %q && cd %q && git checkout %q",
cmd = sprintf("git clone %q %q && cd %q && git checkout %q",
repo, dest, dest, version)
}
@ -1210,41 +1258,41 @@ func moduleUnarchiveWithClient(_ *Executor, client sshFileRunner, args map[strin
remote := getBoolArg(args, "remote_src", false)
if src == "" || dest == "" {
return nil, fmt.Errorf("unarchive: src and dest required")
return nil, mockError("moduleUnarchiveWithClient", "unarchive: src and dest required")
}
// Create dest directory (best-effort)
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("mkdir -p %q", dest))
_, _, _, _ = client.Run(context.Background(), sprintf("mkdir -p %q", dest))
var cmd string
if !remote {
// Upload local file first
content, err := os.ReadFile(src)
content, err := readTestFile(src)
if err != nil {
return nil, fmt.Errorf("read src: %w", err)
return nil, mockWrap("moduleUnarchiveWithClient", "read src", err)
}
tmpPath := "/tmp/ansible_unarchive_" + filepath.Base(src)
err = client.Upload(context.Background(), strings.NewReader(string(content)), tmpPath, 0644)
tmpPath := "/tmp/ansible_unarchive_" + pathBase(src)
err = client.Upload(context.Background(), newReader(string(content)), tmpPath, 0644)
if err != nil {
return nil, err
}
src = tmpPath
defer func() { _, _, _, _ = client.Run(context.Background(), fmt.Sprintf("rm -f %q", tmpPath)) }()
defer func() { _, _, _, _ = client.Run(context.Background(), sprintf("rm -f %q", tmpPath)) }()
}
// Detect archive type and extract
if strings.HasSuffix(src, ".tar.gz") || strings.HasSuffix(src, ".tgz") {
cmd = fmt.Sprintf("tar -xzf %q -C %q", src, dest)
} else if strings.HasSuffix(src, ".tar.xz") {
cmd = fmt.Sprintf("tar -xJf %q -C %q", src, dest)
} else if strings.HasSuffix(src, ".tar.bz2") {
cmd = fmt.Sprintf("tar -xjf %q -C %q", src, dest)
} else if strings.HasSuffix(src, ".tar") {
cmd = fmt.Sprintf("tar -xf %q -C %q", src, dest)
} else if strings.HasSuffix(src, ".zip") {
cmd = fmt.Sprintf("unzip -o %q -d %q", src, dest)
if hasSuffix(src, ".tar.gz") || hasSuffix(src, ".tgz") {
cmd = sprintf("tar -xzf %q -C %q", src, dest)
} else if hasSuffix(src, ".tar.xz") {
cmd = sprintf("tar -xJf %q -C %q", src, dest)
} else if hasSuffix(src, ".tar.bz2") {
cmd = sprintf("tar -xjf %q -C %q", src, dest)
} else if hasSuffix(src, ".tar") {
cmd = sprintf("tar -xf %q -C %q", src, dest)
} else if hasSuffix(src, ".zip") {
cmd = sprintf("unzip -o %q -d %q", src, dest)
} else {
cmd = fmt.Sprintf("tar -xf %q -C %q", src, dest) // Guess tar
cmd = sprintf("tar -xf %q -C %q", src, dest) // Guess tar
}
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
@ -1262,7 +1310,7 @@ func moduleURIWithClient(_ *Executor, client sshRunner, args map[string]any) (*T
method := getStringArg(args, "method", "GET")
if url == "" {
return nil, fmt.Errorf("uri: url required")
return nil, mockError("moduleURIWithClient", "uri: url required")
}
var curlOpts []string
@ -1272,7 +1320,7 @@ func moduleURIWithClient(_ *Executor, client sshRunner, args map[string]any) (*T
// Headers
if headers, ok := args["headers"].(map[string]any); ok {
for k, v := range headers {
curlOpts = append(curlOpts, "-H", fmt.Sprintf("%s: %v", k, v))
curlOpts = append(curlOpts, "-H", sprintf("%s: %v", k, v))
}
}
@ -1284,14 +1332,14 @@ func moduleURIWithClient(_ *Executor, client sshRunner, args map[string]any) (*T
// Status code
curlOpts = append(curlOpts, "-w", "\\n%{http_code}")
cmd := fmt.Sprintf("curl %s %q", strings.Join(curlOpts, " "), url)
cmd := sprintf("curl %s %q", joinStrings(curlOpts, " "), url)
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil {
return &TaskResult{Failed: true, Msg: err.Error()}, nil
}
// Parse status code from last line
lines := strings.Split(strings.TrimSpace(stdout), "\n")
lines := split(trimSpace(stdout), "\n")
statusCode := 0
if len(lines) > 0 {
statusCode, _ = strconv.Atoi(lines[len(lines)-1])
@ -1350,13 +1398,13 @@ func moduleUFWWithClient(_ *Executor, client sshRunner, args map[string]any) (*T
if rule != "" && port != "" {
switch rule {
case "allow":
cmd = fmt.Sprintf("ufw allow %s/%s", port, proto)
cmd = sprintf("ufw allow %s/%s", port, proto)
case "deny":
cmd = fmt.Sprintf("ufw deny %s/%s", port, proto)
cmd = sprintf("ufw deny %s/%s", port, proto)
case "reject":
cmd = fmt.Sprintf("ufw reject %s/%s", port, proto)
cmd = sprintf("ufw reject %s/%s", port, proto)
case "limit":
cmd = fmt.Sprintf("ufw limit %s/%s", port, proto)
cmd = sprintf("ufw limit %s/%s", port, proto)
}
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
@ -1375,19 +1423,19 @@ func moduleDockerComposeWithClient(_ *Executor, client sshRunner, args map[strin
state := getStringArg(args, "state", "present")
if projectSrc == "" {
return nil, fmt.Errorf("docker_compose: project_src required")
return nil, mockError("moduleDockerComposeWithClient", "docker_compose: project_src required")
}
var cmd string
switch state {
case "present":
cmd = fmt.Sprintf("cd %q && docker compose up -d", projectSrc)
cmd = sprintf("cd %q && docker compose up -d", projectSrc)
case "absent":
cmd = fmt.Sprintf("cd %q && docker compose down", projectSrc)
cmd = sprintf("cd %q && docker compose down", projectSrc)
case "restarted":
cmd = fmt.Sprintf("cd %q && docker compose restart", projectSrc)
cmd = sprintf("cd %q && docker compose restart", projectSrc)
default:
cmd = fmt.Sprintf("cd %q && docker compose up -d", projectSrc)
cmd = sprintf("cd %q && docker compose up -d", projectSrc)
}
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
@ -1396,7 +1444,7 @@ func moduleDockerComposeWithClient(_ *Executor, client sshRunner, args map[strin
}
// Heuristic for changed
changed := !strings.Contains(stdout, "Up to date") && !strings.Contains(stderr, "Up to date")
changed := !contains(stdout, "Up to date") && !contains(stderr, "Up to date")
return &TaskResult{Changed: changed, Stdout: stdout}, nil
}
@ -1408,7 +1456,7 @@ func (m *MockSSHClient) containsSubstring(sub string) bool {
m.mu.Lock()
defer m.mu.Unlock()
for _, cmd := range m.executed {
if strings.Contains(cmd.Cmd, sub) {
if contains(cmd.Cmd, sub) {
return true
}
}

View file

@ -3,7 +3,7 @@ package ansible
import (
"context"
"encoding/base64"
"os"
"io/fs"
"strconv"
coreio "dappco.re/go/core/io"
@ -295,10 +295,10 @@ func (e *Executor) moduleCopy(ctx context.Context, client *SSHClient, args map[s
return nil, coreerr.E("Executor.moduleCopy", "src or content required", nil)
}
mode := os.FileMode(0644)
mode := fs.FileMode(0644)
if m := getStringArg(args, "mode", ""); m != "" {
if parsed, err := strconv.ParseInt(m, 8, 32); err == nil {
mode = os.FileMode(parsed)
mode = fs.FileMode(parsed)
}
}
@ -331,10 +331,10 @@ func (e *Executor) moduleTemplate(ctx context.Context, client *SSHClient, args m
return nil, coreerr.E("Executor.moduleTemplate", "template", err)
}
mode := os.FileMode(0644)
mode := fs.FileMode(0644)
if m := getStringArg(args, "mode", ""); m != "" {
if parsed, err := strconv.ParseInt(m, 8, 32); err == nil {
mode = os.FileMode(parsed)
mode = fs.FileMode(parsed)
}
}

View file

@ -1,8 +1,6 @@
package ansible
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@ -17,7 +15,7 @@ import (
// --- user module ---
func TestModuleUser_Good_CreateNewUser(t *testing.T) {
func TestModulesAdv_ModuleUser_Good_CreateNewUser(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`id deploy >/dev/null 2>&1`, "", "no such user", 1)
mock.expectCommand(`useradd`, "", "", 0)
@ -44,7 +42,7 @@ func TestModuleUser_Good_CreateNewUser(t *testing.T) {
assert.True(t, mock.containsSubstring("-m"))
}
func TestModuleUser_Good_ModifyExistingUser(t *testing.T) {
func TestModulesAdv_ModuleUser_Good_ModifyExistingUser(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
// id returns success meaning user exists, so usermod branch is taken
mock.expectCommand(`id deploy >/dev/null 2>&1 && usermod`, "", "", 0)
@ -61,7 +59,7 @@ func TestModuleUser_Good_ModifyExistingUser(t *testing.T) {
assert.True(t, mock.containsSubstring("-s /bin/zsh"))
}
func TestModuleUser_Good_RemoveUser(t *testing.T) {
func TestModulesAdv_ModuleUser_Good_RemoveUser(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`userdel -r deploy`, "", "", 0)
@ -75,7 +73,7 @@ func TestModuleUser_Good_RemoveUser(t *testing.T) {
assert.True(t, mock.hasExecuted(`userdel -r deploy`))
}
func TestModuleUser_Good_SystemUser(t *testing.T) {
func TestModulesAdv_ModuleUser_Good_SystemUser(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`id|useradd`, "", "", 0)
@ -99,7 +97,7 @@ func TestModuleUser_Good_SystemUser(t *testing.T) {
assert.NotContains(t, cmd.Cmd, " -m ")
}
func TestModuleUser_Good_NoOptsUsesSimpleForm(t *testing.T) {
func TestModulesAdv_ModuleUser_Good_NoOptsUsesSimpleForm(t *testing.T) {
// When no options are provided, uses the simple "id || useradd" form
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`id testuser >/dev/null 2>&1 || useradd testuser`, "", "", 0)
@ -114,7 +112,7 @@ func TestModuleUser_Good_NoOptsUsesSimpleForm(t *testing.T) {
assert.False(t, result.Failed)
}
func TestModuleUser_Bad_MissingName(t *testing.T) {
func TestModulesAdv_ModuleUser_Bad_MissingName(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -126,7 +124,7 @@ func TestModuleUser_Bad_MissingName(t *testing.T) {
assert.Contains(t, err.Error(), "name required")
}
func TestModuleUser_Good_CommandFailure(t *testing.T) {
func TestModulesAdv_ModuleUser_Good_CommandFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`id|useradd|usermod`, "", "useradd: Permission denied", 1)
@ -142,7 +140,7 @@ func TestModuleUser_Good_CommandFailure(t *testing.T) {
// --- group module ---
func TestModuleGroup_Good_CreateNewGroup(t *testing.T) {
func TestModulesAdv_ModuleGroup_Good_CreateNewGroup(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
// getent fails → groupadd runs
mock.expectCommand(`getent group appgroup`, "", "", 1)
@ -159,7 +157,7 @@ func TestModuleGroup_Good_CreateNewGroup(t *testing.T) {
assert.True(t, mock.containsSubstring("appgroup"))
}
func TestModuleGroup_Good_GroupAlreadyExists(t *testing.T) {
func TestModulesAdv_ModuleGroup_Good_GroupAlreadyExists(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
// getent succeeds → groupadd skipped (|| short-circuits)
mock.expectCommand(`getent group docker >/dev/null 2>&1 || groupadd`, "", "", 0)
@ -173,7 +171,7 @@ func TestModuleGroup_Good_GroupAlreadyExists(t *testing.T) {
assert.False(t, result.Failed)
}
func TestModuleGroup_Good_RemoveGroup(t *testing.T) {
func TestModulesAdv_ModuleGroup_Good_RemoveGroup(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`groupdel oldgroup`, "", "", 0)
@ -187,7 +185,7 @@ func TestModuleGroup_Good_RemoveGroup(t *testing.T) {
assert.True(t, mock.hasExecuted(`groupdel oldgroup`))
}
func TestModuleGroup_Good_SystemGroup(t *testing.T) {
func TestModulesAdv_ModuleGroup_Good_SystemGroup(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`getent group|groupadd`, "", "", 0)
@ -202,7 +200,7 @@ func TestModuleGroup_Good_SystemGroup(t *testing.T) {
assert.True(t, mock.containsSubstring("-r"))
}
func TestModuleGroup_Good_CustomGID(t *testing.T) {
func TestModulesAdv_ModuleGroup_Good_CustomGID(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`getent group|groupadd`, "", "", 0)
@ -217,7 +215,7 @@ func TestModuleGroup_Good_CustomGID(t *testing.T) {
assert.True(t, mock.containsSubstring("-g 5000"))
}
func TestModuleGroup_Bad_MissingName(t *testing.T) {
func TestModulesAdv_ModuleGroup_Bad_MissingName(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -229,7 +227,7 @@ func TestModuleGroup_Bad_MissingName(t *testing.T) {
assert.Contains(t, err.Error(), "name required")
}
func TestModuleGroup_Good_CommandFailure(t *testing.T) {
func TestModulesAdv_ModuleGroup_Good_CommandFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`getent group|groupadd`, "", "groupadd: Permission denied", 1)
@ -243,7 +241,7 @@ func TestModuleGroup_Good_CommandFailure(t *testing.T) {
// --- cron module ---
func TestModuleCron_Good_AddCronJob(t *testing.T) {
func TestModulesAdv_ModuleCron_Good_AddCronJob(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`crontab -u root`, "", "", 0)
@ -261,7 +259,7 @@ func TestModuleCron_Good_AddCronJob(t *testing.T) {
assert.True(t, mock.containsSubstring("# backup"))
}
func TestModuleCron_Good_RemoveCronJob(t *testing.T) {
func TestModulesAdv_ModuleCron_Good_RemoveCronJob(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`crontab -u root -l`, "* * * * * /bin/backup # backup\n", "", 0)
@ -276,7 +274,7 @@ func TestModuleCron_Good_RemoveCronJob(t *testing.T) {
assert.True(t, mock.containsSubstring("grep -v"))
}
func TestModuleCron_Good_CustomSchedule(t *testing.T) {
func TestModulesAdv_ModuleCron_Good_CustomSchedule(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`crontab -u root`, "", "", 0)
@ -297,7 +295,7 @@ func TestModuleCron_Good_CustomSchedule(t *testing.T) {
assert.True(t, mock.containsSubstring("/opt/scripts/backup.sh"))
}
func TestModuleCron_Good_CustomUser(t *testing.T) {
func TestModulesAdv_ModuleCron_Good_CustomUser(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`crontab -u www-data`, "", "", 0)
@ -316,7 +314,7 @@ func TestModuleCron_Good_CustomUser(t *testing.T) {
assert.True(t, mock.containsSubstring("0 */4 * * *"))
}
func TestModuleCron_Good_AbsentWithNoName(t *testing.T) {
func TestModulesAdv_ModuleCron_Good_AbsentWithNoName(t *testing.T) {
// Absent with no name — changed but no grep command
e, mock := newTestExecutorWithMock("host1")
@ -332,7 +330,7 @@ func TestModuleCron_Good_AbsentWithNoName(t *testing.T) {
// --- authorized_key module ---
func TestModuleAuthorizedKey_Good_AddKey(t *testing.T) {
func TestModulesAdv_ModuleAuthorizedKey_Good_AddKey(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... user@host"
mock.expectCommand(`getent passwd deploy`, "/home/deploy", "", 0)
@ -354,7 +352,7 @@ func TestModuleAuthorizedKey_Good_AddKey(t *testing.T) {
assert.True(t, mock.containsSubstring("authorized_keys"))
}
func TestModuleAuthorizedKey_Good_RemoveKey(t *testing.T) {
func TestModulesAdv_ModuleAuthorizedKey_Good_RemoveKey(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... user@host"
mock.expectCommand(`getent passwd deploy`, "/home/deploy", "", 0)
@ -372,7 +370,7 @@ func TestModuleAuthorizedKey_Good_RemoveKey(t *testing.T) {
assert.True(t, mock.containsSubstring("authorized_keys"))
}
func TestModuleAuthorizedKey_Good_KeyAlreadyExists(t *testing.T) {
func TestModulesAdv_ModuleAuthorizedKey_Good_KeyAlreadyExists(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... user@host"
mock.expectCommand(`getent passwd deploy`, "/home/deploy", "", 0)
@ -391,7 +389,7 @@ func TestModuleAuthorizedKey_Good_KeyAlreadyExists(t *testing.T) {
assert.False(t, result.Failed)
}
func TestModuleAuthorizedKey_Good_RootUserFallback(t *testing.T) {
func TestModulesAdv_ModuleAuthorizedKey_Good_RootUserFallback(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... admin@host"
// getent returns empty — falls back to /root for root user
@ -412,7 +410,7 @@ func TestModuleAuthorizedKey_Good_RootUserFallback(t *testing.T) {
assert.True(t, mock.containsSubstring("/root/.ssh"))
}
func TestModuleAuthorizedKey_Bad_MissingUserAndKey(t *testing.T) {
func TestModulesAdv_ModuleAuthorizedKey_Bad_MissingUserAndKey(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -422,7 +420,7 @@ func TestModuleAuthorizedKey_Bad_MissingUserAndKey(t *testing.T) {
assert.Contains(t, err.Error(), "user and key required")
}
func TestModuleAuthorizedKey_Bad_MissingKey(t *testing.T) {
func TestModulesAdv_ModuleAuthorizedKey_Bad_MissingKey(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -434,7 +432,7 @@ func TestModuleAuthorizedKey_Bad_MissingKey(t *testing.T) {
assert.Contains(t, err.Error(), "user and key required")
}
func TestModuleAuthorizedKey_Bad_MissingUser(t *testing.T) {
func TestModulesAdv_ModuleAuthorizedKey_Bad_MissingUser(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -448,7 +446,7 @@ func TestModuleAuthorizedKey_Bad_MissingUser(t *testing.T) {
// --- git module ---
func TestModuleGit_Good_FreshClone(t *testing.T) {
func TestModulesAdv_ModuleGit_Good_FreshClone(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
// .git does not exist → fresh clone
mock.expectCommand(`git clone`, "", "", 0)
@ -468,7 +466,7 @@ func TestModuleGit_Good_FreshClone(t *testing.T) {
assert.True(t, mock.containsSubstring("git checkout"))
}
func TestModuleGit_Good_UpdateExisting(t *testing.T) {
func TestModulesAdv_ModuleGit_Good_UpdateExisting(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
// .git exists → fetch + checkout
mock.addFile("/opt/app/.git", []byte("gitdir"))
@ -488,7 +486,7 @@ func TestModuleGit_Good_UpdateExisting(t *testing.T) {
assert.False(t, mock.containsSubstring("git clone"))
}
func TestModuleGit_Good_CustomVersion(t *testing.T) {
func TestModulesAdv_ModuleGit_Good_CustomVersion(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`git clone`, "", "", 0)
@ -504,7 +502,7 @@ func TestModuleGit_Good_CustomVersion(t *testing.T) {
assert.True(t, mock.containsSubstring("v2.1.0"))
}
func TestModuleGit_Good_UpdateWithBranch(t *testing.T) {
func TestModulesAdv_ModuleGit_Good_UpdateWithBranch(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.addFile("/srv/myapp/.git", []byte("gitdir"))
mock.expectCommand(`git fetch --all && git checkout`, "", "", 0)
@ -520,7 +518,7 @@ func TestModuleGit_Good_UpdateWithBranch(t *testing.T) {
assert.True(t, mock.containsSubstring("develop"))
}
func TestModuleGit_Bad_MissingRepoAndDest(t *testing.T) {
func TestModulesAdv_ModuleGit_Bad_MissingRepoAndDest(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -530,7 +528,7 @@ func TestModuleGit_Bad_MissingRepoAndDest(t *testing.T) {
assert.Contains(t, err.Error(), "repo and dest required")
}
func TestModuleGit_Bad_MissingRepo(t *testing.T) {
func TestModulesAdv_ModuleGit_Bad_MissingRepo(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -542,7 +540,7 @@ func TestModuleGit_Bad_MissingRepo(t *testing.T) {
assert.Contains(t, err.Error(), "repo and dest required")
}
func TestModuleGit_Bad_MissingDest(t *testing.T) {
func TestModulesAdv_ModuleGit_Bad_MissingDest(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -554,7 +552,7 @@ func TestModuleGit_Bad_MissingDest(t *testing.T) {
assert.Contains(t, err.Error(), "repo and dest required")
}
func TestModuleGit_Good_CloneFailure(t *testing.T) {
func TestModulesAdv_ModuleGit_Good_CloneFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`git clone`, "", "fatal: repository not found", 128)
@ -570,11 +568,11 @@ func TestModuleGit_Good_CloneFailure(t *testing.T) {
// --- unarchive module ---
func TestModuleUnarchive_Good_ExtractTarGzLocal(t *testing.T) {
func TestModulesAdv_ModuleUnarchive_Good_ExtractTarGzLocal(t *testing.T) {
// Create a temporary "archive" file
tmpDir := t.TempDir()
archivePath := filepath.Join(tmpDir, "package.tar.gz")
require.NoError(t, os.WriteFile(archivePath, []byte("fake-archive-content"), 0644))
archivePath := joinPath(tmpDir, "package.tar.gz")
require.NoError(t, writeTestFile(archivePath, []byte("fake-archive-content"), 0644))
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`mkdir -p`, "", "", 0)
@ -595,10 +593,10 @@ func TestModuleUnarchive_Good_ExtractTarGzLocal(t *testing.T) {
assert.True(t, mock.containsSubstring("/opt/app"))
}
func TestModuleUnarchive_Good_ExtractZipLocal(t *testing.T) {
func TestModulesAdv_ModuleUnarchive_Good_ExtractZipLocal(t *testing.T) {
tmpDir := t.TempDir()
archivePath := filepath.Join(tmpDir, "release.zip")
require.NoError(t, os.WriteFile(archivePath, []byte("fake-zip-content"), 0644))
archivePath := joinPath(tmpDir, "release.zip")
require.NoError(t, writeTestFile(archivePath, []byte("fake-zip-content"), 0644))
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`mkdir -p`, "", "", 0)
@ -617,7 +615,7 @@ func TestModuleUnarchive_Good_ExtractZipLocal(t *testing.T) {
assert.True(t, mock.containsSubstring("unzip -o"))
}
func TestModuleUnarchive_Good_RemoteSource(t *testing.T) {
func TestModulesAdv_ModuleUnarchive_Good_RemoteSource(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`mkdir -p`, "", "", 0)
mock.expectCommand(`tar -xzf`, "", "", 0)
@ -636,7 +634,7 @@ func TestModuleUnarchive_Good_RemoteSource(t *testing.T) {
assert.True(t, mock.containsSubstring("tar -xzf"))
}
func TestModuleUnarchive_Good_TarXz(t *testing.T) {
func TestModulesAdv_ModuleUnarchive_Good_TarXz(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`mkdir -p`, "", "", 0)
mock.expectCommand(`tar -xJf`, "", "", 0)
@ -652,7 +650,7 @@ func TestModuleUnarchive_Good_TarXz(t *testing.T) {
assert.True(t, mock.containsSubstring("tar -xJf"))
}
func TestModuleUnarchive_Good_TarBz2(t *testing.T) {
func TestModulesAdv_ModuleUnarchive_Good_TarBz2(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`mkdir -p`, "", "", 0)
mock.expectCommand(`tar -xjf`, "", "", 0)
@ -668,7 +666,7 @@ func TestModuleUnarchive_Good_TarBz2(t *testing.T) {
assert.True(t, mock.containsSubstring("tar -xjf"))
}
func TestModuleUnarchive_Bad_MissingSrcAndDest(t *testing.T) {
func TestModulesAdv_ModuleUnarchive_Bad_MissingSrcAndDest(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -678,7 +676,7 @@ func TestModuleUnarchive_Bad_MissingSrcAndDest(t *testing.T) {
assert.Contains(t, err.Error(), "src and dest required")
}
func TestModuleUnarchive_Bad_MissingSrc(t *testing.T) {
func TestModulesAdv_ModuleUnarchive_Bad_MissingSrc(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -690,7 +688,7 @@ func TestModuleUnarchive_Bad_MissingSrc(t *testing.T) {
assert.Contains(t, err.Error(), "src and dest required")
}
func TestModuleUnarchive_Bad_LocalFileNotFound(t *testing.T) {
func TestModulesAdv_ModuleUnarchive_Bad_LocalFileNotFound(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
mock.expectCommand(`mkdir -p`, "", "", 0)
@ -706,7 +704,7 @@ func TestModuleUnarchive_Bad_LocalFileNotFound(t *testing.T) {
// --- uri module ---
func TestModuleURI_Good_GetRequestDefault(t *testing.T) {
func TestModulesAdv_ModuleURI_Good_GetRequestDefault(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl.*https://example.com/api/health`, "OK\n200", "", 0)
@ -721,7 +719,7 @@ func TestModuleURI_Good_GetRequestDefault(t *testing.T) {
assert.Equal(t, 200, result.Data["status"])
}
func TestModuleURI_Good_PostWithBodyAndHeaders(t *testing.T) {
func TestModulesAdv_ModuleURI_Good_PostWithBodyAndHeaders(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
// Use a broad pattern since header order in map iteration is non-deterministic
mock.expectCommand(`curl.*api\.example\.com`, "{\"id\":1}\n201", "", 0)
@ -746,7 +744,7 @@ func TestModuleURI_Good_PostWithBodyAndHeaders(t *testing.T) {
assert.True(t, mock.containsSubstring("Authorization"))
}
func TestModuleURI_Good_WrongStatusCode(t *testing.T) {
func TestModulesAdv_ModuleURI_Good_WrongStatusCode(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl`, "Not Found\n404", "", 0)
@ -759,7 +757,7 @@ func TestModuleURI_Good_WrongStatusCode(t *testing.T) {
assert.Equal(t, 404, result.RC)
}
func TestModuleURI_Good_CurlCommandFailure(t *testing.T) {
func TestModulesAdv_ModuleURI_Good_CurlCommandFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommandError(`curl`, assert.AnError)
@ -772,7 +770,7 @@ func TestModuleURI_Good_CurlCommandFailure(t *testing.T) {
assert.Contains(t, result.Msg, assert.AnError.Error())
}
func TestModuleURI_Good_CustomExpectedStatus(t *testing.T) {
func TestModulesAdv_ModuleURI_Good_CustomExpectedStatus(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl`, "\n204", "", 0)
@ -787,7 +785,7 @@ func TestModuleURI_Good_CustomExpectedStatus(t *testing.T) {
assert.Equal(t, 204, result.RC)
}
func TestModuleURI_Bad_MissingURL(t *testing.T) {
func TestModulesAdv_ModuleURI_Bad_MissingURL(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -801,7 +799,7 @@ func TestModuleURI_Bad_MissingURL(t *testing.T) {
// --- ufw module ---
func TestModuleUFW_Good_AllowRuleWithPort(t *testing.T) {
func TestModulesAdv_ModuleUFW_Good_AllowRuleWithPort(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw allow 443/tcp`, "Rule added", "", 0)
@ -816,7 +814,7 @@ func TestModuleUFW_Good_AllowRuleWithPort(t *testing.T) {
assert.True(t, mock.hasExecuted(`ufw allow 443/tcp`))
}
func TestModuleUFW_Good_EnableFirewall(t *testing.T) {
func TestModulesAdv_ModuleUFW_Good_EnableFirewall(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw --force enable`, "Firewall is active", "", 0)
@ -830,7 +828,7 @@ func TestModuleUFW_Good_EnableFirewall(t *testing.T) {
assert.True(t, mock.hasExecuted(`ufw --force enable`))
}
func TestModuleUFW_Good_DenyRuleWithProto(t *testing.T) {
func TestModulesAdv_ModuleUFW_Good_DenyRuleWithProto(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw deny 53/udp`, "Rule added", "", 0)
@ -846,7 +844,7 @@ func TestModuleUFW_Good_DenyRuleWithProto(t *testing.T) {
assert.True(t, mock.hasExecuted(`ufw deny 53/udp`))
}
func TestModuleUFW_Good_ResetFirewall(t *testing.T) {
func TestModulesAdv_ModuleUFW_Good_ResetFirewall(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw --force reset`, "Resetting", "", 0)
@ -860,7 +858,7 @@ func TestModuleUFW_Good_ResetFirewall(t *testing.T) {
assert.True(t, mock.hasExecuted(`ufw --force reset`))
}
func TestModuleUFW_Good_DisableFirewall(t *testing.T) {
func TestModulesAdv_ModuleUFW_Good_DisableFirewall(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw disable`, "Firewall stopped", "", 0)
@ -874,7 +872,7 @@ func TestModuleUFW_Good_DisableFirewall(t *testing.T) {
assert.True(t, mock.hasExecuted(`ufw disable`))
}
func TestModuleUFW_Good_ReloadFirewall(t *testing.T) {
func TestModulesAdv_ModuleUFW_Good_ReloadFirewall(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw reload`, "Firewall reloaded", "", 0)
@ -888,7 +886,7 @@ func TestModuleUFW_Good_ReloadFirewall(t *testing.T) {
assert.True(t, mock.hasExecuted(`ufw reload`))
}
func TestModuleUFW_Good_LimitRule(t *testing.T) {
func TestModulesAdv_ModuleUFW_Good_LimitRule(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw limit 22/tcp`, "Rule added", "", 0)
@ -903,7 +901,7 @@ func TestModuleUFW_Good_LimitRule(t *testing.T) {
assert.True(t, mock.hasExecuted(`ufw limit 22/tcp`))
}
func TestModuleUFW_Good_StateCommandFailure(t *testing.T) {
func TestModulesAdv_ModuleUFW_Good_StateCommandFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw --force enable`, "", "ERROR: problem running ufw", 1)
@ -917,7 +915,7 @@ func TestModuleUFW_Good_StateCommandFailure(t *testing.T) {
// --- docker_compose module ---
func TestModuleDockerCompose_Good_StatePresent(t *testing.T) {
func TestModulesAdv_ModuleDockerCompose_Good_StatePresent(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose up -d`, "Creating container_1\nCreating container_2\n", "", 0)
@ -933,7 +931,7 @@ func TestModuleDockerCompose_Good_StatePresent(t *testing.T) {
assert.True(t, mock.containsSubstring("/opt/myapp"))
}
func TestModuleDockerCompose_Good_StateAbsent(t *testing.T) {
func TestModulesAdv_ModuleDockerCompose_Good_StateAbsent(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose down`, "Removing container_1\n", "", 0)
@ -948,7 +946,7 @@ func TestModuleDockerCompose_Good_StateAbsent(t *testing.T) {
assert.True(t, mock.hasExecuted(`docker compose down`))
}
func TestModuleDockerCompose_Good_AlreadyUpToDate(t *testing.T) {
func TestModulesAdv_ModuleDockerCompose_Good_AlreadyUpToDate(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose up -d`, "Container myapp-web-1 Up to date\n", "", 0)
@ -962,7 +960,7 @@ func TestModuleDockerCompose_Good_AlreadyUpToDate(t *testing.T) {
assert.False(t, result.Failed)
}
func TestModuleDockerCompose_Good_StateRestarted(t *testing.T) {
func TestModulesAdv_ModuleDockerCompose_Good_StateRestarted(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose restart`, "Restarting container_1\n", "", 0)
@ -977,7 +975,7 @@ func TestModuleDockerCompose_Good_StateRestarted(t *testing.T) {
assert.True(t, mock.hasExecuted(`docker compose restart`))
}
func TestModuleDockerCompose_Bad_MissingProjectSrc(t *testing.T) {
func TestModulesAdv_ModuleDockerCompose_Bad_MissingProjectSrc(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -989,7 +987,7 @@ func TestModuleDockerCompose_Bad_MissingProjectSrc(t *testing.T) {
assert.Contains(t, err.Error(), "project_src required")
}
func TestModuleDockerCompose_Good_CommandFailure(t *testing.T) {
func TestModulesAdv_ModuleDockerCompose_Good_CommandFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose up -d`, "", "Error response from daemon", 1)
@ -1003,7 +1001,7 @@ func TestModuleDockerCompose_Good_CommandFailure(t *testing.T) {
assert.Contains(t, result.Msg, "Error response from daemon")
}
func TestModuleDockerCompose_Good_DefaultStateIsPresent(t *testing.T) {
func TestModulesAdv_ModuleDockerCompose_Good_DefaultStateIsPresent(t *testing.T) {
// When no state is specified, default is "present"
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose up -d`, "Starting\n", "", 0)
@ -1020,7 +1018,7 @@ func TestModuleDockerCompose_Good_DefaultStateIsPresent(t *testing.T) {
// --- Cross-module dispatch tests for advanced modules ---
func TestExecuteModuleWithMock_Good_DispatchUser(t *testing.T) {
func TestModulesAdv_ExecuteModuleWithMock_Good_DispatchUser(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`id|useradd|usermod`, "", "", 0)
@ -1038,7 +1036,7 @@ func TestExecuteModuleWithMock_Good_DispatchUser(t *testing.T) {
assert.True(t, result.Changed)
}
func TestExecuteModuleWithMock_Good_DispatchGroup(t *testing.T) {
func TestModulesAdv_ExecuteModuleWithMock_Good_DispatchGroup(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`getent group|groupadd`, "", "", 0)
@ -1055,7 +1053,7 @@ func TestExecuteModuleWithMock_Good_DispatchGroup(t *testing.T) {
assert.True(t, result.Changed)
}
func TestExecuteModuleWithMock_Good_DispatchCron(t *testing.T) {
func TestModulesAdv_ExecuteModuleWithMock_Good_DispatchCron(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`crontab`, "", "", 0)
@ -1073,7 +1071,7 @@ func TestExecuteModuleWithMock_Good_DispatchCron(t *testing.T) {
assert.True(t, result.Changed)
}
func TestExecuteModuleWithMock_Good_DispatchGit(t *testing.T) {
func TestModulesAdv_ExecuteModuleWithMock_Good_DispatchGit(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`git clone`, "", "", 0)
@ -1091,7 +1089,7 @@ func TestExecuteModuleWithMock_Good_DispatchGit(t *testing.T) {
assert.True(t, result.Changed)
}
func TestExecuteModuleWithMock_Good_DispatchURI(t *testing.T) {
func TestModulesAdv_ExecuteModuleWithMock_Good_DispatchURI(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl`, "OK\n200", "", 0)
@ -1108,7 +1106,7 @@ func TestExecuteModuleWithMock_Good_DispatchURI(t *testing.T) {
assert.False(t, result.Failed)
}
func TestExecuteModuleWithMock_Good_DispatchDockerCompose(t *testing.T) {
func TestModulesAdv_ExecuteModuleWithMock_Good_DispatchDockerCompose(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose up -d`, "Creating\n", "", 0)

View file

@ -1,8 +1,6 @@
package ansible
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@ -15,7 +13,7 @@ import (
// --- MockSSHClient basic tests ---
func TestMockSSHClient_Good_RunRecordsExecution(t *testing.T) {
func TestModulesCmd_MockSSHClient_Good_RunRecordsExecution(t *testing.T) {
mock := NewMockSSHClient()
mock.expectCommand("echo hello", "hello\n", "", 0)
@ -30,7 +28,7 @@ func TestMockSSHClient_Good_RunRecordsExecution(t *testing.T) {
assert.Equal(t, "echo hello", mock.lastCommand().Cmd)
}
func TestMockSSHClient_Good_RunScriptRecordsExecution(t *testing.T) {
func TestModulesCmd_MockSSHClient_Good_RunScriptRecordsExecution(t *testing.T) {
mock := NewMockSSHClient()
mock.expectCommand("set -e", "ok", "", 0)
@ -43,7 +41,7 @@ func TestMockSSHClient_Good_RunScriptRecordsExecution(t *testing.T) {
assert.Equal(t, "RunScript", mock.lastCommand().Method)
}
func TestMockSSHClient_Good_DefaultSuccessResponse(t *testing.T) {
func TestModulesCmd_MockSSHClient_Good_DefaultSuccessResponse(t *testing.T) {
mock := NewMockSSHClient()
// No expectations registered — should return empty success
@ -55,7 +53,7 @@ func TestMockSSHClient_Good_DefaultSuccessResponse(t *testing.T) {
assert.Equal(t, 0, rc)
}
func TestMockSSHClient_Good_LastMatchWins(t *testing.T) {
func TestModulesCmd_MockSSHClient_Good_LastMatchWins(t *testing.T) {
mock := NewMockSSHClient()
mock.expectCommand("echo", "first", "", 0)
mock.expectCommand("echo", "second", "", 0)
@ -65,7 +63,7 @@ func TestMockSSHClient_Good_LastMatchWins(t *testing.T) {
assert.Equal(t, "second", stdout)
}
func TestMockSSHClient_Good_FileOperations(t *testing.T) {
func TestModulesCmd_MockSSHClient_Good_FileOperations(t *testing.T) {
mock := NewMockSSHClient()
// File does not exist initially
@ -91,7 +89,7 @@ func TestMockSSHClient_Good_FileOperations(t *testing.T) {
assert.Error(t, err)
}
func TestMockSSHClient_Good_StatWithExplicit(t *testing.T) {
func TestModulesCmd_MockSSHClient_Good_StatWithExplicit(t *testing.T) {
mock := NewMockSSHClient()
mock.addStat("/var/log", map[string]any{"exists": true, "isdir": true})
@ -101,7 +99,7 @@ func TestMockSSHClient_Good_StatWithExplicit(t *testing.T) {
assert.Equal(t, true, info["isdir"])
}
func TestMockSSHClient_Good_StatFallback(t *testing.T) {
func TestModulesCmd_MockSSHClient_Good_StatFallback(t *testing.T) {
mock := NewMockSSHClient()
mock.addFile("/etc/hosts", []byte("127.0.0.1 localhost"))
@ -115,7 +113,7 @@ func TestMockSSHClient_Good_StatFallback(t *testing.T) {
assert.Equal(t, false, info["exists"])
}
func TestMockSSHClient_Good_BecomeTracking(t *testing.T) {
func TestModulesCmd_MockSSHClient_Good_BecomeTracking(t *testing.T) {
mock := NewMockSSHClient()
assert.False(t, mock.become)
@ -128,7 +126,7 @@ func TestMockSSHClient_Good_BecomeTracking(t *testing.T) {
assert.Equal(t, "secret", mock.becomePass)
}
func TestMockSSHClient_Good_HasExecuted(t *testing.T) {
func TestModulesCmd_MockSSHClient_Good_HasExecuted(t *testing.T) {
mock := NewMockSSHClient()
_, _, _, _ = mock.Run(nil, "systemctl restart nginx")
_, _, _, _ = mock.Run(nil, "apt-get update")
@ -138,7 +136,7 @@ func TestMockSSHClient_Good_HasExecuted(t *testing.T) {
assert.False(t, mock.hasExecuted("yum"))
}
func TestMockSSHClient_Good_HasExecutedMethod(t *testing.T) {
func TestModulesCmd_MockSSHClient_Good_HasExecutedMethod(t *testing.T) {
mock := NewMockSSHClient()
_, _, _, _ = mock.Run(nil, "echo run")
_, _, _, _ = mock.RunScript(nil, "echo script")
@ -149,7 +147,7 @@ func TestMockSSHClient_Good_HasExecutedMethod(t *testing.T) {
assert.False(t, mock.hasExecutedMethod("RunScript", "echo run"))
}
func TestMockSSHClient_Good_Reset(t *testing.T) {
func TestModulesCmd_MockSSHClient_Good_Reset(t *testing.T) {
mock := NewMockSSHClient()
_, _, _, _ = mock.Run(nil, "echo hello")
assert.Equal(t, 1, mock.commandCount())
@ -158,7 +156,7 @@ func TestMockSSHClient_Good_Reset(t *testing.T) {
assert.Equal(t, 0, mock.commandCount())
}
func TestMockSSHClient_Good_ErrorExpectation(t *testing.T) {
func TestModulesCmd_MockSSHClient_Good_ErrorExpectation(t *testing.T) {
mock := NewMockSSHClient()
mock.expectCommandError("bad cmd", assert.AnError)
@ -168,7 +166,7 @@ func TestMockSSHClient_Good_ErrorExpectation(t *testing.T) {
// --- command module ---
func TestModuleCommand_Good_BasicCommand(t *testing.T) {
func TestModulesCmd_ModuleCommand_Good_BasicCommand(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("ls -la /tmp", "total 0\n", "", 0)
@ -187,7 +185,7 @@ func TestModuleCommand_Good_BasicCommand(t *testing.T) {
assert.False(t, mock.hasExecutedMethod("RunScript", ".*"))
}
func TestModuleCommand_Good_CmdArg(t *testing.T) {
func TestModulesCmd_ModuleCommand_Good_CmdArg(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("whoami", "root\n", "", 0)
@ -201,7 +199,7 @@ func TestModuleCommand_Good_CmdArg(t *testing.T) {
assert.True(t, mock.hasExecutedMethod("Run", "whoami"))
}
func TestModuleCommand_Good_WithChdir(t *testing.T) {
func TestModulesCmd_ModuleCommand_Good_WithChdir(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`cd "/var/log" && ls`, "syslog\n", "", 0)
@ -219,7 +217,7 @@ func TestModuleCommand_Good_WithChdir(t *testing.T) {
assert.Contains(t, last.Cmd, "ls")
}
func TestModuleCommand_Bad_NoCommand(t *testing.T) {
func TestModulesCmd_ModuleCommand_Bad_NoCommand(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -229,7 +227,7 @@ func TestModuleCommand_Bad_NoCommand(t *testing.T) {
assert.Contains(t, err.Error(), "no command specified")
}
func TestModuleCommand_Good_NonZeroRC(t *testing.T) {
func TestModulesCmd_ModuleCommand_Good_NonZeroRC(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("false", "", "error occurred", 1)
@ -243,7 +241,7 @@ func TestModuleCommand_Good_NonZeroRC(t *testing.T) {
assert.Equal(t, "error occurred", result.Stderr)
}
func TestModuleCommand_Good_SSHError(t *testing.T) {
func TestModulesCmd_ModuleCommand_Good_SSHError(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
mock.expectCommandError(".*", assert.AnError)
@ -257,7 +255,7 @@ func TestModuleCommand_Good_SSHError(t *testing.T) {
assert.Contains(t, result.Msg, assert.AnError.Error())
}
func TestModuleCommand_Good_RawParamsTakesPrecedence(t *testing.T) {
func TestModulesCmd_ModuleCommand_Good_RawParamsTakesPrecedence(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("from_raw", "raw\n", "", 0)
@ -273,7 +271,7 @@ func TestModuleCommand_Good_RawParamsTakesPrecedence(t *testing.T) {
// --- shell module ---
func TestModuleShell_Good_BasicShell(t *testing.T) {
func TestModulesCmd_ModuleShell_Good_BasicShell(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("echo hello", "hello\n", "", 0)
@ -291,7 +289,7 @@ func TestModuleShell_Good_BasicShell(t *testing.T) {
assert.False(t, mock.hasExecutedMethod("Run", ".*"))
}
func TestModuleShell_Good_CmdArg(t *testing.T) {
func TestModulesCmd_ModuleShell_Good_CmdArg(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("date", "Thu Feb 20\n", "", 0)
@ -304,7 +302,7 @@ func TestModuleShell_Good_CmdArg(t *testing.T) {
assert.True(t, mock.hasExecutedMethod("RunScript", "date"))
}
func TestModuleShell_Good_WithChdir(t *testing.T) {
func TestModulesCmd_ModuleShell_Good_WithChdir(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`cd "/app" && npm install`, "done\n", "", 0)
@ -321,7 +319,7 @@ func TestModuleShell_Good_WithChdir(t *testing.T) {
assert.Contains(t, last.Cmd, "npm install")
}
func TestModuleShell_Bad_NoCommand(t *testing.T) {
func TestModulesCmd_ModuleShell_Bad_NoCommand(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -331,7 +329,7 @@ func TestModuleShell_Bad_NoCommand(t *testing.T) {
assert.Contains(t, err.Error(), "no command specified")
}
func TestModuleShell_Good_NonZeroRC(t *testing.T) {
func TestModulesCmd_ModuleShell_Good_NonZeroRC(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("exit 2", "", "failed", 2)
@ -344,7 +342,7 @@ func TestModuleShell_Good_NonZeroRC(t *testing.T) {
assert.Equal(t, 2, result.RC)
}
func TestModuleShell_Good_SSHError(t *testing.T) {
func TestModulesCmd_ModuleShell_Good_SSHError(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
mock.expectCommandError(".*", assert.AnError)
@ -357,7 +355,7 @@ func TestModuleShell_Good_SSHError(t *testing.T) {
assert.True(t, result.Failed)
}
func TestModuleShell_Good_PipelineCommand(t *testing.T) {
func TestModulesCmd_ModuleShell_Good_PipelineCommand(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`cat /etc/passwd \| grep root`, "root:x:0:0\n", "", 0)
@ -373,7 +371,7 @@ func TestModuleShell_Good_PipelineCommand(t *testing.T) {
// --- raw module ---
func TestModuleRaw_Good_BasicRaw(t *testing.T) {
func TestModulesCmd_ModuleRaw_Good_BasicRaw(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("uname -a", "Linux host1 5.15\n", "", 0)
@ -390,7 +388,7 @@ func TestModuleRaw_Good_BasicRaw(t *testing.T) {
assert.False(t, mock.hasExecutedMethod("RunScript", ".*"))
}
func TestModuleRaw_Bad_NoCommand(t *testing.T) {
func TestModulesCmd_ModuleRaw_Bad_NoCommand(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -400,7 +398,7 @@ func TestModuleRaw_Bad_NoCommand(t *testing.T) {
assert.Contains(t, err.Error(), "no command specified")
}
func TestModuleRaw_Good_NoChdir(t *testing.T) {
func TestModulesCmd_ModuleRaw_Good_NoChdir(t *testing.T) {
// Raw module does NOT support chdir — it should ignore it
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("echo test", "test\n", "", 0)
@ -418,7 +416,7 @@ func TestModuleRaw_Good_NoChdir(t *testing.T) {
assert.NotContains(t, last.Cmd, "cd")
}
func TestModuleRaw_Good_NonZeroRC(t *testing.T) {
func TestModulesCmd_ModuleRaw_Good_NonZeroRC(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("invalid", "", "not found", 127)
@ -432,7 +430,7 @@ func TestModuleRaw_Good_NonZeroRC(t *testing.T) {
assert.Equal(t, "not found", result.Stderr)
}
func TestModuleRaw_Good_SSHError(t *testing.T) {
func TestModulesCmd_ModuleRaw_Good_SSHError(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
mock.expectCommandError(".*", assert.AnError)
@ -445,7 +443,7 @@ func TestModuleRaw_Good_SSHError(t *testing.T) {
assert.True(t, result.Failed)
}
func TestModuleRaw_Good_ExactCommandPassthrough(t *testing.T) {
func TestModulesCmd_ModuleRaw_Good_ExactCommandPassthrough(t *testing.T) {
// Raw should pass the command exactly as given — no wrapping
e, mock := newTestExecutorWithMock("host1")
complexCmd := `/usr/bin/python3 -c 'import sys; print(sys.version)'`
@ -463,12 +461,12 @@ func TestModuleRaw_Good_ExactCommandPassthrough(t *testing.T) {
// --- script module ---
func TestModuleScript_Good_BasicScript(t *testing.T) {
func TestModulesCmd_ModuleScript_Good_BasicScript(t *testing.T) {
// Create a temporary script file
tmpDir := t.TempDir()
scriptPath := filepath.Join(tmpDir, "setup.sh")
scriptPath := joinPath(tmpDir, "setup.sh")
scriptContent := "#!/bin/bash\necho 'setup complete'\nexit 0"
require.NoError(t, os.WriteFile(scriptPath, []byte(scriptContent), 0755))
require.NoError(t, writeTestFile(scriptPath, []byte(scriptContent), 0755))
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("setup complete", "setup complete\n", "", 0)
@ -490,7 +488,7 @@ func TestModuleScript_Good_BasicScript(t *testing.T) {
assert.Equal(t, scriptContent, last.Cmd)
}
func TestModuleScript_Bad_NoScript(t *testing.T) {
func TestModulesCmd_ModuleScript_Bad_NoScript(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -500,7 +498,7 @@ func TestModuleScript_Bad_NoScript(t *testing.T) {
assert.Contains(t, err.Error(), "no script specified")
}
func TestModuleScript_Bad_FileNotFound(t *testing.T) {
func TestModulesCmd_ModuleScript_Bad_FileNotFound(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -512,10 +510,10 @@ func TestModuleScript_Bad_FileNotFound(t *testing.T) {
assert.Contains(t, err.Error(), "read script")
}
func TestModuleScript_Good_NonZeroRC(t *testing.T) {
func TestModulesCmd_ModuleScript_Good_NonZeroRC(t *testing.T) {
tmpDir := t.TempDir()
scriptPath := filepath.Join(tmpDir, "fail.sh")
require.NoError(t, os.WriteFile(scriptPath, []byte("exit 1"), 0755))
scriptPath := joinPath(tmpDir, "fail.sh")
require.NoError(t, writeTestFile(scriptPath, []byte("exit 1"), 0755))
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("exit 1", "", "script failed", 1)
@ -529,11 +527,11 @@ func TestModuleScript_Good_NonZeroRC(t *testing.T) {
assert.Equal(t, 1, result.RC)
}
func TestModuleScript_Good_MultiLineScript(t *testing.T) {
func TestModulesCmd_ModuleScript_Good_MultiLineScript(t *testing.T) {
tmpDir := t.TempDir()
scriptPath := filepath.Join(tmpDir, "multi.sh")
scriptPath := joinPath(tmpDir, "multi.sh")
scriptContent := "#!/bin/bash\nset -e\napt-get update\napt-get install -y nginx\nsystemctl start nginx"
require.NoError(t, os.WriteFile(scriptPath, []byte(scriptContent), 0755))
require.NoError(t, writeTestFile(scriptPath, []byte(scriptContent), 0755))
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("apt-get", "done\n", "", 0)
@ -551,10 +549,10 @@ func TestModuleScript_Good_MultiLineScript(t *testing.T) {
assert.Equal(t, scriptContent, last.Cmd)
}
func TestModuleScript_Good_SSHError(t *testing.T) {
func TestModulesCmd_ModuleScript_Good_SSHError(t *testing.T) {
tmpDir := t.TempDir()
scriptPath := filepath.Join(tmpDir, "ok.sh")
require.NoError(t, os.WriteFile(scriptPath, []byte("echo ok"), 0755))
scriptPath := joinPath(tmpDir, "ok.sh")
require.NoError(t, writeTestFile(scriptPath, []byte("echo ok"), 0755))
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -570,7 +568,7 @@ func TestModuleScript_Good_SSHError(t *testing.T) {
// --- Cross-module differentiation tests ---
func TestModuleDifferentiation_Good_CommandUsesRun(t *testing.T) {
func TestModulesCmd_ModuleDifferentiation_Good_CommandUsesRun(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("echo test", "test\n", "", 0)
@ -581,7 +579,7 @@ func TestModuleDifferentiation_Good_CommandUsesRun(t *testing.T) {
assert.Equal(t, "Run", cmds[0].Method, "command module must use Run()")
}
func TestModuleDifferentiation_Good_ShellUsesRunScript(t *testing.T) {
func TestModulesCmd_ModuleDifferentiation_Good_ShellUsesRunScript(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("echo test", "test\n", "", 0)
@ -592,7 +590,7 @@ func TestModuleDifferentiation_Good_ShellUsesRunScript(t *testing.T) {
assert.Equal(t, "RunScript", cmds[0].Method, "shell module must use RunScript()")
}
func TestModuleDifferentiation_Good_RawUsesRun(t *testing.T) {
func TestModulesCmd_ModuleDifferentiation_Good_RawUsesRun(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("echo test", "test\n", "", 0)
@ -603,10 +601,10 @@ func TestModuleDifferentiation_Good_RawUsesRun(t *testing.T) {
assert.Equal(t, "Run", cmds[0].Method, "raw module must use Run()")
}
func TestModuleDifferentiation_Good_ScriptUsesRunScript(t *testing.T) {
func TestModulesCmd_ModuleDifferentiation_Good_ScriptUsesRunScript(t *testing.T) {
tmpDir := t.TempDir()
scriptPath := filepath.Join(tmpDir, "test.sh")
require.NoError(t, os.WriteFile(scriptPath, []byte("echo test"), 0755))
scriptPath := joinPath(tmpDir, "test.sh")
require.NoError(t, writeTestFile(scriptPath, []byte("echo test"), 0755))
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("echo test", "test\n", "", 0)
@ -620,7 +618,7 @@ func TestModuleDifferentiation_Good_ScriptUsesRunScript(t *testing.T) {
// --- executeModuleWithMock dispatch tests ---
func TestExecuteModuleWithMock_Good_DispatchCommand(t *testing.T) {
func TestModulesCmd_ExecuteModuleWithMock_Good_DispatchCommand(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("uptime", "up 5 days\n", "", 0)
@ -636,7 +634,7 @@ func TestExecuteModuleWithMock_Good_DispatchCommand(t *testing.T) {
assert.Equal(t, "up 5 days\n", result.Stdout)
}
func TestExecuteModuleWithMock_Good_DispatchShell(t *testing.T) {
func TestModulesCmd_ExecuteModuleWithMock_Good_DispatchShell(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("ps aux", "root.*bash\n", "", 0)
@ -651,7 +649,7 @@ func TestExecuteModuleWithMock_Good_DispatchShell(t *testing.T) {
assert.True(t, result.Changed)
}
func TestExecuteModuleWithMock_Good_DispatchRaw(t *testing.T) {
func TestModulesCmd_ExecuteModuleWithMock_Good_DispatchRaw(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("cat /etc/hostname", "web01\n", "", 0)
@ -667,10 +665,10 @@ func TestExecuteModuleWithMock_Good_DispatchRaw(t *testing.T) {
assert.Equal(t, "web01\n", result.Stdout)
}
func TestExecuteModuleWithMock_Good_DispatchScript(t *testing.T) {
func TestModulesCmd_ExecuteModuleWithMock_Good_DispatchScript(t *testing.T) {
tmpDir := t.TempDir()
scriptPath := filepath.Join(tmpDir, "deploy.sh")
require.NoError(t, os.WriteFile(scriptPath, []byte("echo deploying"), 0755))
scriptPath := joinPath(tmpDir, "deploy.sh")
require.NoError(t, writeTestFile(scriptPath, []byte("echo deploying"), 0755))
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("deploying", "deploying\n", "", 0)
@ -686,7 +684,7 @@ func TestExecuteModuleWithMock_Good_DispatchScript(t *testing.T) {
assert.True(t, result.Changed)
}
func TestExecuteModuleWithMock_Bad_UnsupportedModule(t *testing.T) {
func TestModulesCmd_ExecuteModuleWithMock_Bad_UnsupportedModule(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
task := &Task{
@ -702,7 +700,7 @@ func TestExecuteModuleWithMock_Bad_UnsupportedModule(t *testing.T) {
// --- Template integration tests ---
func TestModuleCommand_Good_TemplatedArgs(t *testing.T) {
func TestModulesCmd_ModuleCommand_Good_TemplatedArgs(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
e.SetVar("service_name", "nginx")
mock.expectCommand("systemctl status nginx", "active\n", "", 0)

View file

@ -1,8 +1,7 @@
package ansible
import (
"os"
"path/filepath"
"io/fs"
"testing"
"github.com/stretchr/testify/assert"
@ -15,7 +14,7 @@ import (
// --- copy module ---
func TestModuleCopy_Good_ContentUpload(t *testing.T) {
func TestModulesFile_ModuleCopy_Good_ContentUpload(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleCopyWithClient(e, mock, map[string]any{
@ -33,13 +32,13 @@ func TestModuleCopy_Good_ContentUpload(t *testing.T) {
require.NotNil(t, up)
assert.Equal(t, "/etc/app/config", up.Remote)
assert.Equal(t, []byte("server_name=web01"), up.Content)
assert.Equal(t, os.FileMode(0644), up.Mode)
assert.Equal(t, fs.FileMode(0644), up.Mode)
}
func TestModuleCopy_Good_SrcFile(t *testing.T) {
func TestModulesFile_ModuleCopy_Good_SrcFile(t *testing.T) {
tmpDir := t.TempDir()
srcPath := filepath.Join(tmpDir, "nginx.conf")
require.NoError(t, os.WriteFile(srcPath, []byte("worker_processes auto;"), 0644))
srcPath := joinPath(tmpDir, "nginx.conf")
require.NoError(t, writeTestFile(srcPath, []byte("worker_processes auto;"), 0644))
e, mock := newTestExecutorWithMock("host1")
@ -57,7 +56,7 @@ func TestModuleCopy_Good_SrcFile(t *testing.T) {
assert.Equal(t, []byte("worker_processes auto;"), up.Content)
}
func TestModuleCopy_Good_OwnerGroup(t *testing.T) {
func TestModulesFile_ModuleCopy_Good_OwnerGroup(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleCopyWithClient(e, mock, map[string]any{
@ -76,7 +75,7 @@ func TestModuleCopy_Good_OwnerGroup(t *testing.T) {
assert.True(t, mock.hasExecuted(`chgrp appgroup "/opt/app/data.txt"`))
}
func TestModuleCopy_Good_CustomMode(t *testing.T) {
func TestModulesFile_ModuleCopy_Good_CustomMode(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleCopyWithClient(e, mock, map[string]any{
@ -90,10 +89,10 @@ func TestModuleCopy_Good_CustomMode(t *testing.T) {
up := mock.lastUpload()
require.NotNil(t, up)
assert.Equal(t, os.FileMode(0755), up.Mode)
assert.Equal(t, fs.FileMode(0755), up.Mode)
}
func TestModuleCopy_Bad_MissingDest(t *testing.T) {
func TestModulesFile_ModuleCopy_Bad_MissingDest(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
_, err := moduleCopyWithClient(e, mock, map[string]any{
@ -104,7 +103,7 @@ func TestModuleCopy_Bad_MissingDest(t *testing.T) {
assert.Contains(t, err.Error(), "dest required")
}
func TestModuleCopy_Bad_MissingSrcAndContent(t *testing.T) {
func TestModulesFile_ModuleCopy_Bad_MissingSrcAndContent(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
_, err := moduleCopyWithClient(e, mock, map[string]any{
@ -115,7 +114,7 @@ func TestModuleCopy_Bad_MissingSrcAndContent(t *testing.T) {
assert.Contains(t, err.Error(), "src or content required")
}
func TestModuleCopy_Bad_SrcFileNotFound(t *testing.T) {
func TestModulesFile_ModuleCopy_Bad_SrcFileNotFound(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
_, err := moduleCopyWithClient(e, mock, map[string]any{
@ -127,7 +126,7 @@ func TestModuleCopy_Bad_SrcFileNotFound(t *testing.T) {
assert.Contains(t, err.Error(), "read src")
}
func TestModuleCopy_Good_ContentTakesPrecedenceOverSrc(t *testing.T) {
func TestModulesFile_ModuleCopy_Good_ContentTakesPrecedenceOverSrc(t *testing.T) {
// When both content and src are given, src is checked first in the implementation
// but if src is empty string, content is used
e, mock := newTestExecutorWithMock("host1")
@ -145,7 +144,7 @@ func TestModuleCopy_Good_ContentTakesPrecedenceOverSrc(t *testing.T) {
// --- file module ---
func TestModuleFile_Good_StateDirectory(t *testing.T) {
func TestModulesFile_ModuleFile_Good_StateDirectory(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{
@ -161,7 +160,7 @@ func TestModuleFile_Good_StateDirectory(t *testing.T) {
assert.True(t, mock.hasExecuted(`chmod 0755`))
}
func TestModuleFile_Good_StateDirectoryCustomMode(t *testing.T) {
func TestModulesFile_ModuleFile_Good_StateDirectoryCustomMode(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{
@ -175,7 +174,7 @@ func TestModuleFile_Good_StateDirectoryCustomMode(t *testing.T) {
assert.True(t, mock.hasExecuted(`mkdir -p "/opt/data" && chmod 0700 "/opt/data"`))
}
func TestModuleFile_Good_StateAbsent(t *testing.T) {
func TestModulesFile_ModuleFile_Good_StateAbsent(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{
@ -188,7 +187,7 @@ func TestModuleFile_Good_StateAbsent(t *testing.T) {
assert.True(t, mock.hasExecuted(`rm -rf "/tmp/old-dir"`))
}
func TestModuleFile_Good_StateTouch(t *testing.T) {
func TestModulesFile_ModuleFile_Good_StateTouch(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{
@ -201,7 +200,7 @@ func TestModuleFile_Good_StateTouch(t *testing.T) {
assert.True(t, mock.hasExecuted(`touch "/var/log/app.log"`))
}
func TestModuleFile_Good_StateLink(t *testing.T) {
func TestModulesFile_ModuleFile_Good_StateLink(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{
@ -215,7 +214,7 @@ func TestModuleFile_Good_StateLink(t *testing.T) {
assert.True(t, mock.hasExecuted(`ln -sf "/opt/node/bin/node" "/usr/local/bin/node"`))
}
func TestModuleFile_Bad_LinkMissingSrc(t *testing.T) {
func TestModulesFile_ModuleFile_Bad_LinkMissingSrc(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
_, err := moduleFileWithClient(e, mock, map[string]any{
@ -227,7 +226,7 @@ func TestModuleFile_Bad_LinkMissingSrc(t *testing.T) {
assert.Contains(t, err.Error(), "src required for link state")
}
func TestModuleFile_Good_OwnerGroupMode(t *testing.T) {
func TestModulesFile_ModuleFile_Good_OwnerGroupMode(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{
@ -247,7 +246,7 @@ func TestModuleFile_Good_OwnerGroupMode(t *testing.T) {
assert.True(t, mock.hasExecuted(`chgrp www-data "/var/lib/app/data"`))
}
func TestModuleFile_Good_RecurseOwner(t *testing.T) {
func TestModulesFile_ModuleFile_Good_RecurseOwner(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{
@ -265,7 +264,7 @@ func TestModuleFile_Good_RecurseOwner(t *testing.T) {
assert.True(t, mock.hasExecuted(`chown -R www-data "/var/www"`))
}
func TestModuleFile_Bad_MissingPath(t *testing.T) {
func TestModulesFile_ModuleFile_Bad_MissingPath(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
_, err := moduleFileWithClient(e, mock, map[string]any{
@ -276,7 +275,7 @@ func TestModuleFile_Bad_MissingPath(t *testing.T) {
assert.Contains(t, err.Error(), "path required")
}
func TestModuleFile_Good_DestAliasForPath(t *testing.T) {
func TestModulesFile_ModuleFile_Good_DestAliasForPath(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{
@ -289,7 +288,7 @@ func TestModuleFile_Good_DestAliasForPath(t *testing.T) {
assert.True(t, mock.hasExecuted(`mkdir -p "/opt/myapp"`))
}
func TestModuleFile_Good_StateFileWithMode(t *testing.T) {
func TestModulesFile_ModuleFile_Good_StateFileWithMode(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{
@ -303,7 +302,7 @@ func TestModuleFile_Good_StateFileWithMode(t *testing.T) {
assert.True(t, mock.hasExecuted(`chmod 0600 "/etc/config.yml"`))
}
func TestModuleFile_Good_DirectoryCommandFailure(t *testing.T) {
func TestModulesFile_ModuleFile_Good_DirectoryCommandFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("mkdir", "", "permission denied", 1)
@ -319,7 +318,7 @@ func TestModuleFile_Good_DirectoryCommandFailure(t *testing.T) {
// --- lineinfile module ---
func TestModuleLineinfile_Good_InsertLine(t *testing.T) {
func TestModulesFile_ModuleLineinfile_Good_InsertLine(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleLineinfileWithClient(e, mock, map[string]any{
@ -335,7 +334,7 @@ func TestModuleLineinfile_Good_InsertLine(t *testing.T) {
assert.True(t, mock.hasExecuted(`192.168.1.100 web01`))
}
func TestModuleLineinfile_Good_ReplaceRegexp(t *testing.T) {
func TestModulesFile_ModuleLineinfile_Good_ReplaceRegexp(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleLineinfileWithClient(e, mock, map[string]any{
@ -351,7 +350,7 @@ func TestModuleLineinfile_Good_ReplaceRegexp(t *testing.T) {
assert.True(t, mock.hasExecuted(`sed -i 's/\^#\?PermitRootLogin/PermitRootLogin no/'`))
}
func TestModuleLineinfile_Good_RemoveLine(t *testing.T) {
func TestModulesFile_ModuleLineinfile_Good_RemoveLine(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleLineinfileWithClient(e, mock, map[string]any{
@ -368,7 +367,7 @@ func TestModuleLineinfile_Good_RemoveLine(t *testing.T) {
assert.True(t, mock.hasExecuted(`/d'`))
}
func TestModuleLineinfile_Good_RegexpFallsBackToAppend(t *testing.T) {
func TestModulesFile_ModuleLineinfile_Good_RegexpFallsBackToAppend(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
// Simulate sed returning non-zero (pattern not found)
mock.expectCommand("sed -i", "", "", 1)
@ -388,7 +387,7 @@ func TestModuleLineinfile_Good_RegexpFallsBackToAppend(t *testing.T) {
assert.True(t, mock.hasExecuted(`echo`))
}
func TestModuleLineinfile_Bad_MissingPath(t *testing.T) {
func TestModulesFile_ModuleLineinfile_Bad_MissingPath(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
_, err := moduleLineinfileWithClient(e, mock, map[string]any{
@ -399,7 +398,7 @@ func TestModuleLineinfile_Bad_MissingPath(t *testing.T) {
assert.Contains(t, err.Error(), "path required")
}
func TestModuleLineinfile_Good_DestAliasForPath(t *testing.T) {
func TestModulesFile_ModuleLineinfile_Good_DestAliasForPath(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleLineinfileWithClient(e, mock, map[string]any{
@ -412,7 +411,7 @@ func TestModuleLineinfile_Good_DestAliasForPath(t *testing.T) {
assert.True(t, mock.hasExecuted(`/etc/config`))
}
func TestModuleLineinfile_Good_AbsentWithNoRegexp(t *testing.T) {
func TestModulesFile_ModuleLineinfile_Good_AbsentWithNoRegexp(t *testing.T) {
// When state=absent but no regexp, nothing happens (no commands)
e, mock := newTestExecutorWithMock("host1")
@ -426,7 +425,7 @@ func TestModuleLineinfile_Good_AbsentWithNoRegexp(t *testing.T) {
assert.Equal(t, 0, mock.commandCount())
}
func TestModuleLineinfile_Good_LineWithSlashes(t *testing.T) {
func TestModulesFile_ModuleLineinfile_Good_LineWithSlashes(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleLineinfileWithClient(e, mock, map[string]any{
@ -444,7 +443,7 @@ func TestModuleLineinfile_Good_LineWithSlashes(t *testing.T) {
// --- blockinfile module ---
func TestModuleBlockinfile_Good_InsertBlock(t *testing.T) {
func TestModulesFile_ModuleBlockinfile_Good_InsertBlock(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleBlockinfileWithClient(e, mock, map[string]any{
@ -461,7 +460,7 @@ func TestModuleBlockinfile_Good_InsertBlock(t *testing.T) {
assert.True(t, mock.hasExecutedMethod("RunScript", "10\\.0\\.0\\.1"))
}
func TestModuleBlockinfile_Good_CustomMarkers(t *testing.T) {
func TestModulesFile_ModuleBlockinfile_Good_CustomMarkers(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleBlockinfileWithClient(e, mock, map[string]any{
@ -478,7 +477,7 @@ func TestModuleBlockinfile_Good_CustomMarkers(t *testing.T) {
assert.True(t, mock.hasExecutedMethod("RunScript", "# END managed by devops"))
}
func TestModuleBlockinfile_Good_RemoveBlock(t *testing.T) {
func TestModulesFile_ModuleBlockinfile_Good_RemoveBlock(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleBlockinfileWithClient(e, mock, map[string]any{
@ -493,7 +492,7 @@ func TestModuleBlockinfile_Good_RemoveBlock(t *testing.T) {
assert.True(t, mock.hasExecuted(`sed -i '/.*BEGIN ANSIBLE MANAGED BLOCK/,/.*END ANSIBLE MANAGED BLOCK/d'`))
}
func TestModuleBlockinfile_Good_CreateFile(t *testing.T) {
func TestModulesFile_ModuleBlockinfile_Good_CreateFile(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleBlockinfileWithClient(e, mock, map[string]any{
@ -509,7 +508,7 @@ func TestModuleBlockinfile_Good_CreateFile(t *testing.T) {
assert.True(t, mock.hasExecuted(`touch "/etc/new-config"`))
}
func TestModuleBlockinfile_Bad_MissingPath(t *testing.T) {
func TestModulesFile_ModuleBlockinfile_Bad_MissingPath(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
_, err := moduleBlockinfileWithClient(e, mock, map[string]any{
@ -520,7 +519,7 @@ func TestModuleBlockinfile_Bad_MissingPath(t *testing.T) {
assert.Contains(t, err.Error(), "path required")
}
func TestModuleBlockinfile_Good_DestAliasForPath(t *testing.T) {
func TestModulesFile_ModuleBlockinfile_Good_DestAliasForPath(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleBlockinfileWithClient(e, mock, map[string]any{
@ -532,7 +531,7 @@ func TestModuleBlockinfile_Good_DestAliasForPath(t *testing.T) {
assert.True(t, result.Changed)
}
func TestModuleBlockinfile_Good_ScriptFailure(t *testing.T) {
func TestModulesFile_ModuleBlockinfile_Good_ScriptFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("BLOCK_EOF", "", "write error", 1)
@ -548,7 +547,7 @@ func TestModuleBlockinfile_Good_ScriptFailure(t *testing.T) {
// --- stat module ---
func TestModuleStat_Good_ExistingFile(t *testing.T) {
func TestModulesFile_ModuleStat_Good_ExistingFile(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.addStat("/etc/nginx/nginx.conf", map[string]any{
"exists": true,
@ -575,7 +574,7 @@ func TestModuleStat_Good_ExistingFile(t *testing.T) {
assert.Equal(t, 1234, stat["size"])
}
func TestModuleStat_Good_MissingFile(t *testing.T) {
func TestModulesFile_ModuleStat_Good_MissingFile(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleStatWithClient(e, mock, map[string]any{
@ -591,7 +590,7 @@ func TestModuleStat_Good_MissingFile(t *testing.T) {
assert.Equal(t, false, stat["exists"])
}
func TestModuleStat_Good_Directory(t *testing.T) {
func TestModulesFile_ModuleStat_Good_Directory(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.addStat("/var/log", map[string]any{
"exists": true,
@ -609,7 +608,7 @@ func TestModuleStat_Good_Directory(t *testing.T) {
assert.Equal(t, true, stat["isdir"])
}
func TestModuleStat_Good_FallbackFromFileSystem(t *testing.T) {
func TestModulesFile_ModuleStat_Good_FallbackFromFileSystem(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
// No explicit stat, but add a file — stat falls back to file existence
mock.addFile("/etc/hosts", []byte("127.0.0.1 localhost"))
@ -624,7 +623,7 @@ func TestModuleStat_Good_FallbackFromFileSystem(t *testing.T) {
assert.Equal(t, false, stat["isdir"])
}
func TestModuleStat_Bad_MissingPath(t *testing.T) {
func TestModulesFile_ModuleStat_Bad_MissingPath(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
_, err := moduleStatWithClient(e, mock, map[string]any{})
@ -635,10 +634,10 @@ func TestModuleStat_Bad_MissingPath(t *testing.T) {
// --- template module ---
func TestModuleTemplate_Good_BasicTemplate(t *testing.T) {
func TestModulesFile_ModuleTemplate_Good_BasicTemplate(t *testing.T) {
tmpDir := t.TempDir()
srcPath := filepath.Join(tmpDir, "app.conf.j2")
require.NoError(t, os.WriteFile(srcPath, []byte("server_name={{ server_name }};"), 0644))
srcPath := joinPath(tmpDir, "app.conf.j2")
require.NoError(t, writeTestFile(srcPath, []byte("server_name={{ server_name }};"), 0644))
e, mock := newTestExecutorWithMock("host1")
e.SetVar("server_name", "web01.example.com")
@ -661,10 +660,10 @@ func TestModuleTemplate_Good_BasicTemplate(t *testing.T) {
assert.Contains(t, string(up.Content), "web01.example.com")
}
func TestModuleTemplate_Good_CustomMode(t *testing.T) {
func TestModulesFile_ModuleTemplate_Good_CustomMode(t *testing.T) {
tmpDir := t.TempDir()
srcPath := filepath.Join(tmpDir, "script.sh.j2")
require.NoError(t, os.WriteFile(srcPath, []byte("#!/bin/bash\necho done"), 0644))
srcPath := joinPath(tmpDir, "script.sh.j2")
require.NoError(t, writeTestFile(srcPath, []byte("#!/bin/bash\necho done"), 0644))
e, mock := newTestExecutorWithMock("host1")
@ -679,10 +678,10 @@ func TestModuleTemplate_Good_CustomMode(t *testing.T) {
up := mock.lastUpload()
require.NotNil(t, up)
assert.Equal(t, os.FileMode(0755), up.Mode)
assert.Equal(t, fs.FileMode(0755), up.Mode)
}
func TestModuleTemplate_Bad_MissingSrc(t *testing.T) {
func TestModulesFile_ModuleTemplate_Bad_MissingSrc(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
_, err := moduleTemplateWithClient(e, mock, map[string]any{
@ -693,7 +692,7 @@ func TestModuleTemplate_Bad_MissingSrc(t *testing.T) {
assert.Contains(t, err.Error(), "src and dest required")
}
func TestModuleTemplate_Bad_MissingDest(t *testing.T) {
func TestModulesFile_ModuleTemplate_Bad_MissingDest(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
_, err := moduleTemplateWithClient(e, mock, map[string]any{
@ -704,7 +703,7 @@ func TestModuleTemplate_Bad_MissingDest(t *testing.T) {
assert.Contains(t, err.Error(), "src and dest required")
}
func TestModuleTemplate_Bad_SrcFileNotFound(t *testing.T) {
func TestModulesFile_ModuleTemplate_Bad_SrcFileNotFound(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
_, err := moduleTemplateWithClient(e, mock, map[string]any{
@ -716,11 +715,11 @@ func TestModuleTemplate_Bad_SrcFileNotFound(t *testing.T) {
assert.Contains(t, err.Error(), "template")
}
func TestModuleTemplate_Good_PlainTextNoVars(t *testing.T) {
func TestModulesFile_ModuleTemplate_Good_PlainTextNoVars(t *testing.T) {
tmpDir := t.TempDir()
srcPath := filepath.Join(tmpDir, "static.conf")
srcPath := joinPath(tmpDir, "static.conf")
content := "listen 80;\nserver_name localhost;"
require.NoError(t, os.WriteFile(srcPath, []byte(content), 0644))
require.NoError(t, writeTestFile(srcPath, []byte(content), 0644))
e, mock := newTestExecutorWithMock("host1")
@ -739,7 +738,7 @@ func TestModuleTemplate_Good_PlainTextNoVars(t *testing.T) {
// --- Cross-module dispatch tests for file modules ---
func TestExecuteModuleWithMock_Good_DispatchCopy(t *testing.T) {
func TestModulesFile_ExecuteModuleWithMock_Good_DispatchCopy(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
task := &Task{
@ -757,7 +756,7 @@ func TestExecuteModuleWithMock_Good_DispatchCopy(t *testing.T) {
assert.Equal(t, 1, mock.uploadCount())
}
func TestExecuteModuleWithMock_Good_DispatchFile(t *testing.T) {
func TestModulesFile_ExecuteModuleWithMock_Good_DispatchFile(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
task := &Task{
@ -775,7 +774,7 @@ func TestExecuteModuleWithMock_Good_DispatchFile(t *testing.T) {
assert.True(t, mock.hasExecuted("mkdir"))
}
func TestExecuteModuleWithMock_Good_DispatchStat(t *testing.T) {
func TestModulesFile_ExecuteModuleWithMock_Good_DispatchStat(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.addStat("/etc/hosts", map[string]any{"exists": true, "isdir": false})
@ -794,7 +793,7 @@ func TestExecuteModuleWithMock_Good_DispatchStat(t *testing.T) {
assert.Equal(t, true, stat["exists"])
}
func TestExecuteModuleWithMock_Good_DispatchLineinfile(t *testing.T) {
func TestModulesFile_ExecuteModuleWithMock_Good_DispatchLineinfile(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
task := &Task{
@ -811,7 +810,7 @@ func TestExecuteModuleWithMock_Good_DispatchLineinfile(t *testing.T) {
assert.True(t, result.Changed)
}
func TestExecuteModuleWithMock_Good_DispatchBlockinfile(t *testing.T) {
func TestModulesFile_ExecuteModuleWithMock_Good_DispatchBlockinfile(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
task := &Task{
@ -828,10 +827,10 @@ func TestExecuteModuleWithMock_Good_DispatchBlockinfile(t *testing.T) {
assert.True(t, result.Changed)
}
func TestExecuteModuleWithMock_Good_DispatchTemplate(t *testing.T) {
func TestModulesFile_ExecuteModuleWithMock_Good_DispatchTemplate(t *testing.T) {
tmpDir := t.TempDir()
srcPath := filepath.Join(tmpDir, "test.j2")
require.NoError(t, os.WriteFile(srcPath, []byte("static content"), 0644))
srcPath := joinPath(tmpDir, "test.j2")
require.NoError(t, writeTestFile(srcPath, []byte("static content"), 0644))
e, mock := newTestExecutorWithMock("host1")
@ -852,7 +851,7 @@ func TestExecuteModuleWithMock_Good_DispatchTemplate(t *testing.T) {
// --- Template variable resolution integration ---
func TestModuleCopy_Good_TemplatedArgs(t *testing.T) {
func TestModulesFile_ModuleCopy_Good_TemplatedArgs(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
e.SetVar("deploy_path", "/opt/myapp")
@ -876,7 +875,7 @@ func TestModuleCopy_Good_TemplatedArgs(t *testing.T) {
assert.Equal(t, "/opt/myapp/config.yml", up.Remote)
}
func TestModuleFile_Good_TemplatedPath(t *testing.T) {
func TestModulesFile_ModuleFile_Good_TemplatedPath(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
e.SetVar("app_dir", "/var/www/html")

View file

@ -11,7 +11,7 @@ import (
// 1. Error Propagation — getHosts
// ===========================================================================
func TestGetHosts_Infra_Good_AllPattern(t *testing.T) {
func TestModulesInfra_GetHosts_Good_AllPattern(t *testing.T) {
e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{
@ -30,7 +30,7 @@ func TestGetHosts_Infra_Good_AllPattern(t *testing.T) {
assert.Contains(t, hosts, "db1")
}
func TestGetHosts_Infra_Good_SpecificHost(t *testing.T) {
func TestModulesInfra_GetHosts_Good_SpecificHost(t *testing.T) {
e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{
@ -45,7 +45,7 @@ func TestGetHosts_Infra_Good_SpecificHost(t *testing.T) {
assert.Equal(t, []string{"web1"}, hosts)
}
func TestGetHosts_Infra_Good_GroupName(t *testing.T) {
func TestModulesInfra_GetHosts_Good_GroupName(t *testing.T) {
e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{
@ -71,21 +71,21 @@ func TestGetHosts_Infra_Good_GroupName(t *testing.T) {
assert.Contains(t, hosts, "web2")
}
func TestGetHosts_Infra_Good_Localhost(t *testing.T) {
func TestModulesInfra_GetHosts_Good_Localhost(t *testing.T) {
e := NewExecutor("/tmp")
// No inventory at all
hosts := e.getHosts("localhost")
assert.Equal(t, []string{"localhost"}, hosts)
}
func TestGetHosts_Infra_Bad_NilInventory(t *testing.T) {
func TestModulesInfra_GetHosts_Bad_NilInventory(t *testing.T) {
e := NewExecutor("/tmp")
// inventory is nil, non-localhost pattern
hosts := e.getHosts("webservers")
assert.Nil(t, hosts)
}
func TestGetHosts_Infra_Bad_NonexistentHost(t *testing.T) {
func TestModulesInfra_GetHosts_Bad_NonexistentHost(t *testing.T) {
e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{
@ -99,7 +99,7 @@ func TestGetHosts_Infra_Bad_NonexistentHost(t *testing.T) {
assert.Empty(t, hosts)
}
func TestGetHosts_Infra_Good_LimitFiltering(t *testing.T) {
func TestModulesInfra_GetHosts_Good_LimitFiltering(t *testing.T) {
e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{
@ -117,13 +117,13 @@ func TestGetHosts_Infra_Good_LimitFiltering(t *testing.T) {
assert.Contains(t, hosts, "web1")
}
func TestGetHosts_Infra_Good_LimitSubstringMatch(t *testing.T) {
func TestModulesInfra_GetHosts_Good_LimitSubstringMatch(t *testing.T) {
e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{
Hosts: map[string]*Host{
"prod-web-01": {},
"prod-web-02": {},
"prod-web-01": {},
"prod-web-02": {},
"staging-web-01": {},
},
},
@ -139,18 +139,18 @@ func TestGetHosts_Infra_Good_LimitSubstringMatch(t *testing.T) {
// 1. Error Propagation — matchesTags
// ===========================================================================
func TestMatchesTags_Infra_Good_NoFiltersNoTags(t *testing.T) {
func TestModulesInfra_MatchesTags_Good_NoFiltersNoTags(t *testing.T) {
e := NewExecutor("/tmp")
// No Tags, no SkipTags set
assert.True(t, e.matchesTags(nil))
}
func TestMatchesTags_Infra_Good_NoFiltersWithTaskTags(t *testing.T) {
func TestModulesInfra_MatchesTags_Good_NoFiltersWithTaskTags(t *testing.T) {
e := NewExecutor("/tmp")
assert.True(t, e.matchesTags([]string{"deploy", "config"}))
}
func TestMatchesTags_Infra_Good_IncludeMatchesOneOfMultiple(t *testing.T) {
func TestModulesInfra_MatchesTags_Good_IncludeMatchesOneOfMultiple(t *testing.T) {
e := NewExecutor("/tmp")
e.Tags = []string{"deploy"}
@ -158,14 +158,14 @@ func TestMatchesTags_Infra_Good_IncludeMatchesOneOfMultiple(t *testing.T) {
assert.True(t, e.matchesTags([]string{"setup", "deploy", "config"}))
}
func TestMatchesTags_Infra_Bad_IncludeNoMatch(t *testing.T) {
func TestModulesInfra_MatchesTags_Bad_IncludeNoMatch(t *testing.T) {
e := NewExecutor("/tmp")
e.Tags = []string{"deploy"}
assert.False(t, e.matchesTags([]string{"build", "test"}))
}
func TestMatchesTags_Infra_Good_SkipOverridesInclude(t *testing.T) {
func TestModulesInfra_MatchesTags_Good_SkipOverridesInclude(t *testing.T) {
e := NewExecutor("/tmp")
e.SkipTags = []string{"slow"}
@ -174,7 +174,7 @@ func TestMatchesTags_Infra_Good_SkipOverridesInclude(t *testing.T) {
assert.True(t, e.matchesTags([]string{"deploy", "fast"}))
}
func TestMatchesTags_Infra_Bad_IncludeFilterNoTaskTags(t *testing.T) {
func TestModulesInfra_MatchesTags_Bad_IncludeFilterNoTaskTags(t *testing.T) {
e := NewExecutor("/tmp")
e.Tags = []string{"deploy"}
@ -183,7 +183,7 @@ func TestMatchesTags_Infra_Bad_IncludeFilterNoTaskTags(t *testing.T) {
assert.False(t, e.matchesTags([]string{}))
}
func TestMatchesTags_Infra_Good_AllTagMatchesEverything(t *testing.T) {
func TestModulesInfra_MatchesTags_Good_AllTagMatchesEverything(t *testing.T) {
e := NewExecutor("/tmp")
e.Tags = []string{"all"}
@ -195,7 +195,7 @@ func TestMatchesTags_Infra_Good_AllTagMatchesEverything(t *testing.T) {
// 1. Error Propagation — evaluateWhen
// ===========================================================================
func TestEvaluateWhen_Infra_Good_DefinedCheck(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_DefinedCheck(t *testing.T) {
e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{
"myresult": {Changed: true},
@ -204,18 +204,18 @@ func TestEvaluateWhen_Infra_Good_DefinedCheck(t *testing.T) {
assert.True(t, e.evaluateWhen("myresult is defined", "host1", nil))
}
func TestEvaluateWhen_Infra_Good_NotDefinedCheck(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_NotDefinedCheck(t *testing.T) {
e := NewExecutor("/tmp")
// No results registered for host1
assert.True(t, e.evaluateWhen("missing_var is not defined", "host1", nil))
}
func TestEvaluateWhen_Infra_Good_UndefinedAlias(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_UndefinedAlias(t *testing.T) {
e := NewExecutor("/tmp")
assert.True(t, e.evaluateWhen("some_var is undefined", "host1", nil))
}
func TestEvaluateWhen_Infra_Good_SucceededCheck(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_SucceededCheck(t *testing.T) {
e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{
"result": {Failed: false, Changed: true},
@ -225,7 +225,7 @@ func TestEvaluateWhen_Infra_Good_SucceededCheck(t *testing.T) {
assert.True(t, e.evaluateWhen("result is succeeded", "host1", nil))
}
func TestEvaluateWhen_Infra_Good_FailedCheck(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_FailedCheck(t *testing.T) {
e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{
"result": {Failed: true},
@ -234,7 +234,7 @@ func TestEvaluateWhen_Infra_Good_FailedCheck(t *testing.T) {
assert.True(t, e.evaluateWhen("result is failed", "host1", nil))
}
func TestEvaluateWhen_Infra_Good_ChangedCheck(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_ChangedCheck(t *testing.T) {
e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{
"result": {Changed: true},
@ -243,7 +243,7 @@ func TestEvaluateWhen_Infra_Good_ChangedCheck(t *testing.T) {
assert.True(t, e.evaluateWhen("result is changed", "host1", nil))
}
func TestEvaluateWhen_Infra_Good_SkippedCheck(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_SkippedCheck(t *testing.T) {
e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{
"result": {Skipped: true},
@ -252,33 +252,33 @@ func TestEvaluateWhen_Infra_Good_SkippedCheck(t *testing.T) {
assert.True(t, e.evaluateWhen("result is skipped", "host1", nil))
}
func TestEvaluateWhen_Infra_Good_BoolVarTruthy(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_BoolVarTruthy(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["my_flag"] = true
assert.True(t, e.evalCondition("my_flag", "host1"))
}
func TestEvaluateWhen_Infra_Good_BoolVarFalsy(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_BoolVarFalsy(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["my_flag"] = false
assert.False(t, e.evalCondition("my_flag", "host1"))
}
func TestEvaluateWhen_Infra_Good_StringVarTruthy(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_StringVarTruthy(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["my_str"] = "hello"
assert.True(t, e.evalCondition("my_str", "host1"))
}
func TestEvaluateWhen_Infra_Good_StringVarEmptyFalsy(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_StringVarEmptyFalsy(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["my_str"] = ""
assert.False(t, e.evalCondition("my_str", "host1"))
}
func TestEvaluateWhen_Infra_Good_StringVarFalseLiteral(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_StringVarFalseLiteral(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["my_str"] = "false"
assert.False(t, e.evalCondition("my_str", "host1"))
@ -287,25 +287,25 @@ func TestEvaluateWhen_Infra_Good_StringVarFalseLiteral(t *testing.T) {
assert.False(t, e.evalCondition("my_str2", "host1"))
}
func TestEvaluateWhen_Infra_Good_IntVarNonZero(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_IntVarNonZero(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["count"] = 42
assert.True(t, e.evalCondition("count", "host1"))
}
func TestEvaluateWhen_Infra_Good_IntVarZero(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_IntVarZero(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["count"] = 0
assert.False(t, e.evalCondition("count", "host1"))
}
func TestEvaluateWhen_Infra_Good_Negation(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_Negation(t *testing.T) {
e := NewExecutor("/tmp")
assert.False(t, e.evalCondition("not true", "host1"))
assert.True(t, e.evalCondition("not false", "host1"))
}
func TestEvaluateWhen_Infra_Good_MultipleConditionsAllTrue(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_MultipleConditionsAllTrue(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["enabled"] = true
e.results["host1"] = map[string]*TaskResult{
@ -316,7 +316,7 @@ func TestEvaluateWhen_Infra_Good_MultipleConditionsAllTrue(t *testing.T) {
assert.True(t, e.evaluateWhen([]any{"enabled", "prev is success"}, "host1", nil))
}
func TestEvaluateWhen_Infra_Bad_MultipleConditionsOneFails(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Bad_MultipleConditionsOneFails(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["enabled"] = true
@ -324,13 +324,13 @@ func TestEvaluateWhen_Infra_Bad_MultipleConditionsOneFails(t *testing.T) {
assert.False(t, e.evaluateWhen([]any{"enabled", "false"}, "host1", nil))
}
func TestEvaluateWhen_Infra_Good_DefaultFilterInCondition(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_DefaultFilterInCondition(t *testing.T) {
e := NewExecutor("/tmp")
// Condition with default filter should be satisfied
assert.True(t, e.evalCondition("my_var | default(true)", "host1"))
}
func TestEvaluateWhen_Infra_Good_RegisteredVarTruthy(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Good_RegisteredVarTruthy(t *testing.T) {
e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{
"check_result": {Failed: false, Skipped: false},
@ -340,7 +340,7 @@ func TestEvaluateWhen_Infra_Good_RegisteredVarTruthy(t *testing.T) {
assert.True(t, e.evalCondition("check_result", "host1"))
}
func TestEvaluateWhen_Infra_Bad_RegisteredVarFailedFalsy(t *testing.T) {
func TestModulesInfra_EvaluateWhen_Bad_RegisteredVarFailedFalsy(t *testing.T) {
e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{
"check_result": {Failed: true},
@ -354,7 +354,7 @@ func TestEvaluateWhen_Infra_Bad_RegisteredVarFailedFalsy(t *testing.T) {
// 1. Error Propagation — templateString
// ===========================================================================
func TestTemplateString_Infra_Good_SimpleSubstitution(t *testing.T) {
func TestModulesInfra_TemplateString_Good_SimpleSubstitution(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["app_name"] = "myapp"
@ -362,7 +362,7 @@ func TestTemplateString_Infra_Good_SimpleSubstitution(t *testing.T) {
assert.Equal(t, "Deploying myapp", result)
}
func TestTemplateString_Infra_Good_MultipleVars(t *testing.T) {
func TestModulesInfra_TemplateString_Good_MultipleVars(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["host"] = "db.example.com"
e.vars["port"] = 5432
@ -371,19 +371,19 @@ func TestTemplateString_Infra_Good_MultipleVars(t *testing.T) {
assert.Equal(t, "postgresql://db.example.com:5432/mydb", result)
}
func TestTemplateString_Infra_Good_Unresolved(t *testing.T) {
func TestModulesInfra_TemplateString_Good_Unresolved(t *testing.T) {
e := NewExecutor("/tmp")
result := e.templateString("{{ missing_var }}", "", nil)
assert.Equal(t, "{{ missing_var }}", result)
}
func TestTemplateString_Infra_Good_NoTemplateMarkup(t *testing.T) {
func TestModulesInfra_TemplateString_Good_NoTemplateMarkup(t *testing.T) {
e := NewExecutor("/tmp")
result := e.templateString("just a plain string", "", nil)
assert.Equal(t, "just a plain string", result)
}
func TestTemplateString_Infra_Good_RegisteredVarStdout(t *testing.T) {
func TestModulesInfra_TemplateString_Good_RegisteredVarStdout(t *testing.T) {
e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{
"cmd_result": {Stdout: "42"},
@ -393,7 +393,7 @@ func TestTemplateString_Infra_Good_RegisteredVarStdout(t *testing.T) {
assert.Equal(t, "42", result)
}
func TestTemplateString_Infra_Good_RegisteredVarRC(t *testing.T) {
func TestModulesInfra_TemplateString_Good_RegisteredVarRC(t *testing.T) {
e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{
"cmd_result": {RC: 0},
@ -403,7 +403,7 @@ func TestTemplateString_Infra_Good_RegisteredVarRC(t *testing.T) {
assert.Equal(t, "0", result)
}
func TestTemplateString_Infra_Good_RegisteredVarChanged(t *testing.T) {
func TestModulesInfra_TemplateString_Good_RegisteredVarChanged(t *testing.T) {
e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{
"cmd_result": {Changed: true},
@ -413,7 +413,7 @@ func TestTemplateString_Infra_Good_RegisteredVarChanged(t *testing.T) {
assert.Equal(t, "true", result)
}
func TestTemplateString_Infra_Good_RegisteredVarFailed(t *testing.T) {
func TestModulesInfra_TemplateString_Good_RegisteredVarFailed(t *testing.T) {
e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{
"cmd_result": {Failed: true},
@ -423,7 +423,7 @@ func TestTemplateString_Infra_Good_RegisteredVarFailed(t *testing.T) {
assert.Equal(t, "true", result)
}
func TestTemplateString_Infra_Good_TaskVars(t *testing.T) {
func TestModulesInfra_TemplateString_Good_TaskVars(t *testing.T) {
e := NewExecutor("/tmp")
task := &Task{
Vars: map[string]any{
@ -435,7 +435,7 @@ func TestTemplateString_Infra_Good_TaskVars(t *testing.T) {
assert.Equal(t, "task_value", result)
}
func TestTemplateString_Infra_Good_FactsResolution(t *testing.T) {
func TestModulesInfra_TemplateString_Good_FactsResolution(t *testing.T) {
e := NewExecutor("/tmp")
e.facts["host1"] = &Facts{
Hostname: "web1",
@ -458,24 +458,24 @@ func TestTemplateString_Infra_Good_FactsResolution(t *testing.T) {
// 1. Error Propagation — applyFilter
// ===========================================================================
func TestApplyFilter_Infra_Good_DefaultWithValue(t *testing.T) {
func TestModulesInfra_ApplyFilter_Good_DefaultWithValue(t *testing.T) {
e := NewExecutor("/tmp")
// When value is non-empty, default is not applied
assert.Equal(t, "hello", e.applyFilter("hello", "default('fallback')"))
}
func TestApplyFilter_Infra_Good_DefaultWithEmpty(t *testing.T) {
func TestModulesInfra_ApplyFilter_Good_DefaultWithEmpty(t *testing.T) {
e := NewExecutor("/tmp")
// When value is empty, default IS applied
assert.Equal(t, "fallback", e.applyFilter("", "default('fallback')"))
}
func TestApplyFilter_Infra_Good_DefaultWithDoubleQuotes(t *testing.T) {
func TestModulesInfra_ApplyFilter_Good_DefaultWithDoubleQuotes(t *testing.T) {
e := NewExecutor("/tmp")
assert.Equal(t, "fallback", e.applyFilter("", `default("fallback")`))
}
func TestApplyFilter_Infra_Good_BoolFilterTrue(t *testing.T) {
func TestModulesInfra_ApplyFilter_Good_BoolFilterTrue(t *testing.T) {
e := NewExecutor("/tmp")
assert.Equal(t, "true", e.applyFilter("true", "bool"))
assert.Equal(t, "true", e.applyFilter("True", "bool"))
@ -484,7 +484,7 @@ func TestApplyFilter_Infra_Good_BoolFilterTrue(t *testing.T) {
assert.Equal(t, "true", e.applyFilter("1", "bool"))
}
func TestApplyFilter_Infra_Good_BoolFilterFalse(t *testing.T) {
func TestModulesInfra_ApplyFilter_Good_BoolFilterFalse(t *testing.T) {
e := NewExecutor("/tmp")
assert.Equal(t, "false", e.applyFilter("false", "bool"))
assert.Equal(t, "false", e.applyFilter("no", "bool"))
@ -492,26 +492,26 @@ func TestApplyFilter_Infra_Good_BoolFilterFalse(t *testing.T) {
assert.Equal(t, "false", e.applyFilter("random", "bool"))
}
func TestApplyFilter_Infra_Good_TrimFilter(t *testing.T) {
func TestModulesInfra_ApplyFilter_Good_TrimFilter(t *testing.T) {
e := NewExecutor("/tmp")
assert.Equal(t, "hello", e.applyFilter(" hello ", "trim"))
assert.Equal(t, "no spaces", e.applyFilter("no spaces", "trim"))
assert.Equal(t, "", e.applyFilter(" ", "trim"))
}
func TestApplyFilter_Infra_Good_B64Decode(t *testing.T) {
func TestModulesInfra_ApplyFilter_Good_B64Decode(t *testing.T) {
e := NewExecutor("/tmp")
// b64decode currently returns value unchanged (placeholder)
assert.Equal(t, "dGVzdA==", e.applyFilter("dGVzdA==", "b64decode"))
}
func TestApplyFilter_Infra_Good_UnknownFilter(t *testing.T) {
func TestModulesInfra_ApplyFilter_Good_UnknownFilter(t *testing.T) {
e := NewExecutor("/tmp")
// Unknown filters return value unchanged
assert.Equal(t, "hello", e.applyFilter("hello", "nonexistent_filter"))
}
func TestTemplateString_Infra_Good_FilterInTemplate(t *testing.T) {
func TestModulesInfra_TemplateString_Good_FilterInTemplate(t *testing.T) {
e := NewExecutor("/tmp")
// When a var is defined, the filter passes through
e.vars["defined_var"] = "hello"
@ -519,7 +519,7 @@ func TestTemplateString_Infra_Good_FilterInTemplate(t *testing.T) {
assert.Equal(t, "hello", result)
}
func TestTemplateString_Infra_Good_DefaultFilterEmptyVar(t *testing.T) {
func TestModulesInfra_TemplateString_Good_DefaultFilterEmptyVar(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["empty_var"] = ""
// When var is empty string, default filter applies
@ -527,7 +527,7 @@ func TestTemplateString_Infra_Good_DefaultFilterEmptyVar(t *testing.T) {
assert.Equal(t, "fallback", result)
}
func TestTemplateString_Infra_Good_BoolFilterInTemplate(t *testing.T) {
func TestModulesInfra_TemplateString_Good_BoolFilterInTemplate(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["flag"] = "yes"
@ -535,7 +535,7 @@ func TestTemplateString_Infra_Good_BoolFilterInTemplate(t *testing.T) {
assert.Equal(t, "true", result)
}
func TestTemplateString_Infra_Good_TrimFilterInTemplate(t *testing.T) {
func TestModulesInfra_TemplateString_Good_TrimFilterInTemplate(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["padded"] = " trimmed "
@ -547,7 +547,7 @@ func TestTemplateString_Infra_Good_TrimFilterInTemplate(t *testing.T) {
// 1. Error Propagation — resolveLoop
// ===========================================================================
func TestResolveLoop_Infra_Good_SliceAny(t *testing.T) {
func TestModulesInfra_ResolveLoop_Good_SliceAny(t *testing.T) {
e := NewExecutor("/tmp")
items := e.resolveLoop([]any{"a", "b", "c"}, "host1")
assert.Len(t, items, 3)
@ -556,7 +556,7 @@ func TestResolveLoop_Infra_Good_SliceAny(t *testing.T) {
assert.Equal(t, "c", items[2])
}
func TestResolveLoop_Infra_Good_SliceString(t *testing.T) {
func TestModulesInfra_ResolveLoop_Good_SliceString(t *testing.T) {
e := NewExecutor("/tmp")
items := e.resolveLoop([]string{"x", "y"}, "host1")
assert.Len(t, items, 2)
@ -564,13 +564,13 @@ func TestResolveLoop_Infra_Good_SliceString(t *testing.T) {
assert.Equal(t, "y", items[1])
}
func TestResolveLoop_Infra_Good_NilLoop(t *testing.T) {
func TestModulesInfra_ResolveLoop_Good_NilLoop(t *testing.T) {
e := NewExecutor("/tmp")
items := e.resolveLoop(nil, "host1")
assert.Nil(t, items)
}
func TestResolveLoop_Infra_Good_VarReference(t *testing.T) {
func TestModulesInfra_ResolveLoop_Good_VarReference(t *testing.T) {
e := NewExecutor("/tmp")
e.vars["my_list"] = []any{"item1", "item2", "item3"}
@ -585,7 +585,7 @@ func TestResolveLoop_Infra_Good_VarReference(t *testing.T) {
assert.Nil(t, items)
}
func TestResolveLoop_Infra_Good_MixedTypes(t *testing.T) {
func TestModulesInfra_ResolveLoop_Good_MixedTypes(t *testing.T) {
e := NewExecutor("/tmp")
items := e.resolveLoop([]any{"str", 42, true, map[string]any{"key": "val"}}, "host1")
assert.Len(t, items, 4)
@ -598,7 +598,7 @@ func TestResolveLoop_Infra_Good_MixedTypes(t *testing.T) {
// 1. Error Propagation — handleNotify
// ===========================================================================
func TestHandleNotify_Infra_Good_SingleString(t *testing.T) {
func TestModulesInfra_HandleNotify_Good_SingleString(t *testing.T) {
e := NewExecutor("/tmp")
e.handleNotify("restart nginx")
@ -606,7 +606,7 @@ func TestHandleNotify_Infra_Good_SingleString(t *testing.T) {
assert.False(t, e.notified["restart apache"])
}
func TestHandleNotify_Infra_Good_StringSlice(t *testing.T) {
func TestModulesInfra_HandleNotify_Good_StringSlice(t *testing.T) {
e := NewExecutor("/tmp")
e.handleNotify([]string{"restart nginx", "reload haproxy"})
@ -614,7 +614,7 @@ func TestHandleNotify_Infra_Good_StringSlice(t *testing.T) {
assert.True(t, e.notified["reload haproxy"])
}
func TestHandleNotify_Infra_Good_AnySlice(t *testing.T) {
func TestModulesInfra_HandleNotify_Good_AnySlice(t *testing.T) {
e := NewExecutor("/tmp")
e.handleNotify([]any{"handler1", "handler2", "handler3"})
@ -623,14 +623,14 @@ func TestHandleNotify_Infra_Good_AnySlice(t *testing.T) {
assert.True(t, e.notified["handler3"])
}
func TestHandleNotify_Infra_Good_NilNotify(t *testing.T) {
func TestModulesInfra_HandleNotify_Good_NilNotify(t *testing.T) {
e := NewExecutor("/tmp")
// Should not panic
e.handleNotify(nil)
assert.Empty(t, e.notified)
}
func TestHandleNotify_Infra_Good_MultipleCallsAccumulate(t *testing.T) {
func TestModulesInfra_HandleNotify_Good_MultipleCallsAccumulate(t *testing.T) {
e := NewExecutor("/tmp")
e.handleNotify("handler1")
e.handleNotify("handler2")
@ -643,33 +643,33 @@ func TestHandleNotify_Infra_Good_MultipleCallsAccumulate(t *testing.T) {
// 1. Error Propagation — normalizeConditions
// ===========================================================================
func TestNormalizeConditions_Infra_Good_String(t *testing.T) {
func TestModulesInfra_NormalizeConditions_Good_String(t *testing.T) {
result := normalizeConditions("my_var is defined")
assert.Equal(t, []string{"my_var is defined"}, result)
}
func TestNormalizeConditions_Infra_Good_StringSlice(t *testing.T) {
func TestModulesInfra_NormalizeConditions_Good_StringSlice(t *testing.T) {
result := normalizeConditions([]string{"cond1", "cond2"})
assert.Equal(t, []string{"cond1", "cond2"}, result)
}
func TestNormalizeConditions_Infra_Good_AnySlice(t *testing.T) {
func TestModulesInfra_NormalizeConditions_Good_AnySlice(t *testing.T) {
result := normalizeConditions([]any{"cond1", "cond2"})
assert.Equal(t, []string{"cond1", "cond2"}, result)
}
func TestNormalizeConditions_Infra_Good_Nil(t *testing.T) {
func TestModulesInfra_NormalizeConditions_Good_Nil(t *testing.T) {
result := normalizeConditions(nil)
assert.Nil(t, result)
}
func TestNormalizeConditions_Infra_Good_IntIgnored(t *testing.T) {
func TestModulesInfra_NormalizeConditions_Good_IntIgnored(t *testing.T) {
// Non-string types in any slice are silently skipped
result := normalizeConditions([]any{"cond1", 42})
assert.Equal(t, []string{"cond1"}, result)
}
func TestNormalizeConditions_Infra_Good_UnsupportedType(t *testing.T) {
func TestModulesInfra_NormalizeConditions_Good_UnsupportedType(t *testing.T) {
result := normalizeConditions(42)
assert.Nil(t, result)
}
@ -678,7 +678,7 @@ func TestNormalizeConditions_Infra_Good_UnsupportedType(t *testing.T) {
// 2. Become/Sudo
// ===========================================================================
func TestBecome_Infra_Good_SetBecomeTrue(t *testing.T) {
func TestModulesInfra_Become_Good_SetBecomeTrue(t *testing.T) {
cfg := SSHConfig{
Host: "test-host",
Port: 22,
@ -695,7 +695,7 @@ func TestBecome_Infra_Good_SetBecomeTrue(t *testing.T) {
assert.Equal(t, "secret", client.becomePass)
}
func TestBecome_Infra_Good_SetBecomeFalse(t *testing.T) {
func TestModulesInfra_Become_Good_SetBecomeFalse(t *testing.T) {
cfg := SSHConfig{
Host: "test-host",
Port: 22,
@ -709,7 +709,7 @@ func TestBecome_Infra_Good_SetBecomeFalse(t *testing.T) {
assert.Empty(t, client.becomePass)
}
func TestBecome_Infra_Good_SetBecomeMethod(t *testing.T) {
func TestModulesInfra_Become_Good_SetBecomeMethod(t *testing.T) {
cfg := SSHConfig{Host: "test-host"}
client, err := NewSSHClient(cfg)
require.NoError(t, err)
@ -722,7 +722,7 @@ func TestBecome_Infra_Good_SetBecomeMethod(t *testing.T) {
assert.Equal(t, "pass123", client.becomePass)
}
func TestBecome_Infra_Good_DisableAfterEnable(t *testing.T) {
func TestModulesInfra_Become_Good_DisableAfterEnable(t *testing.T) {
cfg := SSHConfig{Host: "test-host"}
client, err := NewSSHClient(cfg)
require.NoError(t, err)
@ -737,7 +737,7 @@ func TestBecome_Infra_Good_DisableAfterEnable(t *testing.T) {
assert.Equal(t, "secret", client.becomePass)
}
func TestBecome_Infra_Good_MockBecomeTracking(t *testing.T) {
func TestModulesInfra_Become_Good_MockBecomeTracking(t *testing.T) {
mock := NewMockSSHClient()
assert.False(t, mock.become)
@ -747,7 +747,7 @@ func TestBecome_Infra_Good_MockBecomeTracking(t *testing.T) {
assert.Equal(t, "password", mock.becomePass)
}
func TestBecome_Infra_Good_DefaultBecomeUserRoot(t *testing.T) {
func TestModulesInfra_Become_Good_DefaultBecomeUserRoot(t *testing.T) {
// When become is true but no user specified, it defaults to root in the Run method
cfg := SSHConfig{
Host: "test-host",
@ -762,7 +762,7 @@ func TestBecome_Infra_Good_DefaultBecomeUserRoot(t *testing.T) {
// The Run() method defaults to "root" when becomeUser is empty
}
func TestBecome_Infra_Good_PasswordlessBecome(t *testing.T) {
func TestModulesInfra_Become_Good_PasswordlessBecome(t *testing.T) {
cfg := SSHConfig{
Host: "test-host",
Become: true,
@ -777,7 +777,7 @@ func TestBecome_Infra_Good_PasswordlessBecome(t *testing.T) {
assert.Empty(t, client.password)
}
func TestBecome_Infra_Good_ExecutorPlayBecome(t *testing.T) {
func TestModulesInfra_Become_Good_ExecutorPlayBecome(t *testing.T) {
// Test that getClient applies play-level become settings
e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{
@ -806,7 +806,7 @@ func TestBecome_Infra_Good_ExecutorPlayBecome(t *testing.T) {
// 3. Fact Gathering
// ===========================================================================
func TestFacts_Infra_Good_UbuntuParsing(t *testing.T) {
func TestModulesInfra_Facts_Good_UbuntuParsing(t *testing.T) {
e, mock := newTestExecutorWithMock("web1")
// Mock os-release output for Ubuntu
@ -821,10 +821,10 @@ func TestFacts_Infra_Good_UbuntuParsing(t *testing.T) {
facts := &Facts{}
stdout, _, _, _ := mock.Run(nil, "hostname -f 2>/dev/null || hostname")
facts.FQDN = trimSpace(stdout)
facts.FQDN = trimFactSpace(stdout)
stdout, _, _, _ = mock.Run(nil, "hostname -s 2>/dev/null || hostname")
facts.Hostname = trimSpace(stdout)
facts.Hostname = trimFactSpace(stdout)
stdout, _, _, _ = mock.Run(nil, "cat /etc/os-release 2>/dev/null | grep -E '^(ID|VERSION_ID)=' | head -2")
for _, line := range splitLines(stdout) {
@ -837,10 +837,10 @@ func TestFacts_Infra_Good_UbuntuParsing(t *testing.T) {
}
stdout, _, _, _ = mock.Run(nil, "uname -m")
facts.Architecture = trimSpace(stdout)
facts.Architecture = trimFactSpace(stdout)
stdout, _, _, _ = mock.Run(nil, "uname -r")
facts.Kernel = trimSpace(stdout)
facts.Kernel = trimFactSpace(stdout)
e.facts["web1"] = facts
@ -859,7 +859,7 @@ func TestFacts_Infra_Good_UbuntuParsing(t *testing.T) {
assert.Equal(t, "ubuntu", result)
}
func TestFacts_Infra_Good_CentOSParsing(t *testing.T) {
func TestModulesInfra_Facts_Good_CentOSParsing(t *testing.T) {
facts := &Facts{}
osRelease := "ID=centos\nVERSION_ID=\"8\"\n"
@ -876,7 +876,7 @@ func TestFacts_Infra_Good_CentOSParsing(t *testing.T) {
assert.Equal(t, "8", facts.Version)
}
func TestFacts_Infra_Good_AlpineParsing(t *testing.T) {
func TestModulesInfra_Facts_Good_AlpineParsing(t *testing.T) {
facts := &Facts{}
osRelease := "ID=alpine\nVERSION_ID=3.19.1\n"
@ -893,7 +893,7 @@ func TestFacts_Infra_Good_AlpineParsing(t *testing.T) {
assert.Equal(t, "3.19.1", facts.Version)
}
func TestFacts_Infra_Good_DebianParsing(t *testing.T) {
func TestModulesInfra_Facts_Good_DebianParsing(t *testing.T) {
facts := &Facts{}
osRelease := "ID=debian\nVERSION_ID=\"12\"\n"
@ -910,7 +910,7 @@ func TestFacts_Infra_Good_DebianParsing(t *testing.T) {
assert.Equal(t, "12", facts.Version)
}
func TestFacts_Infra_Good_HostnameFromCommand(t *testing.T) {
func TestModulesInfra_Facts_Good_HostnameFromCommand(t *testing.T) {
e := NewExecutor("/tmp")
e.facts["host1"] = &Facts{
Hostname: "myserver",
@ -921,7 +921,7 @@ func TestFacts_Infra_Good_HostnameFromCommand(t *testing.T) {
assert.Equal(t, "myserver.example.com", e.templateString("{{ ansible_fqdn }}", "host1", nil))
}
func TestFacts_Infra_Good_ArchitectureResolution(t *testing.T) {
func TestModulesInfra_Facts_Good_ArchitectureResolution(t *testing.T) {
e := NewExecutor("/tmp")
e.facts["host1"] = &Facts{
Architecture: "aarch64",
@ -931,7 +931,7 @@ func TestFacts_Infra_Good_ArchitectureResolution(t *testing.T) {
assert.Equal(t, "aarch64", result)
}
func TestFacts_Infra_Good_KernelResolution(t *testing.T) {
func TestModulesInfra_Facts_Good_KernelResolution(t *testing.T) {
e := NewExecutor("/tmp")
e.facts["host1"] = &Facts{
Kernel: "5.15.0-91-generic",
@ -941,7 +941,7 @@ func TestFacts_Infra_Good_KernelResolution(t *testing.T) {
assert.Equal(t, "5.15.0-91-generic", result)
}
func TestFacts_Infra_Good_NoFactsForHost(t *testing.T) {
func TestModulesInfra_Facts_Good_NoFactsForHost(t *testing.T) {
e := NewExecutor("/tmp")
// No facts gathered for host1
result := e.templateString("{{ ansible_hostname }}", "host1", nil)
@ -949,7 +949,7 @@ func TestFacts_Infra_Good_NoFactsForHost(t *testing.T) {
assert.Equal(t, "{{ ansible_hostname }}", result)
}
func TestFacts_Infra_Good_LocalhostFacts(t *testing.T) {
func TestModulesInfra_Facts_Good_LocalhostFacts(t *testing.T) {
// When connection is local, gatherFacts sets minimal facts
e := NewExecutor("/tmp")
e.facts["localhost"] = &Facts{
@ -964,7 +964,7 @@ func TestFacts_Infra_Good_LocalhostFacts(t *testing.T) {
// 4. Idempotency
// ===========================================================================
func TestIdempotency_Infra_Good_GroupAlreadyExists(t *testing.T) {
func TestModulesInfra_Idempotency_Good_GroupAlreadyExists(t *testing.T) {
_, mock := newTestExecutorWithMock("host1")
// Mock: getent group docker succeeds (group exists) — the || means groupadd is skipped
@ -989,7 +989,7 @@ func TestIdempotency_Infra_Good_GroupAlreadyExists(t *testing.T) {
assert.False(t, result.Failed)
}
func TestIdempotency_Infra_Good_AuthorizedKeyAlreadyPresent(t *testing.T) {
func TestModulesInfra_Idempotency_Good_AuthorizedKeyAlreadyPresent(t *testing.T) {
_, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7xfG..." +
@ -1020,7 +1020,7 @@ func TestIdempotency_Infra_Good_AuthorizedKeyAlreadyPresent(t *testing.T) {
assert.False(t, result.Failed)
}
func TestIdempotency_Infra_Good_DockerComposeUpToDate(t *testing.T) {
func TestModulesInfra_Idempotency_Good_DockerComposeUpToDate(t *testing.T) {
_, mock := newTestExecutorWithMock("host1")
// Mock: docker compose up -d returns "Up to date" in stdout
@ -1038,7 +1038,7 @@ func TestIdempotency_Infra_Good_DockerComposeUpToDate(t *testing.T) {
assert.False(t, result.Failed)
}
func TestIdempotency_Infra_Good_DockerComposeChanged(t *testing.T) {
func TestModulesInfra_Idempotency_Good_DockerComposeChanged(t *testing.T) {
_, mock := newTestExecutorWithMock("host1")
// Mock: docker compose up -d with actual changes
@ -1056,7 +1056,7 @@ func TestIdempotency_Infra_Good_DockerComposeChanged(t *testing.T) {
assert.False(t, result.Failed)
}
func TestIdempotency_Infra_Good_DockerComposeUpToDateInStderr(t *testing.T) {
func TestModulesInfra_Idempotency_Good_DockerComposeUpToDateInStderr(t *testing.T) {
_, mock := newTestExecutorWithMock("host1")
// Some versions of docker compose output status to stderr
@ -1073,7 +1073,7 @@ func TestIdempotency_Infra_Good_DockerComposeUpToDateInStderr(t *testing.T) {
assert.False(t, result.Changed)
}
func TestIdempotency_Infra_Good_GroupCreationWhenNew(t *testing.T) {
func TestModulesInfra_Idempotency_Good_GroupCreationWhenNew(t *testing.T) {
_, mock := newTestExecutorWithMock("host1")
// Mock: getent fails (group does not exist), groupadd succeeds
@ -1092,7 +1092,7 @@ func TestIdempotency_Infra_Good_GroupCreationWhenNew(t *testing.T) {
assert.False(t, result.Failed)
}
func TestIdempotency_Infra_Good_ServiceStatChanged(t *testing.T) {
func TestModulesInfra_Idempotency_Good_ServiceStatChanged(t *testing.T) {
_, mock := newTestExecutorWithMock("host1")
// Mock: stat reports the file exists
@ -1110,7 +1110,7 @@ func TestIdempotency_Infra_Good_ServiceStatChanged(t *testing.T) {
assert.True(t, stat["exists"].(bool))
}
func TestIdempotency_Infra_Good_StatFileNotFound(t *testing.T) {
func TestModulesInfra_Idempotency_Good_StatFileNotFound(t *testing.T) {
_, mock := newTestExecutorWithMock("host1")
// No stat info added — will return exists=false from mock
@ -1129,7 +1129,7 @@ func TestIdempotency_Infra_Good_StatFileNotFound(t *testing.T) {
// Additional cross-cutting edge cases
// ===========================================================================
func TestResolveExpr_Infra_Good_HostVars(t *testing.T) {
func TestModulesInfra_ResolveExpr_Good_HostVars(t *testing.T) {
e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{
@ -1148,7 +1148,7 @@ func TestResolveExpr_Infra_Good_HostVars(t *testing.T) {
assert.Equal(t, "custom_value", result)
}
func TestTemplateArgs_Infra_Good_InventoryHostname(t *testing.T) {
func TestModulesInfra_TemplateArgs_Good_InventoryHostname(t *testing.T) {
e := NewExecutor("/tmp")
args := map[string]any{
@ -1159,13 +1159,13 @@ func TestTemplateArgs_Infra_Good_InventoryHostname(t *testing.T) {
assert.Equal(t, "web1", result["hostname"])
}
func TestEvalCondition_Infra_Good_UnknownDefaultsTrue(t *testing.T) {
func TestModulesInfra_EvalCondition_Good_UnknownDefaultsTrue(t *testing.T) {
e := NewExecutor("/tmp")
// Unknown conditions default to true (permissive)
assert.True(t, e.evalCondition("some_complex_expression == 'value'", "host1"))
}
func TestGetRegisteredVar_Infra_Good_DottedAccess(t *testing.T) {
func TestModulesInfra_GetRegisteredVar_Good_DottedAccess(t *testing.T) {
e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{
"my_cmd": {Stdout: "output_text", RC: 0},
@ -1178,14 +1178,14 @@ func TestGetRegisteredVar_Infra_Good_DottedAccess(t *testing.T) {
assert.Equal(t, "output_text", result.Stdout)
}
func TestGetRegisteredVar_Infra_Bad_NotRegistered(t *testing.T) {
func TestModulesInfra_GetRegisteredVar_Bad_NotRegistered(t *testing.T) {
e := NewExecutor("/tmp")
result := e.getRegisteredVar("host1", "nonexistent")
assert.Nil(t, result)
}
func TestGetRegisteredVar_Infra_Bad_WrongHost(t *testing.T) {
func TestModulesInfra_GetRegisteredVar_Bad_WrongHost(t *testing.T) {
e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{
"my_cmd": {Stdout: "output"},
@ -1200,7 +1200,7 @@ func TestGetRegisteredVar_Infra_Bad_WrongHost(t *testing.T) {
// String helper utilities used by fact tests
// ===========================================================================
func trimSpace(s string) string {
func trimFactSpace(s string) string {
result := ""
for _, c := range s {
if c != '\n' && c != '\r' && c != ' ' && c != '\t' {

View file

@ -13,7 +13,7 @@ import (
// --- service module ---
func TestModuleService_Good_Start(t *testing.T) {
func TestModulesSvc_ModuleService_Good_Start(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl start nginx`, "Started", "", 0)
@ -29,7 +29,7 @@ func TestModuleService_Good_Start(t *testing.T) {
assert.Equal(t, 1, mock.commandCount())
}
func TestModuleService_Good_Stop(t *testing.T) {
func TestModulesSvc_ModuleService_Good_Stop(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl stop nginx`, "", "", 0)
@ -44,7 +44,7 @@ func TestModuleService_Good_Stop(t *testing.T) {
assert.True(t, mock.hasExecuted(`systemctl stop nginx`))
}
func TestModuleService_Good_Restart(t *testing.T) {
func TestModulesSvc_ModuleService_Good_Restart(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl restart docker`, "", "", 0)
@ -59,7 +59,7 @@ func TestModuleService_Good_Restart(t *testing.T) {
assert.True(t, mock.hasExecuted(`systemctl restart docker`))
}
func TestModuleService_Good_Reload(t *testing.T) {
func TestModulesSvc_ModuleService_Good_Reload(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl reload nginx`, "", "", 0)
@ -74,7 +74,7 @@ func TestModuleService_Good_Reload(t *testing.T) {
assert.True(t, mock.hasExecuted(`systemctl reload nginx`))
}
func TestModuleService_Good_Enable(t *testing.T) {
func TestModulesSvc_ModuleService_Good_Enable(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl enable nginx`, "", "", 0)
@ -89,7 +89,7 @@ func TestModuleService_Good_Enable(t *testing.T) {
assert.True(t, mock.hasExecuted(`systemctl enable nginx`))
}
func TestModuleService_Good_Disable(t *testing.T) {
func TestModulesSvc_ModuleService_Good_Disable(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl disable nginx`, "", "", 0)
@ -104,7 +104,7 @@ func TestModuleService_Good_Disable(t *testing.T) {
assert.True(t, mock.hasExecuted(`systemctl disable nginx`))
}
func TestModuleService_Good_StartAndEnable(t *testing.T) {
func TestModulesSvc_ModuleService_Good_StartAndEnable(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl start nginx`, "", "", 0)
mock.expectCommand(`systemctl enable nginx`, "", "", 0)
@ -123,7 +123,7 @@ func TestModuleService_Good_StartAndEnable(t *testing.T) {
assert.True(t, mock.hasExecuted(`systemctl enable nginx`))
}
func TestModuleService_Good_RestartAndDisable(t *testing.T) {
func TestModulesSvc_ModuleService_Good_RestartAndDisable(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl restart sshd`, "", "", 0)
mock.expectCommand(`systemctl disable sshd`, "", "", 0)
@ -142,7 +142,7 @@ func TestModuleService_Good_RestartAndDisable(t *testing.T) {
assert.True(t, mock.hasExecuted(`systemctl disable sshd`))
}
func TestModuleService_Bad_MissingName(t *testing.T) {
func TestModulesSvc_ModuleService_Bad_MissingName(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -154,7 +154,7 @@ func TestModuleService_Bad_MissingName(t *testing.T) {
assert.Contains(t, err.Error(), "name required")
}
func TestModuleService_Good_NoStateNoEnabled(t *testing.T) {
func TestModulesSvc_ModuleService_Good_NoStateNoEnabled(t *testing.T) {
// When neither state nor enabled is provided, no commands run
e, mock := newTestExecutorWithMock("host1")
@ -168,7 +168,7 @@ func TestModuleService_Good_NoStateNoEnabled(t *testing.T) {
assert.Equal(t, 0, mock.commandCount())
}
func TestModuleService_Good_CommandFailure(t *testing.T) {
func TestModulesSvc_ModuleService_Good_CommandFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl start.*`, "", "Failed to start nginx.service", 1)
@ -183,7 +183,7 @@ func TestModuleService_Good_CommandFailure(t *testing.T) {
assert.Equal(t, 1, result.RC)
}
func TestModuleService_Good_FirstCommandFailsSkipsRest(t *testing.T) {
func TestModulesSvc_ModuleService_Good_FirstCommandFailsSkipsRest(t *testing.T) {
// When state command fails, enable should not run
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl start`, "", "unit not found", 5)
@ -203,7 +203,7 @@ func TestModuleService_Good_FirstCommandFailsSkipsRest(t *testing.T) {
// --- systemd module ---
func TestModuleSystemd_Good_DaemonReloadThenStart(t *testing.T) {
func TestModulesSvc_ModuleSystemd_Good_DaemonReloadThenStart(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl daemon-reload`, "", "", 0)
mock.expectCommand(`systemctl start nginx`, "", "", 0)
@ -225,7 +225,7 @@ func TestModuleSystemd_Good_DaemonReloadThenStart(t *testing.T) {
assert.Contains(t, cmds[1].Cmd, "systemctl start nginx")
}
func TestModuleSystemd_Good_DaemonReloadOnly(t *testing.T) {
func TestModulesSvc_ModuleSystemd_Good_DaemonReloadOnly(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl daemon-reload`, "", "", 0)
@ -243,7 +243,7 @@ func TestModuleSystemd_Good_DaemonReloadOnly(t *testing.T) {
assert.True(t, mock.hasExecuted(`systemctl daemon-reload`))
}
func TestModuleSystemd_Good_DelegationToService(t *testing.T) {
func TestModulesSvc_ModuleSystemd_Good_DelegationToService(t *testing.T) {
// Without daemon_reload, systemd delegates entirely to service
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl restart docker`, "", "", 0)
@ -261,7 +261,7 @@ func TestModuleSystemd_Good_DelegationToService(t *testing.T) {
assert.False(t, mock.hasExecuted(`daemon-reload`))
}
func TestModuleSystemd_Good_DaemonReloadWithEnable(t *testing.T) {
func TestModulesSvc_ModuleSystemd_Good_DaemonReloadWithEnable(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl daemon-reload`, "", "", 0)
mock.expectCommand(`systemctl enable myapp`, "", "", 0)
@ -281,7 +281,7 @@ func TestModuleSystemd_Good_DaemonReloadWithEnable(t *testing.T) {
// --- apt module ---
func TestModuleApt_Good_InstallPresent(t *testing.T) {
func TestModulesSvc_ModuleApt_Good_InstallPresent(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get install -y -qq nginx`, "installed", "", 0)
@ -296,7 +296,7 @@ func TestModuleApt_Good_InstallPresent(t *testing.T) {
assert.True(t, mock.hasExecuted(`DEBIAN_FRONTEND=noninteractive apt-get install -y -qq nginx`))
}
func TestModuleApt_Good_InstallInstalled(t *testing.T) {
func TestModulesSvc_ModuleApt_Good_InstallInstalled(t *testing.T) {
// state=installed is an alias for present
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get install -y -qq curl`, "", "", 0)
@ -312,7 +312,7 @@ func TestModuleApt_Good_InstallInstalled(t *testing.T) {
assert.True(t, mock.hasExecuted(`apt-get install -y -qq curl`))
}
func TestModuleApt_Good_RemoveAbsent(t *testing.T) {
func TestModulesSvc_ModuleApt_Good_RemoveAbsent(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get remove -y -qq nginx`, "", "", 0)
@ -327,7 +327,7 @@ func TestModuleApt_Good_RemoveAbsent(t *testing.T) {
assert.True(t, mock.hasExecuted(`DEBIAN_FRONTEND=noninteractive apt-get remove -y -qq nginx`))
}
func TestModuleApt_Good_RemoveRemoved(t *testing.T) {
func TestModulesSvc_ModuleApt_Good_RemoveRemoved(t *testing.T) {
// state=removed is an alias for absent
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get remove -y -qq nginx`, "", "", 0)
@ -342,7 +342,7 @@ func TestModuleApt_Good_RemoveRemoved(t *testing.T) {
assert.True(t, mock.hasExecuted(`apt-get remove -y -qq nginx`))
}
func TestModuleApt_Good_UpgradeLatest(t *testing.T) {
func TestModulesSvc_ModuleApt_Good_UpgradeLatest(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get install -y -qq --only-upgrade nginx`, "", "", 0)
@ -357,7 +357,7 @@ func TestModuleApt_Good_UpgradeLatest(t *testing.T) {
assert.True(t, mock.hasExecuted(`DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --only-upgrade nginx`))
}
func TestModuleApt_Good_UpdateCacheBeforeInstall(t *testing.T) {
func TestModulesSvc_ModuleApt_Good_UpdateCacheBeforeInstall(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get update`, "", "", 0)
mock.expectCommand(`apt-get install -y -qq nginx`, "", "", 0)
@ -379,7 +379,7 @@ func TestModuleApt_Good_UpdateCacheBeforeInstall(t *testing.T) {
assert.Contains(t, cmds[1].Cmd, "apt-get install")
}
func TestModuleApt_Good_UpdateCacheOnly(t *testing.T) {
func TestModulesSvc_ModuleApt_Good_UpdateCacheOnly(t *testing.T) {
// update_cache with no name means update only, no install
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get update`, "", "", 0)
@ -395,7 +395,7 @@ func TestModuleApt_Good_UpdateCacheOnly(t *testing.T) {
assert.True(t, mock.hasExecuted(`apt-get update`))
}
func TestModuleApt_Good_CommandFailure(t *testing.T) {
func TestModulesSvc_ModuleApt_Good_CommandFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get install`, "", "E: Unable to locate package badpkg", 100)
@ -410,7 +410,7 @@ func TestModuleApt_Good_CommandFailure(t *testing.T) {
assert.Equal(t, 100, result.RC)
}
func TestModuleApt_Good_DefaultStateIsPresent(t *testing.T) {
func TestModulesSvc_ModuleApt_Good_DefaultStateIsPresent(t *testing.T) {
// If no state is given, default is "present" (install)
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get install -y -qq vim`, "", "", 0)
@ -426,7 +426,7 @@ func TestModuleApt_Good_DefaultStateIsPresent(t *testing.T) {
// --- apt_key module ---
func TestModuleAptKey_Good_AddWithKeyring(t *testing.T) {
func TestModulesSvc_ModuleAptKey_Good_AddWithKeyring(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl -fsSL.*gpg --dearmor`, "", "", 0)
@ -443,7 +443,7 @@ func TestModuleAptKey_Good_AddWithKeyring(t *testing.T) {
assert.True(t, mock.containsSubstring("/etc/apt/keyrings/example.gpg"))
}
func TestModuleAptKey_Good_AddWithoutKeyring(t *testing.T) {
func TestModulesSvc_ModuleAptKey_Good_AddWithoutKeyring(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl -fsSL.*apt-key add -`, "", "", 0)
@ -457,7 +457,7 @@ func TestModuleAptKey_Good_AddWithoutKeyring(t *testing.T) {
assert.True(t, mock.hasExecuted(`apt-key add -`))
}
func TestModuleAptKey_Good_RemoveKey(t *testing.T) {
func TestModulesSvc_ModuleAptKey_Good_RemoveKey(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleAptKeyWithClient(e, mock, map[string]any{
@ -472,7 +472,7 @@ func TestModuleAptKey_Good_RemoveKey(t *testing.T) {
assert.True(t, mock.containsSubstring("/etc/apt/keyrings/old.gpg"))
}
func TestModuleAptKey_Good_RemoveWithoutKeyring(t *testing.T) {
func TestModulesSvc_ModuleAptKey_Good_RemoveWithoutKeyring(t *testing.T) {
// Absent with no keyring — still succeeds, just no rm command
e, mock := newTestExecutorWithMock("host1")
@ -485,7 +485,7 @@ func TestModuleAptKey_Good_RemoveWithoutKeyring(t *testing.T) {
assert.Equal(t, 0, mock.commandCount())
}
func TestModuleAptKey_Bad_MissingURL(t *testing.T) {
func TestModulesSvc_ModuleAptKey_Bad_MissingURL(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -497,7 +497,7 @@ func TestModuleAptKey_Bad_MissingURL(t *testing.T) {
assert.Contains(t, err.Error(), "url required")
}
func TestModuleAptKey_Good_CommandFailure(t *testing.T) {
func TestModulesSvc_ModuleAptKey_Good_CommandFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl`, "", "curl: (22) 404 Not Found", 22)
@ -513,7 +513,7 @@ func TestModuleAptKey_Good_CommandFailure(t *testing.T) {
// --- apt_repository module ---
func TestModuleAptRepository_Good_AddRepository(t *testing.T) {
func TestModulesSvc_ModuleAptRepository_Good_AddRepository(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`echo.*sources\.list\.d`, "", "", 0)
mock.expectCommand(`apt-get update`, "", "", 0)
@ -529,7 +529,7 @@ func TestModuleAptRepository_Good_AddRepository(t *testing.T) {
assert.True(t, mock.containsSubstring("/etc/apt/sources.list.d/example.list"))
}
func TestModuleAptRepository_Good_RemoveRepository(t *testing.T) {
func TestModulesSvc_ModuleAptRepository_Good_RemoveRepository(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
result, err := moduleAptRepositoryWithClient(e, mock, map[string]any{
@ -545,7 +545,7 @@ func TestModuleAptRepository_Good_RemoveRepository(t *testing.T) {
assert.True(t, mock.containsSubstring("example.list"))
}
func TestModuleAptRepository_Good_AddWithUpdateCache(t *testing.T) {
func TestModulesSvc_ModuleAptRepository_Good_AddWithUpdateCache(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`echo`, "", "", 0)
mock.expectCommand(`apt-get update`, "", "", 0)
@ -564,7 +564,7 @@ func TestModuleAptRepository_Good_AddWithUpdateCache(t *testing.T) {
assert.True(t, mock.hasExecuted(`apt-get update`))
}
func TestModuleAptRepository_Good_AddWithoutUpdateCache(t *testing.T) {
func TestModulesSvc_ModuleAptRepository_Good_AddWithoutUpdateCache(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`echo`, "", "", 0)
@ -582,7 +582,7 @@ func TestModuleAptRepository_Good_AddWithoutUpdateCache(t *testing.T) {
assert.False(t, mock.hasExecuted(`apt-get update`))
}
func TestModuleAptRepository_Good_CustomFilename(t *testing.T) {
func TestModulesSvc_ModuleAptRepository_Good_CustomFilename(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`echo`, "", "", 0)
mock.expectCommand(`apt-get update`, "", "", 0)
@ -597,7 +597,7 @@ func TestModuleAptRepository_Good_CustomFilename(t *testing.T) {
assert.True(t, mock.containsSubstring("/etc/apt/sources.list.d/custom-ppa.list"))
}
func TestModuleAptRepository_Good_AutoGeneratedFilename(t *testing.T) {
func TestModulesSvc_ModuleAptRepository_Good_AutoGeneratedFilename(t *testing.T) {
// When no filename is given, it auto-generates from the repo string
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`echo`, "", "", 0)
@ -613,7 +613,7 @@ func TestModuleAptRepository_Good_AutoGeneratedFilename(t *testing.T) {
assert.True(t, mock.containsSubstring("/etc/apt/sources.list.d/"))
}
func TestModuleAptRepository_Bad_MissingRepo(t *testing.T) {
func TestModulesSvc_ModuleAptRepository_Bad_MissingRepo(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
@ -625,7 +625,7 @@ func TestModuleAptRepository_Bad_MissingRepo(t *testing.T) {
assert.Contains(t, err.Error(), "repo required")
}
func TestModuleAptRepository_Good_WriteFailure(t *testing.T) {
func TestModulesSvc_ModuleAptRepository_Good_WriteFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`echo`, "", "permission denied", 1)
@ -641,7 +641,7 @@ func TestModuleAptRepository_Good_WriteFailure(t *testing.T) {
// --- package module ---
func TestModulePackage_Good_DetectAptAndDelegate(t *testing.T) {
func TestModulesSvc_ModulePackage_Good_DetectAptAndDelegate(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
// First command: which apt-get returns the path
mock.expectCommand(`which apt-get`, "/usr/bin/apt-get", "", 0)
@ -660,7 +660,7 @@ func TestModulePackage_Good_DetectAptAndDelegate(t *testing.T) {
assert.True(t, mock.hasExecuted(`apt-get install -y -qq htop`))
}
func TestModulePackage_Good_FallbackToApt(t *testing.T) {
func TestModulesSvc_ModulePackage_Good_FallbackToApt(t *testing.T) {
// When which returns nothing (no package manager found), still falls back to apt
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`which apt-get`, "", "", 1)
@ -677,7 +677,7 @@ func TestModulePackage_Good_FallbackToApt(t *testing.T) {
assert.True(t, mock.hasExecuted(`apt-get install -y -qq vim`))
}
func TestModulePackage_Good_RemovePackage(t *testing.T) {
func TestModulesSvc_ModulePackage_Good_RemovePackage(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`which apt-get`, "/usr/bin/apt-get", "", 0)
mock.expectCommand(`apt-get remove -y -qq nano`, "", "", 0)
@ -694,7 +694,7 @@ func TestModulePackage_Good_RemovePackage(t *testing.T) {
// --- pip module ---
func TestModulePip_Good_InstallPresent(t *testing.T) {
func TestModulesSvc_ModulePip_Good_InstallPresent(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`pip3 install flask`, "Successfully installed", "", 0)
@ -709,7 +709,7 @@ func TestModulePip_Good_InstallPresent(t *testing.T) {
assert.True(t, mock.hasExecuted(`pip3 install flask`))
}
func TestModulePip_Good_UninstallAbsent(t *testing.T) {
func TestModulesSvc_ModulePip_Good_UninstallAbsent(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`pip3 uninstall -y flask`, "Successfully uninstalled", "", 0)
@ -724,7 +724,7 @@ func TestModulePip_Good_UninstallAbsent(t *testing.T) {
assert.True(t, mock.hasExecuted(`pip3 uninstall -y flask`))
}
func TestModulePip_Good_UpgradeLatest(t *testing.T) {
func TestModulesSvc_ModulePip_Good_UpgradeLatest(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`pip3 install --upgrade flask`, "Successfully installed", "", 0)
@ -739,7 +739,7 @@ func TestModulePip_Good_UpgradeLatest(t *testing.T) {
assert.True(t, mock.hasExecuted(`pip3 install --upgrade flask`))
}
func TestModulePip_Good_CustomExecutable(t *testing.T) {
func TestModulesSvc_ModulePip_Good_CustomExecutable(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`/opt/venv/bin/pip install requests`, "", "", 0)
@ -755,7 +755,7 @@ func TestModulePip_Good_CustomExecutable(t *testing.T) {
assert.True(t, mock.hasExecuted(`/opt/venv/bin/pip install requests`))
}
func TestModulePip_Good_DefaultStateIsPresent(t *testing.T) {
func TestModulesSvc_ModulePip_Good_DefaultStateIsPresent(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`pip3 install django`, "", "", 0)
@ -768,7 +768,7 @@ func TestModulePip_Good_DefaultStateIsPresent(t *testing.T) {
assert.True(t, mock.hasExecuted(`pip3 install django`))
}
func TestModulePip_Good_CommandFailure(t *testing.T) {
func TestModulesSvc_ModulePip_Good_CommandFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`pip3 install`, "", "ERROR: No matching distribution found", 1)
@ -782,7 +782,7 @@ func TestModulePip_Good_CommandFailure(t *testing.T) {
assert.Contains(t, result.Msg, "No matching distribution found")
}
func TestModulePip_Good_InstalledAlias(t *testing.T) {
func TestModulesSvc_ModulePip_Good_InstalledAlias(t *testing.T) {
// state=installed is an alias for present
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`pip3 install boto3`, "", "", 0)
@ -797,7 +797,7 @@ func TestModulePip_Good_InstalledAlias(t *testing.T) {
assert.True(t, mock.hasExecuted(`pip3 install boto3`))
}
func TestModulePip_Good_RemovedAlias(t *testing.T) {
func TestModulesSvc_ModulePip_Good_RemovedAlias(t *testing.T) {
// state=removed is an alias for absent
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`pip3 uninstall -y boto3`, "", "", 0)
@ -814,7 +814,7 @@ func TestModulePip_Good_RemovedAlias(t *testing.T) {
// --- Cross-module dispatch tests ---
func TestExecuteModuleWithMock_Good_DispatchService(t *testing.T) {
func TestModulesSvc_ExecuteModuleWithMock_Good_DispatchService(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl restart nginx`, "", "", 0)
@ -833,7 +833,7 @@ func TestExecuteModuleWithMock_Good_DispatchService(t *testing.T) {
assert.True(t, mock.hasExecuted(`systemctl restart nginx`))
}
func TestExecuteModuleWithMock_Good_DispatchSystemd(t *testing.T) {
func TestModulesSvc_ExecuteModuleWithMock_Good_DispatchSystemd(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl daemon-reload`, "", "", 0)
mock.expectCommand(`systemctl start myapp`, "", "", 0)
@ -855,7 +855,7 @@ func TestExecuteModuleWithMock_Good_DispatchSystemd(t *testing.T) {
assert.True(t, mock.hasExecuted(`systemctl start myapp`))
}
func TestExecuteModuleWithMock_Good_DispatchApt(t *testing.T) {
func TestModulesSvc_ExecuteModuleWithMock_Good_DispatchApt(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get install -y -qq nginx`, "", "", 0)
@ -874,7 +874,7 @@ func TestExecuteModuleWithMock_Good_DispatchApt(t *testing.T) {
assert.True(t, mock.hasExecuted(`apt-get install`))
}
func TestExecuteModuleWithMock_Good_DispatchAptKey(t *testing.T) {
func TestModulesSvc_ExecuteModuleWithMock_Good_DispatchAptKey(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl.*gpg`, "", "", 0)
@ -892,7 +892,7 @@ func TestExecuteModuleWithMock_Good_DispatchAptKey(t *testing.T) {
assert.True(t, result.Changed)
}
func TestExecuteModuleWithMock_Good_DispatchAptRepository(t *testing.T) {
func TestModulesSvc_ExecuteModuleWithMock_Good_DispatchAptRepository(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`echo`, "", "", 0)
mock.expectCommand(`apt-get update`, "", "", 0)
@ -911,7 +911,7 @@ func TestExecuteModuleWithMock_Good_DispatchAptRepository(t *testing.T) {
assert.True(t, result.Changed)
}
func TestExecuteModuleWithMock_Good_DispatchPackage(t *testing.T) {
func TestModulesSvc_ExecuteModuleWithMock_Good_DispatchPackage(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`which apt-get`, "/usr/bin/apt-get", "", 0)
mock.expectCommand(`apt-get install -y -qq git`, "", "", 0)
@ -930,7 +930,7 @@ func TestExecuteModuleWithMock_Good_DispatchPackage(t *testing.T) {
assert.True(t, result.Changed)
}
func TestExecuteModuleWithMock_Good_DispatchPip(t *testing.T) {
func TestModulesSvc_ExecuteModuleWithMock_Good_DispatchPip(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`pip3 install ansible`, "", "", 0)

View file

@ -11,12 +11,20 @@ import (
)
// Parser handles Ansible YAML parsing.
//
// Example:
//
// parser := NewParser("/workspace/playbooks")
type Parser struct {
basePath string
vars map[string]any
}
// NewParser creates a new Ansible parser.
//
// Example:
//
// parser := NewParser("/workspace/playbooks")
func NewParser(basePath string) *Parser {
return &Parser{
basePath: basePath,
@ -25,6 +33,10 @@ func NewParser(basePath string) *Parser {
}
// ParsePlaybook parses an Ansible playbook file.
//
// Example:
//
// plays, err := parser.ParsePlaybook("/workspace/playbooks/site.yml")
func (p *Parser) ParsePlaybook(path string) ([]Play, error) {
data, err := coreio.Local.Read(path)
if err != nil {
@ -47,6 +59,10 @@ func (p *Parser) ParsePlaybook(path string) ([]Play, error) {
}
// ParsePlaybookIter returns an iterator for plays in an Ansible playbook file.
//
// Example:
//
// seq, err := parser.ParsePlaybookIter("/workspace/playbooks/site.yml")
func (p *Parser) ParsePlaybookIter(path string) (iter.Seq[Play], error) {
plays, err := p.ParsePlaybook(path)
if err != nil {
@ -62,6 +78,10 @@ func (p *Parser) ParsePlaybookIter(path string) (iter.Seq[Play], error) {
}
// ParseInventory parses an Ansible inventory file.
//
// Example:
//
// inv, err := parser.ParseInventory("/workspace/inventory.yml")
func (p *Parser) ParseInventory(path string) (*Inventory, error) {
data, err := coreio.Local.Read(path)
if err != nil {
@ -77,6 +97,10 @@ func (p *Parser) ParseInventory(path string) (*Inventory, error) {
}
// ParseTasks parses a tasks file (used by include_tasks).
//
// Example:
//
// tasks, err := parser.ParseTasks("/workspace/roles/web/tasks/main.yml")
func (p *Parser) ParseTasks(path string) ([]Task, error) {
data, err := coreio.Local.Read(path)
if err != nil {
@ -98,6 +122,10 @@ func (p *Parser) ParseTasks(path string) ([]Task, error) {
}
// ParseTasksIter returns an iterator for tasks in a tasks file.
//
// Example:
//
// seq, err := parser.ParseTasksIter("/workspace/roles/web/tasks/main.yml")
func (p *Parser) ParseTasksIter(path string) (iter.Seq[Task], error) {
tasks, err := p.ParseTasks(path)
if err != nil {
@ -113,6 +141,10 @@ func (p *Parser) ParseTasksIter(path string) (iter.Seq[Task], error) {
}
// ParseRole parses a role and returns its tasks.
//
// Example:
//
// tasks, err := parser.ParseRole("nginx", "main.yml")
func (p *Parser) ParseRole(name string, tasksFrom string) ([]Task, error) {
if tasksFrom == "" {
tasksFrom = "main.yml"
@ -233,6 +265,11 @@ func (p *Parser) extractModule(task *Task) error {
}
// UnmarshalYAML implements custom YAML unmarshaling for Task.
//
// Example:
//
// var task Task
// _ = yaml.Unmarshal([]byte("shell: echo ok"), &task)
func (t *Task) UnmarshalYAML(node *yaml.Node) error {
// First decode known fields
type rawTask Task
@ -316,6 +353,10 @@ func isModule(key string) bool {
}
// NormalizeModule normalizes a module name to its canonical form.
//
// Example:
//
// module := NormalizeModule("shell")
func NormalizeModule(name string) string {
// Add ansible.builtin. prefix if missing
if !contains(name, ".") {
@ -325,6 +366,10 @@ func NormalizeModule(name string) string {
}
// GetHosts returns hosts matching a pattern from inventory.
//
// Example:
//
// hosts := GetHosts(inv, "webservers")
func GetHosts(inv *Inventory, pattern string) []string {
if pattern == "all" {
return getAllHosts(inv.All)
@ -350,6 +395,10 @@ func GetHosts(inv *Inventory, pattern string) []string {
}
// GetHostsIter returns an iterator for hosts matching a pattern from inventory.
//
// Example:
//
// seq := GetHostsIter(inv, "all")
func GetHostsIter(inv *Inventory, pattern string) iter.Seq[string] {
hosts := GetHosts(inv, pattern)
return func(yield func(string) bool) {
@ -377,6 +426,10 @@ func getAllHosts(group *InventoryGroup) []string {
}
// AllHostsIter returns an iterator for all hosts in an inventory group.
//
// Example:
//
// seq := AllHostsIter(inv.All)
func AllHostsIter(group *InventoryGroup) iter.Seq[string] {
return func(yield func(string) bool) {
if group == nil {
@ -442,6 +495,10 @@ func hasHost(group *InventoryGroup, name string) bool {
}
// GetHostVars returns variables for a specific host.
//
// Example:
//
// vars := GetHostVars(inv, "web1")
func GetHostVars(inv *Inventory, hostname string) map[string]any {
vars := make(map[string]any)

View file

@ -1,8 +1,6 @@
package ansible
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
@ -11,9 +9,9 @@ import (
// --- ParsePlaybook ---
func TestParsePlaybook_Good_SimplePlay(t *testing.T) {
func TestParser_ParsePlaybook_Good_SimplePlay(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "playbook.yml")
path := joinPath(dir, "playbook.yml")
yaml := `---
- name: Configure webserver
@ -25,7 +23,7 @@ func TestParsePlaybook_Good_SimplePlay(t *testing.T) {
name: nginx
state: present
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
plays, err := p.ParsePlaybook(path)
@ -42,9 +40,9 @@ func TestParsePlaybook_Good_SimplePlay(t *testing.T) {
assert.Equal(t, "present", plays[0].Tasks[0].Args["state"])
}
func TestParsePlaybook_Good_MultiplePlays(t *testing.T) {
func TestParser_ParsePlaybook_Good_MultiplePlays(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "playbook.yml")
path := joinPath(dir, "playbook.yml")
yaml := `---
- name: Play one
@ -62,7 +60,7 @@ func TestParsePlaybook_Good_MultiplePlays(t *testing.T) {
debug:
msg: "Goodbye"
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
plays, err := p.ParsePlaybook(path)
@ -76,9 +74,9 @@ func TestParsePlaybook_Good_MultiplePlays(t *testing.T) {
assert.Equal(t, "local", plays[1].Connection)
}
func TestParsePlaybook_Good_WithVars(t *testing.T) {
func TestParser_ParsePlaybook_Good_WithVars(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "playbook.yml")
path := joinPath(dir, "playbook.yml")
yaml := `---
- name: With vars
@ -91,7 +89,7 @@ func TestParsePlaybook_Good_WithVars(t *testing.T) {
debug:
msg: "Port is {{ http_port }}"
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
plays, err := p.ParsePlaybook(path)
@ -102,9 +100,9 @@ func TestParsePlaybook_Good_WithVars(t *testing.T) {
assert.Equal(t, "myapp", plays[0].Vars["app_name"])
}
func TestParsePlaybook_Good_PrePostTasks(t *testing.T) {
func TestParser_ParsePlaybook_Good_PrePostTasks(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "playbook.yml")
path := joinPath(dir, "playbook.yml")
yaml := `---
- name: Full lifecycle
@ -122,7 +120,7 @@ func TestParsePlaybook_Good_PrePostTasks(t *testing.T) {
debug:
msg: "post"
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
plays, err := p.ParsePlaybook(path)
@ -137,9 +135,9 @@ func TestParsePlaybook_Good_PrePostTasks(t *testing.T) {
assert.Equal(t, "Post task", plays[0].PostTasks[0].Name)
}
func TestParsePlaybook_Good_Handlers(t *testing.T) {
func TestParser_ParsePlaybook_Good_Handlers(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "playbook.yml")
path := joinPath(dir, "playbook.yml")
yaml := `---
- name: With handlers
@ -155,7 +153,7 @@ func TestParsePlaybook_Good_Handlers(t *testing.T) {
name: nginx
state: restarted
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
plays, err := p.ParsePlaybook(path)
@ -167,9 +165,9 @@ func TestParsePlaybook_Good_Handlers(t *testing.T) {
assert.Equal(t, "service", plays[0].Handlers[0].Module)
}
func TestParsePlaybook_Good_ShellFreeForm(t *testing.T) {
func TestParser_ParsePlaybook_Good_ShellFreeForm(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "playbook.yml")
path := joinPath(dir, "playbook.yml")
yaml := `---
- name: Shell tasks
@ -180,7 +178,7 @@ func TestParsePlaybook_Good_ShellFreeForm(t *testing.T) {
- name: Run raw command
command: ls -la /tmp
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
plays, err := p.ParsePlaybook(path)
@ -193,9 +191,9 @@ func TestParsePlaybook_Good_ShellFreeForm(t *testing.T) {
assert.Equal(t, "ls -la /tmp", plays[0].Tasks[1].Args["_raw_params"])
}
func TestParsePlaybook_Good_WithTags(t *testing.T) {
func TestParser_ParsePlaybook_Good_WithTags(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "playbook.yml")
path := joinPath(dir, "playbook.yml")
yaml := `---
- name: Tagged play
@ -210,7 +208,7 @@ func TestParsePlaybook_Good_WithTags(t *testing.T) {
- debug
- always
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
plays, err := p.ParsePlaybook(path)
@ -220,9 +218,9 @@ func TestParsePlaybook_Good_WithTags(t *testing.T) {
assert.Equal(t, []string{"debug", "always"}, plays[0].Tasks[0].Tags)
}
func TestParsePlaybook_Good_BlockRescueAlways(t *testing.T) {
func TestParser_ParsePlaybook_Good_BlockRescueAlways(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "playbook.yml")
path := joinPath(dir, "playbook.yml")
yaml := `---
- name: With blocks
@ -241,7 +239,7 @@ func TestParsePlaybook_Good_BlockRescueAlways(t *testing.T) {
debug:
msg: "always"
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
plays, err := p.ParsePlaybook(path)
@ -256,9 +254,9 @@ func TestParsePlaybook_Good_BlockRescueAlways(t *testing.T) {
assert.Equal(t, "Always runs", task.Always[0].Name)
}
func TestParsePlaybook_Good_WithLoop(t *testing.T) {
func TestParser_ParsePlaybook_Good_WithLoop(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "playbook.yml")
path := joinPath(dir, "playbook.yml")
yaml := `---
- name: Loop test
@ -273,7 +271,7 @@ func TestParsePlaybook_Good_WithLoop(t *testing.T) {
- curl
- git
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
plays, err := p.ParsePlaybook(path)
@ -286,9 +284,9 @@ func TestParsePlaybook_Good_WithLoop(t *testing.T) {
assert.Len(t, items, 3)
}
func TestParsePlaybook_Good_RoleRefs(t *testing.T) {
func TestParser_ParsePlaybook_Good_RoleRefs(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "playbook.yml")
path := joinPath(dir, "playbook.yml")
yaml := `---
- name: With roles
@ -301,7 +299,7 @@ func TestParsePlaybook_Good_RoleRefs(t *testing.T) {
tags:
- web
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
plays, err := p.ParsePlaybook(path)
@ -314,9 +312,9 @@ func TestParsePlaybook_Good_RoleRefs(t *testing.T) {
assert.Equal(t, []string{"web"}, plays[0].Roles[1].Tags)
}
func TestParsePlaybook_Good_FullyQualifiedModules(t *testing.T) {
func TestParser_ParsePlaybook_Good_FullyQualifiedModules(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "playbook.yml")
path := joinPath(dir, "playbook.yml")
yaml := `---
- name: FQCN modules
@ -329,7 +327,7 @@ func TestParsePlaybook_Good_FullyQualifiedModules(t *testing.T) {
- name: Run shell
ansible.builtin.shell: echo hello
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
plays, err := p.ParsePlaybook(path)
@ -341,9 +339,9 @@ func TestParsePlaybook_Good_FullyQualifiedModules(t *testing.T) {
assert.Equal(t, "echo hello", plays[0].Tasks[1].Args["_raw_params"])
}
func TestParsePlaybook_Good_RegisterAndWhen(t *testing.T) {
func TestParser_ParsePlaybook_Good_RegisterAndWhen(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "playbook.yml")
path := joinPath(dir, "playbook.yml")
yaml := `---
- name: Conditional play
@ -358,7 +356,7 @@ func TestParsePlaybook_Good_RegisterAndWhen(t *testing.T) {
msg: "File exists"
when: nginx_conf.stat.exists
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
plays, err := p.ParsePlaybook(path)
@ -368,11 +366,11 @@ func TestParsePlaybook_Good_RegisterAndWhen(t *testing.T) {
assert.NotNil(t, plays[0].Tasks[1].When)
}
func TestParsePlaybook_Good_EmptyPlaybook(t *testing.T) {
func TestParser_ParsePlaybook_Good_EmptyPlaybook(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "playbook.yml")
path := joinPath(dir, "playbook.yml")
require.NoError(t, os.WriteFile(path, []byte("---\n[]"), 0644))
require.NoError(t, writeTestFile(path, []byte("---\n[]"), 0644))
p := NewParser(dir)
plays, err := p.ParsePlaybook(path)
@ -381,11 +379,11 @@ func TestParsePlaybook_Good_EmptyPlaybook(t *testing.T) {
assert.Empty(t, plays)
}
func TestParsePlaybook_Bad_InvalidYAML(t *testing.T) {
func TestParser_ParsePlaybook_Bad_InvalidYAML(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "bad.yml")
path := joinPath(dir, "bad.yml")
require.NoError(t, os.WriteFile(path, []byte("{{invalid yaml}}"), 0644))
require.NoError(t, writeTestFile(path, []byte("{{invalid yaml}}"), 0644))
p := NewParser(dir)
_, err := p.ParsePlaybook(path)
@ -394,7 +392,7 @@ func TestParsePlaybook_Bad_InvalidYAML(t *testing.T) {
assert.Contains(t, err.Error(), "parse playbook")
}
func TestParsePlaybook_Bad_FileNotFound(t *testing.T) {
func TestParser_ParsePlaybook_Bad_FileNotFound(t *testing.T) {
p := NewParser(t.TempDir())
_, err := p.ParsePlaybook("/nonexistent/playbook.yml")
@ -402,9 +400,9 @@ func TestParsePlaybook_Bad_FileNotFound(t *testing.T) {
assert.Contains(t, err.Error(), "read playbook")
}
func TestParsePlaybook_Good_GatherFactsDisabled(t *testing.T) {
func TestParser_ParsePlaybook_Good_GatherFactsDisabled(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "playbook.yml")
path := joinPath(dir, "playbook.yml")
yaml := `---
- name: No facts
@ -412,7 +410,7 @@ func TestParsePlaybook_Good_GatherFactsDisabled(t *testing.T) {
gather_facts: false
tasks: []
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
plays, err := p.ParsePlaybook(path)
@ -424,9 +422,9 @@ func TestParsePlaybook_Good_GatherFactsDisabled(t *testing.T) {
// --- ParseInventory ---
func TestParseInventory_Good_SimpleInventory(t *testing.T) {
func TestParser_ParseInventory_Good_SimpleInventory(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "inventory.yml")
path := joinPath(dir, "inventory.yml")
yaml := `---
all:
@ -436,7 +434,7 @@ all:
web2:
ansible_host: 192.168.1.11
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
inv, err := p.ParseInventory(path)
@ -448,9 +446,9 @@ all:
assert.Equal(t, "192.168.1.11", inv.All.Hosts["web2"].AnsibleHost)
}
func TestParseInventory_Good_WithGroups(t *testing.T) {
func TestParser_ParseInventory_Good_WithGroups(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "inventory.yml")
path := joinPath(dir, "inventory.yml")
yaml := `---
all:
@ -466,7 +464,7 @@ all:
db1:
ansible_host: 10.0.1.1
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
inv, err := p.ParseInventory(path)
@ -478,9 +476,9 @@ all:
assert.Len(t, inv.All.Children["databases"].Hosts, 1)
}
func TestParseInventory_Good_WithVars(t *testing.T) {
func TestParser_ParseInventory_Good_WithVars(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "inventory.yml")
path := joinPath(dir, "inventory.yml")
yaml := `---
all:
@ -495,7 +493,7 @@ all:
ansible_host: 10.0.0.1
ansible_port: 2222
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
inv, err := p.ParseInventory(path)
@ -506,11 +504,11 @@ all:
assert.Equal(t, 2222, inv.All.Children["production"].Hosts["prod1"].AnsiblePort)
}
func TestParseInventory_Bad_InvalidYAML(t *testing.T) {
func TestParser_ParseInventory_Bad_InvalidYAML(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "bad.yml")
path := joinPath(dir, "bad.yml")
require.NoError(t, os.WriteFile(path, []byte("{{{bad"), 0644))
require.NoError(t, writeTestFile(path, []byte("{{{bad"), 0644))
p := NewParser(dir)
_, err := p.ParseInventory(path)
@ -519,7 +517,7 @@ func TestParseInventory_Bad_InvalidYAML(t *testing.T) {
assert.Contains(t, err.Error(), "parse inventory")
}
func TestParseInventory_Bad_FileNotFound(t *testing.T) {
func TestParser_ParseInventory_Bad_FileNotFound(t *testing.T) {
p := NewParser(t.TempDir())
_, err := p.ParseInventory("/nonexistent/inventory.yml")
@ -529,9 +527,9 @@ func TestParseInventory_Bad_FileNotFound(t *testing.T) {
// --- ParseTasks ---
func TestParseTasks_Good_TaskFile(t *testing.T) {
func TestParser_ParseTasks_Good_TaskFile(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "tasks.yml")
path := joinPath(dir, "tasks.yml")
yaml := `---
- name: First task
@ -541,7 +539,7 @@ func TestParseTasks_Good_TaskFile(t *testing.T) {
src: /tmp/a
dest: /tmp/b
`
require.NoError(t, os.WriteFile(path, []byte(yaml), 0644))
require.NoError(t, writeTestFile(path, []byte(yaml), 0644))
p := NewParser(dir)
tasks, err := p.ParseTasks(path)
@ -554,11 +552,11 @@ func TestParseTasks_Good_TaskFile(t *testing.T) {
assert.Equal(t, "/tmp/a", tasks[1].Args["src"])
}
func TestParseTasks_Bad_InvalidYAML(t *testing.T) {
func TestParser_ParseTasks_Bad_InvalidYAML(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "bad.yml")
path := joinPath(dir, "bad.yml")
require.NoError(t, os.WriteFile(path, []byte("not: [valid: tasks"), 0644))
require.NoError(t, writeTestFile(path, []byte("not: [valid: tasks"), 0644))
p := NewParser(dir)
_, err := p.ParseTasks(path)
@ -568,7 +566,7 @@ func TestParseTasks_Bad_InvalidYAML(t *testing.T) {
// --- GetHosts ---
func TestGetHosts_Good_AllPattern(t *testing.T) {
func TestParser_GetHosts_Good_AllPattern(t *testing.T) {
inv := &Inventory{
All: &InventoryGroup{
Hosts: map[string]*Host{
@ -584,13 +582,13 @@ func TestGetHosts_Good_AllPattern(t *testing.T) {
assert.Contains(t, hosts, "host2")
}
func TestGetHosts_Good_LocalhostPattern(t *testing.T) {
func TestParser_GetHosts_Good_LocalhostPattern(t *testing.T) {
inv := &Inventory{All: &InventoryGroup{}}
hosts := GetHosts(inv, "localhost")
assert.Equal(t, []string{"localhost"}, hosts)
}
func TestGetHosts_Good_GroupPattern(t *testing.T) {
func TestParser_GetHosts_Good_GroupPattern(t *testing.T) {
inv := &Inventory{
All: &InventoryGroup{
Children: map[string]*InventoryGroup{
@ -615,7 +613,7 @@ func TestGetHosts_Good_GroupPattern(t *testing.T) {
assert.Contains(t, hosts, "web2")
}
func TestGetHosts_Good_SpecificHost(t *testing.T) {
func TestParser_GetHosts_Good_SpecificHost(t *testing.T) {
inv := &Inventory{
All: &InventoryGroup{
Children: map[string]*InventoryGroup{
@ -632,7 +630,7 @@ func TestGetHosts_Good_SpecificHost(t *testing.T) {
assert.Equal(t, []string{"myhost"}, hosts)
}
func TestGetHosts_Good_AllIncludesChildren(t *testing.T) {
func TestParser_GetHosts_Good_AllIncludesChildren(t *testing.T) {
inv := &Inventory{
All: &InventoryGroup{
Hosts: map[string]*Host{"top": {}},
@ -650,7 +648,7 @@ func TestGetHosts_Good_AllIncludesChildren(t *testing.T) {
assert.Contains(t, hosts, "child1")
}
func TestGetHosts_Bad_NoMatch(t *testing.T) {
func TestParser_GetHosts_Bad_NoMatch(t *testing.T) {
inv := &Inventory{
All: &InventoryGroup{
Hosts: map[string]*Host{"host1": {}},
@ -661,7 +659,7 @@ func TestGetHosts_Bad_NoMatch(t *testing.T) {
assert.Empty(t, hosts)
}
func TestGetHosts_Bad_NilGroup(t *testing.T) {
func TestParser_GetHosts_Bad_NilGroup(t *testing.T) {
inv := &Inventory{All: nil}
hosts := GetHosts(inv, "all")
assert.Empty(t, hosts)
@ -669,7 +667,7 @@ func TestGetHosts_Bad_NilGroup(t *testing.T) {
// --- GetHostVars ---
func TestGetHostVars_Good_DirectHost(t *testing.T) {
func TestParser_GetHostVars_Good_DirectHost(t *testing.T) {
inv := &Inventory{
All: &InventoryGroup{
Vars: map[string]any{"global_var": "global"},
@ -690,7 +688,7 @@ func TestGetHostVars_Good_DirectHost(t *testing.T) {
assert.Equal(t, "global", vars["global_var"])
}
func TestGetHostVars_Good_InheritedGroupVars(t *testing.T) {
func TestParser_GetHostVars_Good_InheritedGroupVars(t *testing.T) {
inv := &Inventory{
All: &InventoryGroup{
Vars: map[string]any{"level": "all"},
@ -712,7 +710,7 @@ func TestGetHostVars_Good_InheritedGroupVars(t *testing.T) {
assert.Equal(t, "prod", vars["env"])
}
func TestGetHostVars_Good_HostNotFound(t *testing.T) {
func TestParser_GetHostVars_Good_HostNotFound(t *testing.T) {
inv := &Inventory{
All: &InventoryGroup{
Hosts: map[string]*Host{"other": {}},
@ -725,7 +723,7 @@ func TestGetHostVars_Good_HostNotFound(t *testing.T) {
// --- isModule ---
func TestIsModule_Good_KnownModules(t *testing.T) {
func TestParser_IsModule_Good_KnownModules(t *testing.T) {
assert.True(t, isModule("shell"))
assert.True(t, isModule("command"))
assert.True(t, isModule("copy"))
@ -737,39 +735,39 @@ func TestIsModule_Good_KnownModules(t *testing.T) {
assert.True(t, isModule("set_fact"))
}
func TestIsModule_Good_FQCN(t *testing.T) {
func TestParser_IsModule_Good_FQCN(t *testing.T) {
assert.True(t, isModule("ansible.builtin.shell"))
assert.True(t, isModule("ansible.builtin.copy"))
assert.True(t, isModule("ansible.builtin.apt"))
}
func TestIsModule_Good_DottedUnknown(t *testing.T) {
func TestParser_IsModule_Good_DottedUnknown(t *testing.T) {
// Any key with dots is considered a module
assert.True(t, isModule("community.general.ufw"))
assert.True(t, isModule("ansible.posix.authorized_key"))
}
func TestIsModule_Bad_NotAModule(t *testing.T) {
func TestParser_IsModule_Bad_NotAModule(t *testing.T) {
assert.False(t, isModule("some_random_key"))
assert.False(t, isModule("foobar"))
}
// --- NormalizeModule ---
func TestNormalizeModule_Good(t *testing.T) {
func TestParser_NormalizeModule_Good(t *testing.T) {
assert.Equal(t, "ansible.builtin.shell", NormalizeModule("shell"))
assert.Equal(t, "ansible.builtin.copy", NormalizeModule("copy"))
assert.Equal(t, "ansible.builtin.apt", NormalizeModule("apt"))
}
func TestNormalizeModule_Good_AlreadyFQCN(t *testing.T) {
func TestParser_NormalizeModule_Good_AlreadyFQCN(t *testing.T) {
assert.Equal(t, "ansible.builtin.shell", NormalizeModule("ansible.builtin.shell"))
assert.Equal(t, "community.general.ufw", NormalizeModule("community.general.ufw"))
}
// --- NewParser ---
func TestNewParser_Good(t *testing.T) {
func TestParser_NewParser_Good(t *testing.T) {
p := NewParser("/some/path")
assert.NotNil(t, p)
assert.Equal(t, "/some/path", p.basePath)

52
ssh.go
View file

@ -4,8 +4,8 @@ import (
"bytes"
"context"
"io"
"io/fs"
"net"
"os"
"sync"
"time"
@ -16,6 +16,10 @@ import (
)
// SSHClient handles SSH connections to remote hosts.
//
// Example:
//
// client, _ := NewSSHClient(SSHConfig{Host: "web1"})
type SSHClient struct {
host string
port int
@ -31,6 +35,10 @@ type SSHClient struct {
}
// SSHConfig holds SSH connection configuration.
//
// Example:
//
// cfg := SSHConfig{Host: "web1", User: "deploy", Port: 22}
type SSHConfig struct {
Host string
Port int
@ -44,6 +52,10 @@ type SSHConfig struct {
}
// NewSSHClient creates a new SSH client.
//
// Example:
//
// client, err := NewSSHClient(SSHConfig{Host: "web1", User: "deploy"})
func NewSSHClient(cfg SSHConfig) (*SSHClient, error) {
if cfg.Port == 0 {
cfg.Port = 22
@ -71,6 +83,10 @@ func NewSSHClient(cfg SSHConfig) (*SSHClient, error) {
}
// Connect establishes the SSH connection.
//
// Example:
//
// _ = client.Connect(context.Background())
func (c *SSHClient) Connect(ctx context.Context) error {
c.mu.Lock()
defer c.mu.Unlock()
@ -180,6 +196,10 @@ func (c *SSHClient) Connect(ctx context.Context) error {
}
// Close closes the SSH connection.
//
// Example:
//
// _ = client.Close()
func (c *SSHClient) Close() error {
c.mu.Lock()
defer c.mu.Unlock()
@ -193,6 +213,10 @@ func (c *SSHClient) Close() error {
}
// Run executes a command on the remote host.
//
// Example:
//
// stdout, stderr, rc, err := client.Run(context.Background(), "hostname")
func (c *SSHClient) Run(ctx context.Context, cmd string) (stdout, stderr string, exitCode int, err error) {
if err := c.Connect(ctx); err != nil {
return "", "", -1, err
@ -269,6 +293,10 @@ func (c *SSHClient) Run(ctx context.Context, cmd string) (stdout, stderr string,
}
// RunScript runs a script on the remote host.
//
// Example:
//
// stdout, stderr, rc, err := client.RunScript(context.Background(), "echo hello")
func (c *SSHClient) RunScript(ctx context.Context, script string) (stdout, stderr string, exitCode int, err error) {
// Escape the script for heredoc
cmd := sprintf("bash <<'ANSIBLE_SCRIPT_EOF'\n%s\nANSIBLE_SCRIPT_EOF", script)
@ -276,7 +304,11 @@ func (c *SSHClient) RunScript(ctx context.Context, script string) (stdout, stder
}
// Upload copies a file to the remote host.
func (c *SSHClient) Upload(ctx context.Context, local io.Reader, remote string, mode os.FileMode) error {
//
// Example:
//
// err := client.Upload(context.Background(), newReader("hello"), "/tmp/hello.txt", 0644)
func (c *SSHClient) Upload(ctx context.Context, local io.Reader, remote string, mode fs.FileMode) error {
if err := c.Connect(ctx); err != nil {
return err
}
@ -370,6 +402,10 @@ func (c *SSHClient) Upload(ctx context.Context, local io.Reader, remote string,
}
// Download copies a file from the remote host.
//
// Example:
//
// data, err := client.Download(context.Background(), "/etc/hostname")
func (c *SSHClient) Download(ctx context.Context, remote string) ([]byte, error) {
if err := c.Connect(ctx); err != nil {
return nil, err
@ -389,6 +425,10 @@ func (c *SSHClient) Download(ctx context.Context, remote string) ([]byte, error)
}
// FileExists checks if a file exists on the remote host.
//
// Example:
//
// ok, err := client.FileExists(context.Background(), "/etc/hosts")
func (c *SSHClient) FileExists(ctx context.Context, path string) (bool, error) {
cmd := sprintf("test -e %q && echo yes || echo no", path)
stdout, _, exitCode, err := c.Run(ctx, cmd)
@ -403,6 +443,10 @@ func (c *SSHClient) FileExists(ctx context.Context, path string) (bool, error) {
}
// Stat returns file info from the remote host.
//
// Example:
//
// info, err := client.Stat(context.Background(), "/etc/hosts")
func (c *SSHClient) Stat(ctx context.Context, path string) (map[string]any, error) {
// Simple approach - get basic file info
cmd := sprintf(`
@ -435,6 +479,10 @@ fi
}
// SetBecome enables privilege escalation.
//
// Example:
//
// client.SetBecome(true, "root", "")
func (c *SSHClient) SetBecome(become bool, user, password string) {
c.mu.Lock()
defer c.mu.Unlock()

View file

@ -7,7 +7,7 @@ import (
"github.com/stretchr/testify/assert"
)
func TestNewSSHClient(t *testing.T) {
func TestSSH_NewSSHClient_Good_CustomConfig(t *testing.T) {
cfg := SSHConfig{
Host: "localhost",
Port: 2222,
@ -23,7 +23,7 @@ func TestNewSSHClient(t *testing.T) {
assert.Equal(t, 30*time.Second, client.timeout)
}
func TestSSHConfig_Defaults(t *testing.T) {
func TestSSH_NewSSHClient_Good_Defaults(t *testing.T) {
cfg := SSHConfig{
Host: "localhost",
}

23
test_primitives_test.go Normal file
View file

@ -0,0 +1,23 @@
package ansible
import (
"io/fs"
coreio "dappco.re/go/core/io"
)
func readTestFile(path string) ([]byte, error) {
content, err := coreio.Local.Read(path)
if err != nil {
return nil, err
}
return []byte(content), nil
}
func writeTestFile(path string, content []byte, mode fs.FileMode) error {
return coreio.Local.WriteMode(path, string(content), mode)
}
func joinStrings(parts []string, sep string) string {
return join(sep, parts)
}

View file

@ -5,11 +5,19 @@ import (
)
// Playbook represents an Ansible playbook.
//
// Example:
//
// playbook := Playbook{Plays: []Play{{Name: "Bootstrap", Hosts: "all"}}}
type Playbook struct {
Plays []Play `yaml:",inline"`
}
// Play represents a single play in a playbook.
//
// Example:
//
// play := Play{Name: "Configure web", Hosts: "webservers", Become: true}
type Play struct {
Name string `yaml:"name"`
Hosts string `yaml:"hosts"`
@ -30,6 +38,10 @@ type Play struct {
}
// RoleRef represents a role reference in a play.
//
// Example:
//
// role := RoleRef{Role: "nginx", TasksFrom: "install.yml"}
type RoleRef struct {
Role string `yaml:"role,omitempty"`
Name string `yaml:"name,omitempty"` // Alternative to role
@ -40,6 +52,11 @@ type RoleRef struct {
}
// UnmarshalYAML handles both string and struct role refs.
//
// Example:
//
// var ref RoleRef
// _ = yaml.Unmarshal([]byte("common"), &ref)
func (r *RoleRef) UnmarshalYAML(unmarshal func(any) error) error {
// Try string first
var s string
@ -62,6 +79,10 @@ func (r *RoleRef) UnmarshalYAML(unmarshal func(any) error) error {
}
// Task represents an Ansible task.
//
// Example:
//
// task := Task{Name: "Install nginx", Module: "apt", Args: map[string]any{"name": "nginx"}}
type Task struct {
Name string `yaml:"name,omitempty"`
Module string `yaml:"-"` // Derived from the module key
@ -108,6 +129,10 @@ type Task struct {
}
// LoopControl controls loop behavior.
//
// Example:
//
// loop := LoopControl{LoopVar: "item", IndexVar: "idx"}
type LoopControl struct {
LoopVar string `yaml:"loop_var,omitempty"`
IndexVar string `yaml:"index_var,omitempty"`
@ -117,6 +142,10 @@ type LoopControl struct {
}
// TaskResult holds the result of executing a task.
//
// Example:
//
// result := TaskResult{Changed: true, Stdout: "ok"}
type TaskResult struct {
Changed bool `json:"changed"`
Failed bool `json:"failed"`
@ -131,11 +160,19 @@ type TaskResult struct {
}
// Inventory represents Ansible inventory.
//
// Example:
//
// inv := Inventory{All: &InventoryGroup{Hosts: map[string]*Host{"web1": {AnsibleHost: "10.0.0.1"}}}}
type Inventory struct {
All *InventoryGroup `yaml:"all"`
}
// InventoryGroup represents a group in inventory.
//
// Example:
//
// group := InventoryGroup{Hosts: map[string]*Host{"db1": {AnsibleHost: "10.0.1.10"}}}
type InventoryGroup struct {
Hosts map[string]*Host `yaml:"hosts,omitempty"`
Children map[string]*InventoryGroup `yaml:"children,omitempty"`
@ -143,6 +180,10 @@ type InventoryGroup struct {
}
// Host represents a host in inventory.
//
// Example:
//
// host := Host{AnsibleHost: "192.168.1.10", AnsibleUser: "deploy"}
type Host struct {
AnsibleHost string `yaml:"ansible_host,omitempty"`
AnsiblePort int `yaml:"ansible_port,omitempty"`
@ -157,6 +198,10 @@ type Host struct {
}
// Facts holds gathered facts about a host.
//
// Example:
//
// facts := Facts{Hostname: "web1", Distribution: "Ubuntu", Kernel: "Linux"}
type Facts struct {
Hostname string `json:"ansible_hostname"`
FQDN string `json:"ansible_fqdn"`
@ -170,7 +215,13 @@ type Facts struct {
IPv4 string `json:"ansible_default_ipv4_address"`
}
// Known Ansible modules
// KnownModules lists the Ansible module names recognized by the parser.
//
// Example:
//
// if slices.Contains(KnownModules, "ansible.builtin.command") {
// // parser accepts command tasks
// }
var KnownModules = []string{
// Builtin
"ansible.builtin.shell",

View file

@ -10,7 +10,7 @@ import (
// --- RoleRef UnmarshalYAML ---
func TestRoleRef_UnmarshalYAML_Good_StringForm(t *testing.T) {
func TestTypes_RoleRef_UnmarshalYAML_Good_StringForm(t *testing.T) {
input := `common`
var ref RoleRef
err := yaml.Unmarshal([]byte(input), &ref)
@ -19,7 +19,7 @@ func TestRoleRef_UnmarshalYAML_Good_StringForm(t *testing.T) {
assert.Equal(t, "common", ref.Role)
}
func TestRoleRef_UnmarshalYAML_Good_StructForm(t *testing.T) {
func TestTypes_RoleRef_UnmarshalYAML_Good_StructForm(t *testing.T) {
input := `
role: webserver
vars:
@ -36,7 +36,7 @@ tags:
assert.Equal(t, []string{"web"}, ref.Tags)
}
func TestRoleRef_UnmarshalYAML_Good_NameField(t *testing.T) {
func TestTypes_RoleRef_UnmarshalYAML_Good_NameField(t *testing.T) {
// Some playbooks use "name:" instead of "role:"
input := `
name: myapp
@ -50,7 +50,7 @@ tasks_from: install.yml
assert.Equal(t, "install.yml", ref.TasksFrom)
}
func TestRoleRef_UnmarshalYAML_Good_WithWhen(t *testing.T) {
func TestTypes_RoleRef_UnmarshalYAML_Good_WithWhen(t *testing.T) {
input := `
role: conditional_role
when: ansible_os_family == "Debian"
@ -65,7 +65,7 @@ when: ansible_os_family == "Debian"
// --- Task UnmarshalYAML ---
func TestTask_UnmarshalYAML_Good_ModuleWithArgs(t *testing.T) {
func TestTypes_Task_UnmarshalYAML_Good_ModuleWithArgs(t *testing.T) {
input := `
name: Install nginx
apt:
@ -82,7 +82,7 @@ apt:
assert.Equal(t, "present", task.Args["state"])
}
func TestTask_UnmarshalYAML_Good_FreeFormModule(t *testing.T) {
func TestTypes_Task_UnmarshalYAML_Good_FreeFormModule(t *testing.T) {
input := `
name: Run command
shell: echo hello world
@ -95,7 +95,7 @@ shell: echo hello world
assert.Equal(t, "echo hello world", task.Args["_raw_params"])
}
func TestTask_UnmarshalYAML_Good_ModuleNoArgs(t *testing.T) {
func TestTypes_Task_UnmarshalYAML_Good_ModuleNoArgs(t *testing.T) {
input := `
name: Gather facts
setup:
@ -108,7 +108,7 @@ setup:
assert.NotNil(t, task.Args)
}
func TestTask_UnmarshalYAML_Good_WithRegister(t *testing.T) {
func TestTypes_Task_UnmarshalYAML_Good_WithRegister(t *testing.T) {
input := `
name: Check file
stat:
@ -123,7 +123,7 @@ register: stat_result
assert.Equal(t, "stat", task.Module)
}
func TestTask_UnmarshalYAML_Good_WithWhen(t *testing.T) {
func TestTypes_Task_UnmarshalYAML_Good_WithWhen(t *testing.T) {
input := `
name: Conditional task
debug:
@ -137,7 +137,7 @@ when: some_var is defined
assert.NotNil(t, task.When)
}
func TestTask_UnmarshalYAML_Good_WithLoop(t *testing.T) {
func TestTypes_Task_UnmarshalYAML_Good_WithLoop(t *testing.T) {
input := `
name: Install packages
apt:
@ -156,7 +156,7 @@ loop:
assert.Len(t, items, 3)
}
func TestTask_UnmarshalYAML_Good_WithItems(t *testing.T) {
func TestTypes_Task_UnmarshalYAML_Good_WithItems(t *testing.T) {
// with_items should be converted to loop
input := `
name: Old-style loop
@ -176,7 +176,7 @@ with_items:
assert.Len(t, items, 2)
}
func TestTask_UnmarshalYAML_Good_WithNotify(t *testing.T) {
func TestTypes_Task_UnmarshalYAML_Good_WithNotify(t *testing.T) {
input := `
name: Install package
apt:
@ -190,7 +190,7 @@ notify: restart nginx
assert.Equal(t, "restart nginx", task.Notify)
}
func TestTask_UnmarshalYAML_Good_WithNotifyList(t *testing.T) {
func TestTypes_Task_UnmarshalYAML_Good_WithNotifyList(t *testing.T) {
input := `
name: Install package
apt:
@ -208,7 +208,7 @@ notify:
assert.Len(t, notifyList, 2)
}
func TestTask_UnmarshalYAML_Good_IncludeTasks(t *testing.T) {
func TestTypes_Task_UnmarshalYAML_Good_IncludeTasks(t *testing.T) {
input := `
name: Include tasks
include_tasks: other-tasks.yml
@ -220,7 +220,7 @@ include_tasks: other-tasks.yml
assert.Equal(t, "other-tasks.yml", task.IncludeTasks)
}
func TestTask_UnmarshalYAML_Good_IncludeRole(t *testing.T) {
func TestTypes_Task_UnmarshalYAML_Good_IncludeRole(t *testing.T) {
input := `
name: Include role
include_role:
@ -236,7 +236,7 @@ include_role:
assert.Equal(t, "setup.yml", task.IncludeRole.TasksFrom)
}
func TestTask_UnmarshalYAML_Good_BecomeFields(t *testing.T) {
func TestTypes_Task_UnmarshalYAML_Good_BecomeFields(t *testing.T) {
input := `
name: Privileged task
shell: systemctl restart nginx
@ -252,7 +252,7 @@ become_user: root
assert.Equal(t, "root", task.BecomeUser)
}
func TestTask_UnmarshalYAML_Good_IgnoreErrors(t *testing.T) {
func TestTypes_Task_UnmarshalYAML_Good_IgnoreErrors(t *testing.T) {
input := `
name: Might fail
shell: some risky command
@ -267,7 +267,7 @@ ignore_errors: true
// --- Inventory data structure ---
func TestInventory_UnmarshalYAML_Good_Complex(t *testing.T) {
func TestTypes_Inventory_UnmarshalYAML_Good_Complex(t *testing.T) {
input := `
all:
vars:
@ -317,7 +317,7 @@ all:
// --- Facts ---
func TestFacts_Struct(t *testing.T) {
func TestTypes_Facts_Good_Struct(t *testing.T) {
facts := Facts{
Hostname: "web1",
FQDN: "web1.example.com",
@ -341,7 +341,7 @@ func TestFacts_Struct(t *testing.T) {
// --- TaskResult ---
func TestTaskResult_Struct(t *testing.T) {
func TestTypes_TaskResult_Good_Struct(t *testing.T) {
result := TaskResult{
Changed: true,
Failed: false,
@ -358,7 +358,7 @@ func TestTaskResult_Struct(t *testing.T) {
assert.Equal(t, 0, result.RC)
}
func TestTaskResult_WithLoopResults(t *testing.T) {
func TestTypes_TaskResult_Good_WithLoopResults(t *testing.T) {
result := TaskResult{
Changed: true,
Results: []TaskResult{
@ -375,7 +375,7 @@ func TestTaskResult_WithLoopResults(t *testing.T) {
// --- KnownModules ---
func TestKnownModules_ContainsExpected(t *testing.T) {
func TestTypes_KnownModules_Good_ContainsExpected(t *testing.T) {
// Verify both FQCN and short forms are present
fqcnModules := []string{
"ansible.builtin.shell",