From f127ac2fcb64039fdf8655aaea0f18685d37e440 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 26 Mar 2026 16:39:59 +0000 Subject: [PATCH] chore: polish ax v0.8.0 compliance Co-Authored-By: Virgil --- cmd/ansible/cmd.go | 5 + core_primitives.go | 1 + executor.go | 32 ++++ executor_extra_test.go | 91 +++++------ executor_test.go | 88 +++++----- mock_ssh_test.go | 344 +++++++++++++++++++++++----------------- modules.go | 10 +- modules_adv_test.go | 148 +++++++++-------- modules_cmd_test.go | 122 +++++++------- modules_file_test.go | 137 ++++++++-------- modules_infra_test.go | 222 +++++++++++++------------- modules_svc_test.go | 114 ++++++------- parser.go | 57 +++++++ parser_test.go | 166 ++++++++++--------- ssh.go | 52 +++++- ssh_test.go | 4 +- test_primitives_test.go | 23 +++ types.go | 53 ++++++- types_test.go | 44 ++--- 19 files changed, 984 insertions(+), 729 deletions(-) create mode 100644 test_primitives_test.go diff --git a/cmd/ansible/cmd.go b/cmd/ansible/cmd.go index 7f84a99..6aa8c58 100644 --- a/cmd/ansible/cmd.go +++ b/cmd/ansible/cmd.go @@ -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)", diff --git a/core_primitives.go b/core_primitives.go index a262fa2..c8ba482 100644 --- a/core_primitives.go +++ b/core_primitives.go @@ -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) } diff --git a/executor.go b/executor.go index 5b11f7f..21010a7 100644 --- a/executor.go +++ b/executor.go @@ -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 { diff --git a/executor_extra_test.go b/executor_extra_test.go index c3617d3..d66e725 100644 --- a/executor_extra_test.go +++ b/executor_extra_test.go @@ -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 " diff --git a/executor_test.go b/executor_test.go index 4ac9d1c..0e67bd7 100644 --- a/executor_test.go +++ b/executor_test.go @@ -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() diff --git a/mock_ssh_test.go b/mock_ssh_test.go index 6452d74..bc60809 100644 --- a/mock_ssh_test.go +++ b/mock_ssh_test.go @@ -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 } } diff --git a/modules.go b/modules.go index 2a0d815..0c4e189 100644 --- a/modules.go +++ b/modules.go @@ -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) } } diff --git a/modules_adv_test.go b/modules_adv_test.go index 389c5dd..e336c8e 100644 --- a/modules_adv_test.go +++ b/modules_adv_test.go @@ -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) diff --git a/modules_cmd_test.go b/modules_cmd_test.go index cf4f28e..59920c8 100644 --- a/modules_cmd_test.go +++ b/modules_cmd_test.go @@ -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) diff --git a/modules_file_test.go b/modules_file_test.go index cf3cd33..e1a3e56 100644 --- a/modules_file_test.go +++ b/modules_file_test.go @@ -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") diff --git a/modules_infra_test.go b/modules_infra_test.go index 14b6ea3..09e008a 100644 --- a/modules_infra_test.go +++ b/modules_infra_test.go @@ -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' { diff --git a/modules_svc_test.go b/modules_svc_test.go index 8d642eb..b3952bf 100644 --- a/modules_svc_test.go +++ b/modules_svc_test.go @@ -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) diff --git a/parser.go b/parser.go index f5378c7..aa4bc18 100644 --- a/parser.go +++ b/parser.go @@ -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) diff --git a/parser_test.go b/parser_test.go index 8712e72..96725c3 100644 --- a/parser_test.go +++ b/parser_test.go @@ -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) diff --git a/ssh.go b/ssh.go index b0afb6e..26ef65a 100644 --- a/ssh.go +++ b/ssh.go @@ -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() diff --git a/ssh_test.go b/ssh_test.go index 17179b0..5b12e92 100644 --- a/ssh_test.go +++ b/ssh_test.go @@ -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", } diff --git a/test_primitives_test.go b/test_primitives_test.go new file mode 100644 index 0000000..ad11b98 --- /dev/null +++ b/test_primitives_test.go @@ -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) +} diff --git a/types.go b/types.go index 5a6939f..1907a78 100644 --- a/types.go +++ b/types.go @@ -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", diff --git a/types_test.go b/types_test.go index d11fe43..b8625b0 100644 --- a/types_test.go +++ b/types_test.go @@ -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",