chore: polish ax v0.8.0 compliance

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

View file

@ -5,6 +5,11 @@ import (
) )
// Register registers the 'ansible' command and all subcommands on the given Core instance. // 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) { func Register(c *core.Core) {
c.Command("ansible", core.Command{ c.Command("ansible", core.Command{
Description: "Run Ansible playbooks natively (no Python required)", Description: "Run Ansible playbooks natively (no Python required)",

View file

@ -280,6 +280,7 @@ func join(sep string, parts []string) string { return corexJo
func lower(s string) string { return corexLower(s) } func lower(s string) string { return corexLower(s) }
func replaceAll(s, old, new string) string { return corexReplaceAll(s, old, new) } 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 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 trimCutset(s, cutset string) string { return corexTrimCutset(s, cutset) }
func repeat(s string, count int) string { return corexRepeat(s, count) } func repeat(s string, count int) string { return corexRepeat(s, count) }
func fields(s string) []string { return corexFields(s) } func fields(s string) []string { return corexFields(s) }

View file

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

View file

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

View file

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

View file

@ -2,20 +2,23 @@ package ansible
import ( import (
"context" "context"
"fmt"
"io" "io"
"os" "io/fs"
"path/filepath"
"regexp" "regexp"
"strconv" "strconv"
"strings"
"sync" "sync"
core "dappco.re/go/core"
) )
// --- Mock SSH Client --- // --- Mock SSH Client ---
// MockSSHClient simulates an SSHClient for testing module logic // MockSSHClient simulates an SSHClient for testing module logic
// without requiring real SSH connections. // without requiring real SSH connections.
//
// Example:
//
// mock := NewMockSSHClient()
type MockSSHClient struct { type MockSSHClient struct {
mu sync.Mutex mu sync.Mutex
@ -59,10 +62,15 @@ type executedCommand struct {
type uploadRecord struct { type uploadRecord struct {
Content []byte Content []byte
Remote string Remote string
Mode os.FileMode Mode fs.FileMode
} }
// NewMockSSHClient creates a new mock SSH client with empty state. // NewMockSSHClient creates a new mock SSH client with empty state.
//
// Example:
//
// mock := NewMockSSHClient()
// mock.expectCommand("echo ok", "ok", "", 0)
func NewMockSSHClient() *MockSSHClient { func NewMockSSHClient() *MockSSHClient {
return &MockSSHClient{ return &MockSSHClient{
files: make(map[string][]byte), 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. // expectCommand registers a command pattern with a pre-configured response.
// The pattern is a regular expression matched against the full command string. // The pattern is a regular expression matched against the full command string.
func (m *MockSSHClient) expectCommand(pattern, stdout, stderr string, rc int) { 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 // Run simulates executing a command. It matches against registered
// expectations in order (last match wins) and records the execution. // 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) { func (m *MockSSHClient) Run(_ context.Context, cmd string) (string, string, int, error) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() 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. // 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) { func (m *MockSSHClient) RunScript(_ context.Context, script string) (string, string, int, error) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() 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. // 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() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
content, err := io.ReadAll(local) content, err := io.ReadAll(local)
if err != nil { 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{ 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. // 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) { func (m *MockSSHClient) Download(_ context.Context, remote string) ([]byte, error) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
content, ok := m.files[remote] content, ok := m.files[remote]
if !ok { 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 return content, nil
} }
// FileExists checks if a path exists in the simulated filesystem. // 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) { func (m *MockSSHClient) FileExists(_ context.Context, path string) (bool, error) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() 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 // Stat returns stat info from the pre-configured map, or constructs
// a basic result from the file existence in the simulated filesystem. // 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) { func (m *MockSSHClient) Stat(_ context.Context, path string) (map[string]any, error) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() 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. // SetBecome records become state changes.
//
// Example:
//
// mock.SetBecome(true, "root", "")
func (m *MockSSHClient) SetBecome(become bool, user, password string) { func (m *MockSSHClient) SetBecome(become bool, user, password string) {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() 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. // Close is a no-op for the mock.
//
// Example:
//
// _ = mock.Close()
func (m *MockSSHClient) Close() error { func (m *MockSSHClient) Close() error {
return nil return nil
} }
@ -426,7 +474,7 @@ func executeModuleWithMock(e *Executor, mock *MockSSHClient, host string, task *
return moduleDockerComposeWithClient(e, mock, args) return moduleDockerComposeWithClient(e, mock, args)
default: 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", "") cmd = getStringArg(args, "cmd", "")
} }
if 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 != "" { 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) 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", "") cmd = getStringArg(args, "cmd", "")
} }
if 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 != "" { 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) 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) { func moduleRawWithClient(_ *Executor, client sshRunner, args map[string]any) (*TaskResult, error) {
cmd := getStringArg(args, "_raw_params", "") cmd := getStringArg(args, "_raw_params", "")
if cmd == "" { 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) 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) { func moduleScriptWithClient(_ *Executor, client sshRunner, args map[string]any) (*TaskResult, error) {
script := getStringArg(args, "_raw_params", "") script := getStringArg(args, "_raw_params", "")
if script == "" { 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 { 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)) 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 { type sshFileRunner interface {
sshRunner 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) Stat(ctx context.Context, path string) (map[string]any, error)
FileExists(ctx context.Context, path string) (bool, 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) { func moduleCopyWithClient(e *Executor, client sshFileRunner, args map[string]any, host string, task *Task) (*TaskResult, error) {
dest := getStringArg(args, "dest", "") dest := getStringArg(args, "dest", "")
if dest == "" { if dest == "" {
return nil, fmt.Errorf("copy: dest required") return nil, mockError("moduleCopyWithClient", "copy: dest required")
} }
var content []byte var content []byte
var err error var err error
if src := getStringArg(args, "src", ""); src != "" { if src := getStringArg(args, "src", ""); src != "" {
content, err = os.ReadFile(src) content, err = readTestFile(src)
if err != nil { 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 != "" { } else if c := getStringArg(args, "content", ""); c != "" {
content = []byte(c) content = []byte(c)
} else { } 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 m := getStringArg(args, "mode", ""); m != "" {
if parsed, parseErr := strconv.ParseInt(m, 8, 32); parseErr == nil { 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 { if err != nil {
return nil, err return nil, err
} }
// Handle owner/group (best-effort, errors ignored) // Handle owner/group (best-effort, errors ignored)
if owner := getStringArg(args, "owner", ""); owner != "" { 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 != "" { 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) { func moduleTemplateWithClient(e *Executor, client sshFileRunner, args map[string]any, host string, task *Task) (*TaskResult, error) {
src := getStringArg(args, "src", "") src := getStringArg(args, "src", "")
dest := getStringArg(args, "dest", "") dest := getStringArg(args, "dest", "")
if src == "" || dest == "" { if src == "" || dest == "" {
return nil, fmt.Errorf("template: src and dest required") return nil, mockError("moduleTemplateWithClient", "template: src and dest required")
} }
// Process template // Process template
content, err := e.TemplateFile(src, host, task) content, err := e.TemplateFile(src, host, task)
if err != nil { 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 m := getStringArg(args, "mode", ""); m != "" {
if parsed, parseErr := strconv.ParseInt(m, 8, 32); parseErr == nil { 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 { if err != nil {
return nil, err 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) { 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", "") path = getStringArg(args, "dest", "")
} }
if path == "" { if path == "" {
return nil, fmt.Errorf("file: path required") return nil, mockError("moduleFileWithClient", "file: path required")
} }
state := getStringArg(args, "state", "file") state := getStringArg(args, "state", "file")
@ -633,21 +681,21 @@ func moduleFileWithClient(_ *Executor, client sshFileRunner, args map[string]any
switch state { switch state {
case "directory": case "directory":
mode := getStringArg(args, "mode", "0755") 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) stdout, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil || rc != 0 { if err != nil || rc != 0 {
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
} }
case "absent": case "absent":
cmd := fmt.Sprintf("rm -rf %q", path) cmd := sprintf("rm -rf %q", path)
_, stderr, rc, err := client.Run(context.Background(), cmd) _, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil || rc != 0 { if err != nil || rc != 0 {
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
} }
case "touch": case "touch":
cmd := fmt.Sprintf("touch %q", path) cmd := sprintf("touch %q", path)
_, stderr, rc, err := client.Run(context.Background(), cmd) _, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil || rc != 0 { if err != nil || rc != 0 {
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
@ -656,9 +704,9 @@ func moduleFileWithClient(_ *Executor, client sshFileRunner, args map[string]any
case "link": case "link":
src := getStringArg(args, "src", "") src := getStringArg(args, "src", "")
if 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) _, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil || rc != 0 { if err != nil || rc != 0 {
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
@ -667,20 +715,20 @@ func moduleFileWithClient(_ *Executor, client sshFileRunner, args map[string]any
case "file": case "file":
// Ensure file exists and set permissions // Ensure file exists and set permissions
if mode := getStringArg(args, "mode", ""); mode != "" { 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) // Handle owner/group (best-effort, errors ignored)
if owner := getStringArg(args, "owner", ""); owner != "" { 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 != "" { 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 recurse := getBoolArg(args, "recurse", false); recurse {
if owner := getStringArg(args, "owner", ""); owner != "" { 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", "") path = getStringArg(args, "dest", "")
} }
if path == "" { if path == "" {
return nil, fmt.Errorf("lineinfile: path required") return nil, mockError("moduleLineinfileWithClient", "lineinfile: path required")
} }
line := getStringArg(args, "line", "") line := getStringArg(args, "line", "")
@ -702,7 +750,7 @@ func moduleLineinfileWithClient(_ *Executor, client sshRunner, args map[string]a
if state == "absent" { if state == "absent" {
if regexpArg != "" { 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) _, stderr, rc, _ := client.Run(context.Background(), cmd)
if rc != 0 { if rc != 0 {
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
@ -712,17 +760,17 @@ func moduleLineinfileWithClient(_ *Executor, client sshRunner, args map[string]a
// state == present // state == present
if regexpArg != "" { if regexpArg != "" {
// Replace line matching regexp // Replace line matching regexp
escapedLine := strings.ReplaceAll(line, "/", "\\/") escapedLine := replaceAll(line, "/", "\\/")
cmd := fmt.Sprintf("sed -i 's/%s/%s/' %q", regexpArg, escapedLine, path) cmd := sprintf("sed -i 's/%s/%s/' %q", regexpArg, escapedLine, path)
_, _, rc, _ := client.Run(context.Background(), cmd) _, _, rc, _ := client.Run(context.Background(), cmd)
if rc != 0 { if rc != 0 {
// Line not found, append // Line not found, append
cmd = fmt.Sprintf("echo %q >> %q", line, path) cmd = sprintf("echo %q >> %q", line, path)
_, _, _, _ = client.Run(context.Background(), cmd) _, _, _, _ = client.Run(context.Background(), cmd)
} }
} else if line != "" { } else if line != "" {
// Ensure line is present // 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) _, _, _, _ = client.Run(context.Background(), cmd)
} }
} }
@ -736,7 +784,7 @@ func moduleBlockinfileWithClient(_ *Executor, client sshFileRunner, args map[str
path = getStringArg(args, "dest", "") path = getStringArg(args, "dest", "")
} }
if path == "" { if path == "" {
return nil, fmt.Errorf("blockinfile: path required") return nil, mockError("moduleBlockinfileWithClient", "blockinfile: path required")
} }
block := getStringArg(args, "block", "") block := getStringArg(args, "block", "")
@ -744,14 +792,14 @@ func moduleBlockinfileWithClient(_ *Executor, client sshFileRunner, args map[str
state := getStringArg(args, "state", "present") state := getStringArg(args, "state", "present")
create := getBoolArg(args, "create", false) create := getBoolArg(args, "create", false)
beginMarker := strings.Replace(marker, "{mark}", "BEGIN", 1) beginMarker := replaceN(marker, "{mark}", "BEGIN", 1)
endMarker := strings.Replace(marker, "{mark}", "END", 1) endMarker := replaceN(marker, "{mark}", "END", 1)
if state == "absent" { if state == "absent" {
// Remove block // Remove block
cmd := fmt.Sprintf("sed -i '/%s/,/%s/d' %q", cmd := sprintf("sed -i '/%s/,/%s/d' %q",
strings.ReplaceAll(beginMarker, "/", "\\/"), replaceAll(beginMarker, "/", "\\/"),
strings.ReplaceAll(endMarker, "/", "\\/"), replaceAll(endMarker, "/", "\\/"),
path) path)
_, _, _, _ = client.Run(context.Background(), cmd) _, _, _, _ = client.Run(context.Background(), cmd)
return &TaskResult{Changed: true}, nil return &TaskResult{Changed: true}, nil
@ -759,20 +807,20 @@ func moduleBlockinfileWithClient(_ *Executor, client sshFileRunner, args map[str
// Create file if needed (best-effort) // Create file if needed (best-effort)
if create { 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 // Remove existing block and add new one
escapedBlock := strings.ReplaceAll(block, "'", "'\\''") escapedBlock := replaceAll(block, "'", "'\\''")
cmd := fmt.Sprintf(` cmd := sprintf(`
sed -i '/%s/,/%s/d' %q 2>/dev/null || true sed -i '/%s/,/%s/d' %q 2>/dev/null || true
cat >> %q << 'BLOCK_EOF' cat >> %q << 'BLOCK_EOF'
%s %s
%s %s
%s %s
BLOCK_EOF BLOCK_EOF
`, strings.ReplaceAll(beginMarker, "/", "\\/"), `, replaceAll(beginMarker, "/", "\\/"),
strings.ReplaceAll(endMarker, "/", "\\/"), replaceAll(endMarker, "/", "\\/"),
path, path, beginMarker, escapedBlock, endMarker) path, path, beginMarker, escapedBlock, endMarker)
stdout, stderr, rc, err := client.RunScript(context.Background(), cmd) 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) { func moduleStatWithClient(_ *Executor, client sshFileRunner, args map[string]any) (*TaskResult, error) {
path := getStringArg(args, "path", "") path := getStringArg(args, "path", "")
if path == "" { if path == "" {
return nil, fmt.Errorf("stat: path required") return nil, mockError("moduleStatWithClient", "stat: path required")
} }
stat, err := client.Stat(context.Background(), path) stat, err := client.Stat(context.Background(), path)
@ -808,7 +856,7 @@ func moduleServiceWithClient(_ *Executor, client sshRunner, args map[string]any)
enabled := args["enabled"] enabled := args["enabled"]
if name == "" { if name == "" {
return nil, fmt.Errorf("service: name required") return nil, mockError("moduleServiceWithClient", "service: name required")
} }
var cmds []string var cmds []string
@ -816,21 +864,21 @@ func moduleServiceWithClient(_ *Executor, client sshRunner, args map[string]any)
if state != "" { if state != "" {
switch state { switch state {
case "started": case "started":
cmds = append(cmds, fmt.Sprintf("systemctl start %s", name)) cmds = append(cmds, sprintf("systemctl start %s", name))
case "stopped": case "stopped":
cmds = append(cmds, fmt.Sprintf("systemctl stop %s", name)) cmds = append(cmds, sprintf("systemctl stop %s", name))
case "restarted": case "restarted":
cmds = append(cmds, fmt.Sprintf("systemctl restart %s", name)) cmds = append(cmds, sprintf("systemctl restart %s", name))
case "reloaded": case "reloaded":
cmds = append(cmds, fmt.Sprintf("systemctl reload %s", name)) cmds = append(cmds, sprintf("systemctl reload %s", name))
} }
} }
if enabled != nil { if enabled != nil {
if getBoolArg(args, "enabled", false) { if getBoolArg(args, "enabled", false) {
cmds = append(cmds, fmt.Sprintf("systemctl enable %s", name)) cmds = append(cmds, sprintf("systemctl enable %s", name))
} else { } 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 { switch state {
case "present", "installed": case "present", "installed":
if name != "" { 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": 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": 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 == "" { if cmd == "" {
@ -895,20 +943,20 @@ func moduleAptKeyWithClient(_ *Executor, client sshRunner, args map[string]any)
if state == "absent" { if state == "absent" {
if keyring != "" { 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 return &TaskResult{Changed: true}, nil
} }
if url == "" { if url == "" {
return nil, fmt.Errorf("apt_key: url required") return nil, mockError("moduleAptKeyWithClient", "apt_key: url required")
} }
var cmd string var cmd string
if keyring != "" { 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 { } 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) 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") state := getStringArg(args, "state", "present")
if repo == "" { if repo == "" {
return nil, fmt.Errorf("apt_repository: repo required") return nil, mockError("moduleAptRepositoryWithClient", "apt_repository: repo required")
} }
if filename == "" { if filename == "" {
filename = strings.ReplaceAll(repo, " ", "-") filename = replaceAll(repo, " ", "-")
filename = strings.ReplaceAll(filename, "/", "-") filename = replaceAll(filename, "/", "-")
filename = strings.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" { 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 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) stdout, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil || rc != 0 { if err != nil || rc != 0 {
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil 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) { 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, _, _, _ := 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) return moduleAptWithClient(e, client, args)
} }
@ -973,11 +1021,11 @@ func modulePipWithClient(_ *Executor, client sshRunner, args map[string]any) (*T
var cmd string var cmd string
switch state { switch state {
case "present", "installed": case "present", "installed":
cmd = fmt.Sprintf("%s install %s", executable, name) cmd = sprintf("%s install %s", executable, name)
case "absent", "removed": case "absent", "removed":
cmd = fmt.Sprintf("%s uninstall -y %s", executable, name) cmd = sprintf("%s uninstall -y %s", executable, name)
case "latest": 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) 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") state := getStringArg(args, "state", "present")
if name == "" { if name == "" {
return nil, fmt.Errorf("user: name required") return nil, mockError("moduleUserWithClient", "user: name required")
} }
if state == "absent" { 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) _, _, _, _ = client.Run(context.Background(), cmd)
return &TaskResult{Changed: true}, nil return &TaskResult{Changed: true}, nil
} }
@ -1030,12 +1078,12 @@ func moduleUserWithClient(_ *Executor, client sshRunner, args map[string]any) (*
} }
// Try usermod first, then useradd // Try usermod first, then useradd
optsStr := strings.Join(opts, " ") optsStr := joinStrings(opts, " ")
var cmd string var cmd string
if optsStr == "" { 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 { } 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) name, optsStr, name, optsStr, name)
} }
@ -1052,11 +1100,11 @@ func moduleGroupWithClient(_ *Executor, client sshRunner, args map[string]any) (
state := getStringArg(args, "state", "present") state := getStringArg(args, "state", "present")
if name == "" { if name == "" {
return nil, fmt.Errorf("group: name required") return nil, mockError("moduleGroupWithClient", "group: name required")
} }
if state == "absent" { 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) _, _, _, _ = client.Run(context.Background(), cmd)
return &TaskResult{Changed: true}, nil return &TaskResult{Changed: true}, nil
} }
@ -1069,8 +1117,8 @@ func moduleGroupWithClient(_ *Executor, client sshRunner, args map[string]any) (
opts = append(opts, "-r") opts = append(opts, "-r")
} }
cmd := fmt.Sprintf("getent group %s >/dev/null 2>&1 || groupadd %s %s", cmd := sprintf("getent group %s >/dev/null 2>&1 || groupadd %s %s",
name, strings.Join(opts, " "), name) name, joinStrings(opts, " "), name)
stdout, stderr, rc, err := client.Run(context.Background(), cmd) stdout, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil || rc != 0 { if err != nil || rc != 0 {
@ -1097,7 +1145,7 @@ func moduleCronWithClient(_ *Executor, client sshRunner, args map[string]any) (*
if state == "absent" { if state == "absent" {
if name != "" { if name != "" {
// Remove by name (comment marker) // 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) user, name, job, user)
_, _, _, _ = client.Run(context.Background(), cmd) _, _, _, _ = client.Run(context.Background(), cmd)
} }
@ -1105,11 +1153,11 @@ func moduleCronWithClient(_ *Executor, client sshRunner, args map[string]any) (*
} }
// Build cron entry // Build cron entry
schedule := fmt.Sprintf("%s %s %s %s %s", minute, hour, day, month, weekday) schedule := sprintf("%s %s %s %s %s", minute, hour, day, month, weekday)
entry := fmt.Sprintf("%s %s # %s", schedule, job, name) entry := sprintf("%s %s # %s", schedule, job, name)
// Add to crontab // 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) user, name, entry, user)
stdout, stderr, rc, err := client.Run(context.Background(), cmd) stdout, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil || rc != 0 { if err != nil || rc != 0 {
@ -1127,15 +1175,15 @@ func moduleAuthorizedKeyWithClient(_ *Executor, client sshRunner, args map[strin
state := getStringArg(args, "state", "present") state := getStringArg(args, "state", "present")
if user == "" || key == "" { 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 // 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 { 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 == "" { if home == "" {
home = "/root" home = "/root"
if user != "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" { if state == "absent" {
// Remove key // Remove key
escapedKey := strings.ReplaceAll(key, "/", "\\/") escapedKey := replaceAll(key, "/", "\\/")
cmd := fmt.Sprintf("sed -i '/%s/d' %q 2>/dev/null || true", escapedKey[:40], authKeysPath) cmd := sprintf("sed -i '/%s/d' %q 2>/dev/null || true", escapedKey[:40], authKeysPath)
_, _, _, _ = client.Run(context.Background(), cmd) _, _, _, _ = client.Run(context.Background(), cmd)
return &TaskResult{Changed: true}, nil return &TaskResult{Changed: true}, nil
} }
// Ensure .ssh directory exists (best-effort) // Ensure .ssh directory exists (best-effort)
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("mkdir -p %q && chmod 700 %q && chown %s:%s %q", _, _, _, _ = client.Run(context.Background(), sprintf("mkdir -p %q && chmod 700 %q && chown %s:%s %q",
filepath.Dir(authKeysPath), filepath.Dir(authKeysPath), user, user, filepath.Dir(authKeysPath))) pathDir(authKeysPath), pathDir(authKeysPath), user, user, pathDir(authKeysPath)))
// Add key if not present // 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) key[:40], authKeysPath, key, authKeysPath)
stdout, stderr, rc, err := client.Run(context.Background(), cmd) stdout, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil || rc != 0 { if err != nil || rc != 0 {
@ -1166,7 +1214,7 @@ func moduleAuthorizedKeyWithClient(_ *Executor, client sshRunner, args map[strin
} }
// Fix permissions (best-effort) // 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)) authKeysPath, user, user, authKeysPath))
return &TaskResult{Changed: true}, nil return &TaskResult{Changed: true}, nil
@ -1180,7 +1228,7 @@ func moduleGitWithClient(_ *Executor, client sshFileRunner, args map[string]any)
version := getStringArg(args, "version", "HEAD") version := getStringArg(args, "version", "HEAD")
if repo == "" || dest == "" { 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 // Check if dest exists
@ -1188,9 +1236,9 @@ func moduleGitWithClient(_ *Executor, client sshFileRunner, args map[string]any)
var cmd string var cmd string
if exists { 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 { } 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) repo, dest, dest, version)
} }
@ -1210,41 +1258,41 @@ func moduleUnarchiveWithClient(_ *Executor, client sshFileRunner, args map[strin
remote := getBoolArg(args, "remote_src", false) remote := getBoolArg(args, "remote_src", false)
if src == "" || dest == "" { 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) // 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 var cmd string
if !remote { if !remote {
// Upload local file first // Upload local file first
content, err := os.ReadFile(src) content, err := readTestFile(src)
if err != nil { 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) tmpPath := "/tmp/ansible_unarchive_" + pathBase(src)
err = client.Upload(context.Background(), strings.NewReader(string(content)), tmpPath, 0644) err = client.Upload(context.Background(), newReader(string(content)), tmpPath, 0644)
if err != nil { if err != nil {
return nil, err return nil, err
} }
src = tmpPath 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 // Detect archive type and extract
if strings.HasSuffix(src, ".tar.gz") || strings.HasSuffix(src, ".tgz") { if hasSuffix(src, ".tar.gz") || hasSuffix(src, ".tgz") {
cmd = fmt.Sprintf("tar -xzf %q -C %q", src, dest) cmd = sprintf("tar -xzf %q -C %q", src, dest)
} else if strings.HasSuffix(src, ".tar.xz") { } else if hasSuffix(src, ".tar.xz") {
cmd = fmt.Sprintf("tar -xJf %q -C %q", src, dest) cmd = sprintf("tar -xJf %q -C %q", src, dest)
} else if strings.HasSuffix(src, ".tar.bz2") { } else if hasSuffix(src, ".tar.bz2") {
cmd = fmt.Sprintf("tar -xjf %q -C %q", src, dest) cmd = sprintf("tar -xjf %q -C %q", src, dest)
} else if strings.HasSuffix(src, ".tar") { } else if hasSuffix(src, ".tar") {
cmd = fmt.Sprintf("tar -xf %q -C %q", src, dest) cmd = sprintf("tar -xf %q -C %q", src, dest)
} else if strings.HasSuffix(src, ".zip") { } else if hasSuffix(src, ".zip") {
cmd = fmt.Sprintf("unzip -o %q -d %q", src, dest) cmd = sprintf("unzip -o %q -d %q", src, dest)
} else { } 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) 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") method := getStringArg(args, "method", "GET")
if url == "" { if url == "" {
return nil, fmt.Errorf("uri: url required") return nil, mockError("moduleURIWithClient", "uri: url required")
} }
var curlOpts []string var curlOpts []string
@ -1272,7 +1320,7 @@ func moduleURIWithClient(_ *Executor, client sshRunner, args map[string]any) (*T
// Headers // Headers
if headers, ok := args["headers"].(map[string]any); ok { if headers, ok := args["headers"].(map[string]any); ok {
for k, v := range headers { 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 // Status code
curlOpts = append(curlOpts, "-w", "\\n%{http_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) stdout, stderr, rc, err := client.Run(context.Background(), cmd)
if err != nil { if err != nil {
return &TaskResult{Failed: true, Msg: err.Error()}, nil return &TaskResult{Failed: true, Msg: err.Error()}, nil
} }
// Parse status code from last line // Parse status code from last line
lines := strings.Split(strings.TrimSpace(stdout), "\n") lines := split(trimSpace(stdout), "\n")
statusCode := 0 statusCode := 0
if len(lines) > 0 { if len(lines) > 0 {
statusCode, _ = strconv.Atoi(lines[len(lines)-1]) statusCode, _ = strconv.Atoi(lines[len(lines)-1])
@ -1350,13 +1398,13 @@ func moduleUFWWithClient(_ *Executor, client sshRunner, args map[string]any) (*T
if rule != "" && port != "" { if rule != "" && port != "" {
switch rule { switch rule {
case "allow": case "allow":
cmd = fmt.Sprintf("ufw allow %s/%s", port, proto) cmd = sprintf("ufw allow %s/%s", port, proto)
case "deny": case "deny":
cmd = fmt.Sprintf("ufw deny %s/%s", port, proto) cmd = sprintf("ufw deny %s/%s", port, proto)
case "reject": case "reject":
cmd = fmt.Sprintf("ufw reject %s/%s", port, proto) cmd = sprintf("ufw reject %s/%s", port, proto)
case "limit": 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) 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") state := getStringArg(args, "state", "present")
if projectSrc == "" { if projectSrc == "" {
return nil, fmt.Errorf("docker_compose: project_src required") return nil, mockError("moduleDockerComposeWithClient", "docker_compose: project_src required")
} }
var cmd string var cmd string
switch state { switch state {
case "present": case "present":
cmd = fmt.Sprintf("cd %q && docker compose up -d", projectSrc) cmd = sprintf("cd %q && docker compose up -d", projectSrc)
case "absent": case "absent":
cmd = fmt.Sprintf("cd %q && docker compose down", projectSrc) cmd = sprintf("cd %q && docker compose down", projectSrc)
case "restarted": case "restarted":
cmd = fmt.Sprintf("cd %q && docker compose restart", projectSrc) cmd = sprintf("cd %q && docker compose restart", projectSrc)
default: 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) stdout, stderr, rc, err := client.Run(context.Background(), cmd)
@ -1396,7 +1444,7 @@ func moduleDockerComposeWithClient(_ *Executor, client sshRunner, args map[strin
} }
// Heuristic for changed // 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 return &TaskResult{Changed: changed, Stdout: stdout}, nil
} }
@ -1408,7 +1456,7 @@ func (m *MockSSHClient) containsSubstring(sub string) bool {
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
for _, cmd := range m.executed { for _, cmd := range m.executed {
if strings.Contains(cmd.Cmd, sub) { if contains(cmd.Cmd, sub) {
return true return true
} }
} }

View file

@ -3,7 +3,7 @@ package ansible
import ( import (
"context" "context"
"encoding/base64" "encoding/base64"
"os" "io/fs"
"strconv" "strconv"
coreio "dappco.re/go/core/io" 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) 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 m := getStringArg(args, "mode", ""); m != "" {
if parsed, err := strconv.ParseInt(m, 8, 32); err == nil { 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) return nil, coreerr.E("Executor.moduleTemplate", "template", err)
} }
mode := os.FileMode(0644) mode := fs.FileMode(0644)
if m := getStringArg(args, "mode", ""); m != "" { if m := getStringArg(args, "mode", ""); m != "" {
if parsed, err := strconv.ParseInt(m, 8, 32); err == nil { if parsed, err := strconv.ParseInt(m, 8, 32); err == nil {
mode = os.FileMode(parsed) mode = fs.FileMode(parsed)
} }
} }

View file

@ -1,8 +1,6 @@
package ansible package ansible
import ( import (
"os"
"path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -17,7 +15,7 @@ import (
// --- user module --- // --- user module ---
func TestModuleUser_Good_CreateNewUser(t *testing.T) { func TestModulesAdv_ModuleUser_Good_CreateNewUser(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`id deploy >/dev/null 2>&1`, "", "no such user", 1) mock.expectCommand(`id deploy >/dev/null 2>&1`, "", "no such user", 1)
mock.expectCommand(`useradd`, "", "", 0) mock.expectCommand(`useradd`, "", "", 0)
@ -44,7 +42,7 @@ func TestModuleUser_Good_CreateNewUser(t *testing.T) {
assert.True(t, mock.containsSubstring("-m")) 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") e, mock := newTestExecutorWithMock("host1")
// id returns success meaning user exists, so usermod branch is taken // id returns success meaning user exists, so usermod branch is taken
mock.expectCommand(`id deploy >/dev/null 2>&1 && usermod`, "", "", 0) 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")) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`userdel -r deploy`, "", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`id|useradd`, "", "", 0) mock.expectCommand(`id|useradd`, "", "", 0)
@ -99,7 +97,7 @@ func TestModuleUser_Good_SystemUser(t *testing.T) {
assert.NotContains(t, cmd.Cmd, " -m ") 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 // When no options are provided, uses the simple "id || useradd" form
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`id testuser >/dev/null 2>&1 || useradd testuser`, "", "", 0) 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) assert.False(t, result.Failed)
} }
func TestModuleUser_Bad_MissingName(t *testing.T) { func TestModulesAdv_ModuleUser_Bad_MissingName(t *testing.T) {
e, _ := newTestExecutorWithMock("host1") e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient() mock := NewMockSSHClient()
@ -126,7 +124,7 @@ func TestModuleUser_Bad_MissingName(t *testing.T) {
assert.Contains(t, err.Error(), "name required") 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`id|useradd|usermod`, "", "useradd: Permission denied", 1) mock.expectCommand(`id|useradd|usermod`, "", "useradd: Permission denied", 1)
@ -142,7 +140,7 @@ func TestModuleUser_Good_CommandFailure(t *testing.T) {
// --- group module --- // --- group module ---
func TestModuleGroup_Good_CreateNewGroup(t *testing.T) { func TestModulesAdv_ModuleGroup_Good_CreateNewGroup(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
// getent fails → groupadd runs // getent fails → groupadd runs
mock.expectCommand(`getent group appgroup`, "", "", 1) mock.expectCommand(`getent group appgroup`, "", "", 1)
@ -159,7 +157,7 @@ func TestModuleGroup_Good_CreateNewGroup(t *testing.T) {
assert.True(t, mock.containsSubstring("appgroup")) 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") e, mock := newTestExecutorWithMock("host1")
// getent succeeds → groupadd skipped (|| short-circuits) // getent succeeds → groupadd skipped (|| short-circuits)
mock.expectCommand(`getent group docker >/dev/null 2>&1 || groupadd`, "", "", 0) 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) assert.False(t, result.Failed)
} }
func TestModuleGroup_Good_RemoveGroup(t *testing.T) { func TestModulesAdv_ModuleGroup_Good_RemoveGroup(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`groupdel oldgroup`, "", "", 0) mock.expectCommand(`groupdel oldgroup`, "", "", 0)
@ -187,7 +185,7 @@ func TestModuleGroup_Good_RemoveGroup(t *testing.T) {
assert.True(t, mock.hasExecuted(`groupdel oldgroup`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`getent group|groupadd`, "", "", 0) mock.expectCommand(`getent group|groupadd`, "", "", 0)
@ -202,7 +200,7 @@ func TestModuleGroup_Good_SystemGroup(t *testing.T) {
assert.True(t, mock.containsSubstring("-r")) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`getent group|groupadd`, "", "", 0) mock.expectCommand(`getent group|groupadd`, "", "", 0)
@ -217,7 +215,7 @@ func TestModuleGroup_Good_CustomGID(t *testing.T) {
assert.True(t, mock.containsSubstring("-g 5000")) 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") e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient() mock := NewMockSSHClient()
@ -229,7 +227,7 @@ func TestModuleGroup_Bad_MissingName(t *testing.T) {
assert.Contains(t, err.Error(), "name required") 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`getent group|groupadd`, "", "groupadd: Permission denied", 1) mock.expectCommand(`getent group|groupadd`, "", "groupadd: Permission denied", 1)
@ -243,7 +241,7 @@ func TestModuleGroup_Good_CommandFailure(t *testing.T) {
// --- cron module --- // --- cron module ---
func TestModuleCron_Good_AddCronJob(t *testing.T) { func TestModulesAdv_ModuleCron_Good_AddCronJob(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`crontab -u root`, "", "", 0) mock.expectCommand(`crontab -u root`, "", "", 0)
@ -261,7 +259,7 @@ func TestModuleCron_Good_AddCronJob(t *testing.T) {
assert.True(t, mock.containsSubstring("# backup")) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`crontab -u root -l`, "* * * * * /bin/backup # backup\n", "", 0) 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")) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`crontab -u root`, "", "", 0) 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")) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`crontab -u www-data`, "", "", 0) 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 * * *")) 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 // Absent with no name — changed but no grep command
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
@ -332,7 +330,7 @@ func TestModuleCron_Good_AbsentWithNoName(t *testing.T) {
// --- authorized_key module --- // --- authorized_key module ---
func TestModuleAuthorizedKey_Good_AddKey(t *testing.T) { func TestModulesAdv_ModuleAuthorizedKey_Good_AddKey(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... user@host" testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... user@host"
mock.expectCommand(`getent passwd deploy`, "/home/deploy", "", 0) 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")) 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") e, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... user@host" testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... user@host"
mock.expectCommand(`getent passwd deploy`, "/home/deploy", "", 0) 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")) 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") e, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... user@host" testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... user@host"
mock.expectCommand(`getent passwd deploy`, "/home/deploy", "", 0) mock.expectCommand(`getent passwd deploy`, "/home/deploy", "", 0)
@ -391,7 +389,7 @@ func TestModuleAuthorizedKey_Good_KeyAlreadyExists(t *testing.T) {
assert.False(t, result.Failed) assert.False(t, result.Failed)
} }
func TestModuleAuthorizedKey_Good_RootUserFallback(t *testing.T) { func TestModulesAdv_ModuleAuthorizedKey_Good_RootUserFallback(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... admin@host" testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... admin@host"
// getent returns empty — falls back to /root for root user // 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")) 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") e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient() mock := NewMockSSHClient()
@ -422,7 +420,7 @@ func TestModuleAuthorizedKey_Bad_MissingUserAndKey(t *testing.T) {
assert.Contains(t, err.Error(), "user and key required") 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") e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient() mock := NewMockSSHClient()
@ -434,7 +432,7 @@ func TestModuleAuthorizedKey_Bad_MissingKey(t *testing.T) {
assert.Contains(t, err.Error(), "user and key required") 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") e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient() mock := NewMockSSHClient()
@ -448,7 +446,7 @@ func TestModuleAuthorizedKey_Bad_MissingUser(t *testing.T) {
// --- git module --- // --- git module ---
func TestModuleGit_Good_FreshClone(t *testing.T) { func TestModulesAdv_ModuleGit_Good_FreshClone(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
// .git does not exist → fresh clone // .git does not exist → fresh clone
mock.expectCommand(`git clone`, "", "", 0) mock.expectCommand(`git clone`, "", "", 0)
@ -468,7 +466,7 @@ func TestModuleGit_Good_FreshClone(t *testing.T) {
assert.True(t, mock.containsSubstring("git checkout")) 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") e, mock := newTestExecutorWithMock("host1")
// .git exists → fetch + checkout // .git exists → fetch + checkout
mock.addFile("/opt/app/.git", []byte("gitdir")) 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")) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`git clone`, "", "", 0) mock.expectCommand(`git clone`, "", "", 0)
@ -504,7 +502,7 @@ func TestModuleGit_Good_CustomVersion(t *testing.T) {
assert.True(t, mock.containsSubstring("v2.1.0")) 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") e, mock := newTestExecutorWithMock("host1")
mock.addFile("/srv/myapp/.git", []byte("gitdir")) mock.addFile("/srv/myapp/.git", []byte("gitdir"))
mock.expectCommand(`git fetch --all && git checkout`, "", "", 0) 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")) assert.True(t, mock.containsSubstring("develop"))
} }
func TestModuleGit_Bad_MissingRepoAndDest(t *testing.T) { func TestModulesAdv_ModuleGit_Bad_MissingRepoAndDest(t *testing.T) {
e, _ := newTestExecutorWithMock("host1") e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient() mock := NewMockSSHClient()
@ -530,7 +528,7 @@ func TestModuleGit_Bad_MissingRepoAndDest(t *testing.T) {
assert.Contains(t, err.Error(), "repo and dest required") 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") e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient() mock := NewMockSSHClient()
@ -542,7 +540,7 @@ func TestModuleGit_Bad_MissingRepo(t *testing.T) {
assert.Contains(t, err.Error(), "repo and dest required") 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") e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient() mock := NewMockSSHClient()
@ -554,7 +552,7 @@ func TestModuleGit_Bad_MissingDest(t *testing.T) {
assert.Contains(t, err.Error(), "repo and dest required") 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`git clone`, "", "fatal: repository not found", 128) mock.expectCommand(`git clone`, "", "fatal: repository not found", 128)
@ -570,11 +568,11 @@ func TestModuleGit_Good_CloneFailure(t *testing.T) {
// --- unarchive module --- // --- unarchive module ---
func TestModuleUnarchive_Good_ExtractTarGzLocal(t *testing.T) { func TestModulesAdv_ModuleUnarchive_Good_ExtractTarGzLocal(t *testing.T) {
// Create a temporary "archive" file // Create a temporary "archive" file
tmpDir := t.TempDir() tmpDir := t.TempDir()
archivePath := filepath.Join(tmpDir, "package.tar.gz") archivePath := joinPath(tmpDir, "package.tar.gz")
require.NoError(t, os.WriteFile(archivePath, []byte("fake-archive-content"), 0644)) require.NoError(t, writeTestFile(archivePath, []byte("fake-archive-content"), 0644))
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`mkdir -p`, "", "", 0) mock.expectCommand(`mkdir -p`, "", "", 0)
@ -595,10 +593,10 @@ func TestModuleUnarchive_Good_ExtractTarGzLocal(t *testing.T) {
assert.True(t, mock.containsSubstring("/opt/app")) 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() tmpDir := t.TempDir()
archivePath := filepath.Join(tmpDir, "release.zip") archivePath := joinPath(tmpDir, "release.zip")
require.NoError(t, os.WriteFile(archivePath, []byte("fake-zip-content"), 0644)) require.NoError(t, writeTestFile(archivePath, []byte("fake-zip-content"), 0644))
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`mkdir -p`, "", "", 0) mock.expectCommand(`mkdir -p`, "", "", 0)
@ -617,7 +615,7 @@ func TestModuleUnarchive_Good_ExtractZipLocal(t *testing.T) {
assert.True(t, mock.containsSubstring("unzip -o")) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`mkdir -p`, "", "", 0) mock.expectCommand(`mkdir -p`, "", "", 0)
mock.expectCommand(`tar -xzf`, "", "", 0) mock.expectCommand(`tar -xzf`, "", "", 0)
@ -636,7 +634,7 @@ func TestModuleUnarchive_Good_RemoteSource(t *testing.T) {
assert.True(t, mock.containsSubstring("tar -xzf")) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`mkdir -p`, "", "", 0) mock.expectCommand(`mkdir -p`, "", "", 0)
mock.expectCommand(`tar -xJf`, "", "", 0) mock.expectCommand(`tar -xJf`, "", "", 0)
@ -652,7 +650,7 @@ func TestModuleUnarchive_Good_TarXz(t *testing.T) {
assert.True(t, mock.containsSubstring("tar -xJf")) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`mkdir -p`, "", "", 0) mock.expectCommand(`mkdir -p`, "", "", 0)
mock.expectCommand(`tar -xjf`, "", "", 0) mock.expectCommand(`tar -xjf`, "", "", 0)
@ -668,7 +666,7 @@ func TestModuleUnarchive_Good_TarBz2(t *testing.T) {
assert.True(t, mock.containsSubstring("tar -xjf")) 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") e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient() mock := NewMockSSHClient()
@ -678,7 +676,7 @@ func TestModuleUnarchive_Bad_MissingSrcAndDest(t *testing.T) {
assert.Contains(t, err.Error(), "src and dest required") 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") e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient() mock := NewMockSSHClient()
@ -690,7 +688,7 @@ func TestModuleUnarchive_Bad_MissingSrc(t *testing.T) {
assert.Contains(t, err.Error(), "src and dest required") 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") e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient() mock := NewMockSSHClient()
mock.expectCommand(`mkdir -p`, "", "", 0) mock.expectCommand(`mkdir -p`, "", "", 0)
@ -706,7 +704,7 @@ func TestModuleUnarchive_Bad_LocalFileNotFound(t *testing.T) {
// --- uri module --- // --- uri module ---
func TestModuleURI_Good_GetRequestDefault(t *testing.T) { func TestModulesAdv_ModuleURI_Good_GetRequestDefault(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl.*https://example.com/api/health`, "OK\n200", "", 0) 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"]) 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") e, mock := newTestExecutorWithMock("host1")
// Use a broad pattern since header order in map iteration is non-deterministic // Use a broad pattern since header order in map iteration is non-deterministic
mock.expectCommand(`curl.*api\.example\.com`, "{\"id\":1}\n201", "", 0) 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")) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl`, "Not Found\n404", "", 0) mock.expectCommand(`curl`, "Not Found\n404", "", 0)
@ -759,7 +757,7 @@ func TestModuleURI_Good_WrongStatusCode(t *testing.T) {
assert.Equal(t, 404, result.RC) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommandError(`curl`, assert.AnError) mock.expectCommandError(`curl`, assert.AnError)
@ -772,7 +770,7 @@ func TestModuleURI_Good_CurlCommandFailure(t *testing.T) {
assert.Contains(t, result.Msg, assert.AnError.Error()) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl`, "\n204", "", 0) mock.expectCommand(`curl`, "\n204", "", 0)
@ -787,7 +785,7 @@ func TestModuleURI_Good_CustomExpectedStatus(t *testing.T) {
assert.Equal(t, 204, result.RC) assert.Equal(t, 204, result.RC)
} }
func TestModuleURI_Bad_MissingURL(t *testing.T) { func TestModulesAdv_ModuleURI_Bad_MissingURL(t *testing.T) {
e, _ := newTestExecutorWithMock("host1") e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient() mock := NewMockSSHClient()
@ -801,7 +799,7 @@ func TestModuleURI_Bad_MissingURL(t *testing.T) {
// --- ufw module --- // --- ufw module ---
func TestModuleUFW_Good_AllowRuleWithPort(t *testing.T) { func TestModulesAdv_ModuleUFW_Good_AllowRuleWithPort(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw allow 443/tcp`, "Rule added", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw --force enable`, "Firewall is active", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw deny 53/udp`, "Rule added", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw --force reset`, "Resetting", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw disable`, "Firewall stopped", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw reload`, "Firewall reloaded", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw limit 22/tcp`, "Rule added", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw --force enable`, "", "ERROR: problem running ufw", 1) mock.expectCommand(`ufw --force enable`, "", "ERROR: problem running ufw", 1)
@ -917,7 +915,7 @@ func TestModuleUFW_Good_StateCommandFailure(t *testing.T) {
// --- docker_compose module --- // --- docker_compose module ---
func TestModuleDockerCompose_Good_StatePresent(t *testing.T) { func TestModulesAdv_ModuleDockerCompose_Good_StatePresent(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose up -d`, "Creating container_1\nCreating container_2\n", "", 0) 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")) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose down`, "Removing container_1\n", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose up -d`, "Container myapp-web-1 Up to date\n", "", 0) 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) assert.False(t, result.Failed)
} }
func TestModuleDockerCompose_Good_StateRestarted(t *testing.T) { func TestModulesAdv_ModuleDockerCompose_Good_StateRestarted(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose restart`, "Restarting container_1\n", "", 0) 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`)) 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") e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient() mock := NewMockSSHClient()
@ -989,7 +987,7 @@ func TestModuleDockerCompose_Bad_MissingProjectSrc(t *testing.T) {
assert.Contains(t, err.Error(), "project_src required") 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose up -d`, "", "Error response from daemon", 1) 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") 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" // When no state is specified, default is "present"
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose up -d`, "Starting\n", "", 0) 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 --- // --- 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`id|useradd|usermod`, "", "", 0) mock.expectCommand(`id|useradd|usermod`, "", "", 0)
@ -1038,7 +1036,7 @@ func TestExecuteModuleWithMock_Good_DispatchUser(t *testing.T) {
assert.True(t, result.Changed) assert.True(t, result.Changed)
} }
func TestExecuteModuleWithMock_Good_DispatchGroup(t *testing.T) { func TestModulesAdv_ExecuteModuleWithMock_Good_DispatchGroup(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`getent group|groupadd`, "", "", 0) mock.expectCommand(`getent group|groupadd`, "", "", 0)
@ -1055,7 +1053,7 @@ func TestExecuteModuleWithMock_Good_DispatchGroup(t *testing.T) {
assert.True(t, result.Changed) assert.True(t, result.Changed)
} }
func TestExecuteModuleWithMock_Good_DispatchCron(t *testing.T) { func TestModulesAdv_ExecuteModuleWithMock_Good_DispatchCron(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`crontab`, "", "", 0) mock.expectCommand(`crontab`, "", "", 0)
@ -1073,7 +1071,7 @@ func TestExecuteModuleWithMock_Good_DispatchCron(t *testing.T) {
assert.True(t, result.Changed) assert.True(t, result.Changed)
} }
func TestExecuteModuleWithMock_Good_DispatchGit(t *testing.T) { func TestModulesAdv_ExecuteModuleWithMock_Good_DispatchGit(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`git clone`, "", "", 0) mock.expectCommand(`git clone`, "", "", 0)
@ -1091,7 +1089,7 @@ func TestExecuteModuleWithMock_Good_DispatchGit(t *testing.T) {
assert.True(t, result.Changed) assert.True(t, result.Changed)
} }
func TestExecuteModuleWithMock_Good_DispatchURI(t *testing.T) { func TestModulesAdv_ExecuteModuleWithMock_Good_DispatchURI(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl`, "OK\n200", "", 0) mock.expectCommand(`curl`, "OK\n200", "", 0)
@ -1108,7 +1106,7 @@ func TestExecuteModuleWithMock_Good_DispatchURI(t *testing.T) {
assert.False(t, result.Failed) assert.False(t, result.Failed)
} }
func TestExecuteModuleWithMock_Good_DispatchDockerCompose(t *testing.T) { func TestModulesAdv_ExecuteModuleWithMock_Good_DispatchDockerCompose(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose up -d`, "Creating\n", "", 0) mock.expectCommand(`docker compose up -d`, "Creating\n", "", 0)

View file

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

View file

@ -1,8 +1,7 @@
package ansible package ansible
import ( import (
"os" "io/fs"
"path/filepath"
"testing" "testing"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
@ -15,7 +14,7 @@ import (
// --- copy module --- // --- copy module ---
func TestModuleCopy_Good_ContentUpload(t *testing.T) { func TestModulesFile_ModuleCopy_Good_ContentUpload(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
result, err := moduleCopyWithClient(e, mock, map[string]any{ result, err := moduleCopyWithClient(e, mock, map[string]any{
@ -33,13 +32,13 @@ func TestModuleCopy_Good_ContentUpload(t *testing.T) {
require.NotNil(t, up) require.NotNil(t, up)
assert.Equal(t, "/etc/app/config", up.Remote) assert.Equal(t, "/etc/app/config", up.Remote)
assert.Equal(t, []byte("server_name=web01"), up.Content) 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() tmpDir := t.TempDir()
srcPath := filepath.Join(tmpDir, "nginx.conf") srcPath := joinPath(tmpDir, "nginx.conf")
require.NoError(t, os.WriteFile(srcPath, []byte("worker_processes auto;"), 0644)) require.NoError(t, writeTestFile(srcPath, []byte("worker_processes auto;"), 0644))
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
@ -57,7 +56,7 @@ func TestModuleCopy_Good_SrcFile(t *testing.T) {
assert.Equal(t, []byte("worker_processes auto;"), up.Content) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleCopyWithClient(e, mock, map[string]any{ 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"`)) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleCopyWithClient(e, mock, map[string]any{ result, err := moduleCopyWithClient(e, mock, map[string]any{
@ -90,10 +89,10 @@ func TestModuleCopy_Good_CustomMode(t *testing.T) {
up := mock.lastUpload() up := mock.lastUpload()
require.NotNil(t, up) 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") e, mock := newTestExecutorWithMock("host1")
_, err := moduleCopyWithClient(e, mock, map[string]any{ _, 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") 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") e, mock := newTestExecutorWithMock("host1")
_, err := moduleCopyWithClient(e, mock, map[string]any{ _, 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") 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") e, mock := newTestExecutorWithMock("host1")
_, err := moduleCopyWithClient(e, mock, map[string]any{ _, 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") 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 // When both content and src are given, src is checked first in the implementation
// but if src is empty string, content is used // but if src is empty string, content is used
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
@ -145,7 +144,7 @@ func TestModuleCopy_Good_ContentTakesPrecedenceOverSrc(t *testing.T) {
// --- file module --- // --- file module ---
func TestModuleFile_Good_StateDirectory(t *testing.T) { func TestModulesFile_ModuleFile_Good_StateDirectory(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{ 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`)) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{ 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"`)) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{ 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"`)) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{ 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"`)) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{ 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"`)) 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") e, mock := newTestExecutorWithMock("host1")
_, err := moduleFileWithClient(e, mock, map[string]any{ _, 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") 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{ 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"`)) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{ 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"`)) 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") e, mock := newTestExecutorWithMock("host1")
_, err := moduleFileWithClient(e, mock, map[string]any{ _, 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") 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{ 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"`)) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleFileWithClient(e, mock, map[string]any{ 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"`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("mkdir", "", "permission denied", 1) mock.expectCommand("mkdir", "", "permission denied", 1)
@ -319,7 +318,7 @@ func TestModuleFile_Good_DirectoryCommandFailure(t *testing.T) {
// --- lineinfile module --- // --- lineinfile module ---
func TestModuleLineinfile_Good_InsertLine(t *testing.T) { func TestModulesFile_ModuleLineinfile_Good_InsertLine(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
result, err := moduleLineinfileWithClient(e, mock, map[string]any{ 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`)) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleLineinfileWithClient(e, mock, map[string]any{ 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/'`)) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleLineinfileWithClient(e, mock, map[string]any{ 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'`)) 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") e, mock := newTestExecutorWithMock("host1")
// Simulate sed returning non-zero (pattern not found) // Simulate sed returning non-zero (pattern not found)
mock.expectCommand("sed -i", "", "", 1) mock.expectCommand("sed -i", "", "", 1)
@ -388,7 +387,7 @@ func TestModuleLineinfile_Good_RegexpFallsBackToAppend(t *testing.T) {
assert.True(t, mock.hasExecuted(`echo`)) 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") e, mock := newTestExecutorWithMock("host1")
_, err := moduleLineinfileWithClient(e, mock, map[string]any{ _, 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") 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleLineinfileWithClient(e, mock, map[string]any{ 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`)) 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) // When state=absent but no regexp, nothing happens (no commands)
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
@ -426,7 +425,7 @@ func TestModuleLineinfile_Good_AbsentWithNoRegexp(t *testing.T) {
assert.Equal(t, 0, mock.commandCount()) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleLineinfileWithClient(e, mock, map[string]any{ result, err := moduleLineinfileWithClient(e, mock, map[string]any{
@ -444,7 +443,7 @@ func TestModuleLineinfile_Good_LineWithSlashes(t *testing.T) {
// --- blockinfile module --- // --- blockinfile module ---
func TestModuleBlockinfile_Good_InsertBlock(t *testing.T) { func TestModulesFile_ModuleBlockinfile_Good_InsertBlock(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
result, err := moduleBlockinfileWithClient(e, mock, map[string]any{ 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")) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleBlockinfileWithClient(e, mock, map[string]any{ 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")) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleBlockinfileWithClient(e, mock, map[string]any{ 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'`)) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleBlockinfileWithClient(e, mock, map[string]any{ 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"`)) 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") e, mock := newTestExecutorWithMock("host1")
_, err := moduleBlockinfileWithClient(e, mock, map[string]any{ _, 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") 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleBlockinfileWithClient(e, mock, map[string]any{ result, err := moduleBlockinfileWithClient(e, mock, map[string]any{
@ -532,7 +531,7 @@ func TestModuleBlockinfile_Good_DestAliasForPath(t *testing.T) {
assert.True(t, result.Changed) assert.True(t, result.Changed)
} }
func TestModuleBlockinfile_Good_ScriptFailure(t *testing.T) { func TestModulesFile_ModuleBlockinfile_Good_ScriptFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand("BLOCK_EOF", "", "write error", 1) mock.expectCommand("BLOCK_EOF", "", "write error", 1)
@ -548,7 +547,7 @@ func TestModuleBlockinfile_Good_ScriptFailure(t *testing.T) {
// --- stat module --- // --- stat module ---
func TestModuleStat_Good_ExistingFile(t *testing.T) { func TestModulesFile_ModuleStat_Good_ExistingFile(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.addStat("/etc/nginx/nginx.conf", map[string]any{ mock.addStat("/etc/nginx/nginx.conf", map[string]any{
"exists": true, "exists": true,
@ -575,7 +574,7 @@ func TestModuleStat_Good_ExistingFile(t *testing.T) {
assert.Equal(t, 1234, stat["size"]) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleStatWithClient(e, mock, map[string]any{ 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"]) 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") e, mock := newTestExecutorWithMock("host1")
mock.addStat("/var/log", map[string]any{ mock.addStat("/var/log", map[string]any{
"exists": true, "exists": true,
@ -609,7 +608,7 @@ func TestModuleStat_Good_Directory(t *testing.T) {
assert.Equal(t, true, stat["isdir"]) 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") e, mock := newTestExecutorWithMock("host1")
// No explicit stat, but add a file — stat falls back to file existence // No explicit stat, but add a file — stat falls back to file existence
mock.addFile("/etc/hosts", []byte("127.0.0.1 localhost")) 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"]) 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") e, mock := newTestExecutorWithMock("host1")
_, err := moduleStatWithClient(e, mock, map[string]any{}) _, err := moduleStatWithClient(e, mock, map[string]any{})
@ -635,10 +634,10 @@ func TestModuleStat_Bad_MissingPath(t *testing.T) {
// --- template module --- // --- template module ---
func TestModuleTemplate_Good_BasicTemplate(t *testing.T) { func TestModulesFile_ModuleTemplate_Good_BasicTemplate(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
srcPath := filepath.Join(tmpDir, "app.conf.j2") srcPath := joinPath(tmpDir, "app.conf.j2")
require.NoError(t, os.WriteFile(srcPath, []byte("server_name={{ server_name }};"), 0644)) require.NoError(t, writeTestFile(srcPath, []byte("server_name={{ server_name }};"), 0644))
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
e.SetVar("server_name", "web01.example.com") 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") 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() tmpDir := t.TempDir()
srcPath := filepath.Join(tmpDir, "script.sh.j2") srcPath := joinPath(tmpDir, "script.sh.j2")
require.NoError(t, os.WriteFile(srcPath, []byte("#!/bin/bash\necho done"), 0644)) require.NoError(t, writeTestFile(srcPath, []byte("#!/bin/bash\necho done"), 0644))
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
@ -679,10 +678,10 @@ func TestModuleTemplate_Good_CustomMode(t *testing.T) {
up := mock.lastUpload() up := mock.lastUpload()
require.NotNil(t, up) 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") e, mock := newTestExecutorWithMock("host1")
_, err := moduleTemplateWithClient(e, mock, map[string]any{ _, 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") 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") e, mock := newTestExecutorWithMock("host1")
_, err := moduleTemplateWithClient(e, mock, map[string]any{ _, 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") 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") e, mock := newTestExecutorWithMock("host1")
_, err := moduleTemplateWithClient(e, mock, map[string]any{ _, err := moduleTemplateWithClient(e, mock, map[string]any{
@ -716,11 +715,11 @@ func TestModuleTemplate_Bad_SrcFileNotFound(t *testing.T) {
assert.Contains(t, err.Error(), "template") assert.Contains(t, err.Error(), "template")
} }
func TestModuleTemplate_Good_PlainTextNoVars(t *testing.T) { func TestModulesFile_ModuleTemplate_Good_PlainTextNoVars(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
srcPath := filepath.Join(tmpDir, "static.conf") srcPath := joinPath(tmpDir, "static.conf")
content := "listen 80;\nserver_name localhost;" 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") e, mock := newTestExecutorWithMock("host1")
@ -739,7 +738,7 @@ func TestModuleTemplate_Good_PlainTextNoVars(t *testing.T) {
// --- Cross-module dispatch tests for file modules --- // --- 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") e, mock := newTestExecutorWithMock("host1")
task := &Task{ task := &Task{
@ -757,7 +756,7 @@ func TestExecuteModuleWithMock_Good_DispatchCopy(t *testing.T) {
assert.Equal(t, 1, mock.uploadCount()) 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") e, mock := newTestExecutorWithMock("host1")
task := &Task{ task := &Task{
@ -775,7 +774,7 @@ func TestExecuteModuleWithMock_Good_DispatchFile(t *testing.T) {
assert.True(t, mock.hasExecuted("mkdir")) 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") e, mock := newTestExecutorWithMock("host1")
mock.addStat("/etc/hosts", map[string]any{"exists": true, "isdir": false}) 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"]) 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") e, mock := newTestExecutorWithMock("host1")
task := &Task{ task := &Task{
@ -811,7 +810,7 @@ func TestExecuteModuleWithMock_Good_DispatchLineinfile(t *testing.T) {
assert.True(t, result.Changed) assert.True(t, result.Changed)
} }
func TestExecuteModuleWithMock_Good_DispatchBlockinfile(t *testing.T) { func TestModulesFile_ExecuteModuleWithMock_Good_DispatchBlockinfile(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
task := &Task{ task := &Task{
@ -828,10 +827,10 @@ func TestExecuteModuleWithMock_Good_DispatchBlockinfile(t *testing.T) {
assert.True(t, result.Changed) assert.True(t, result.Changed)
} }
func TestExecuteModuleWithMock_Good_DispatchTemplate(t *testing.T) { func TestModulesFile_ExecuteModuleWithMock_Good_DispatchTemplate(t *testing.T) {
tmpDir := t.TempDir() tmpDir := t.TempDir()
srcPath := filepath.Join(tmpDir, "test.j2") srcPath := joinPath(tmpDir, "test.j2")
require.NoError(t, os.WriteFile(srcPath, []byte("static content"), 0644)) require.NoError(t, writeTestFile(srcPath, []byte("static content"), 0644))
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
@ -852,7 +851,7 @@ func TestExecuteModuleWithMock_Good_DispatchTemplate(t *testing.T) {
// --- Template variable resolution integration --- // --- Template variable resolution integration ---
func TestModuleCopy_Good_TemplatedArgs(t *testing.T) { func TestModulesFile_ModuleCopy_Good_TemplatedArgs(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
e.SetVar("deploy_path", "/opt/myapp") 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) 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, mock := newTestExecutorWithMock("host1")
e.SetVar("app_dir", "/var/www/html") e.SetVar("app_dir", "/var/www/html")

View file

@ -11,7 +11,7 @@ import (
// 1. Error Propagation — getHosts // 1. Error Propagation — getHosts
// =========================================================================== // ===========================================================================
func TestGetHosts_Infra_Good_AllPattern(t *testing.T) { func TestModulesInfra_GetHosts_Good_AllPattern(t *testing.T) {
e := NewExecutor("/tmp") e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{ e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{ All: &InventoryGroup{
@ -30,7 +30,7 @@ func TestGetHosts_Infra_Good_AllPattern(t *testing.T) {
assert.Contains(t, hosts, "db1") 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 := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{ e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{ All: &InventoryGroup{
@ -45,7 +45,7 @@ func TestGetHosts_Infra_Good_SpecificHost(t *testing.T) {
assert.Equal(t, []string{"web1"}, hosts) 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 := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{ e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{ All: &InventoryGroup{
@ -71,21 +71,21 @@ func TestGetHosts_Infra_Good_GroupName(t *testing.T) {
assert.Contains(t, hosts, "web2") assert.Contains(t, hosts, "web2")
} }
func TestGetHosts_Infra_Good_Localhost(t *testing.T) { func TestModulesInfra_GetHosts_Good_Localhost(t *testing.T) {
e := NewExecutor("/tmp") e := NewExecutor("/tmp")
// No inventory at all // No inventory at all
hosts := e.getHosts("localhost") hosts := e.getHosts("localhost")
assert.Equal(t, []string{"localhost"}, hosts) 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") e := NewExecutor("/tmp")
// inventory is nil, non-localhost pattern // inventory is nil, non-localhost pattern
hosts := e.getHosts("webservers") hosts := e.getHosts("webservers")
assert.Nil(t, hosts) assert.Nil(t, hosts)
} }
func TestGetHosts_Infra_Bad_NonexistentHost(t *testing.T) { func TestModulesInfra_GetHosts_Bad_NonexistentHost(t *testing.T) {
e := NewExecutor("/tmp") e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{ e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{ All: &InventoryGroup{
@ -99,7 +99,7 @@ func TestGetHosts_Infra_Bad_NonexistentHost(t *testing.T) {
assert.Empty(t, hosts) assert.Empty(t, hosts)
} }
func TestGetHosts_Infra_Good_LimitFiltering(t *testing.T) { func TestModulesInfra_GetHosts_Good_LimitFiltering(t *testing.T) {
e := NewExecutor("/tmp") e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{ e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{ All: &InventoryGroup{
@ -117,13 +117,13 @@ func TestGetHosts_Infra_Good_LimitFiltering(t *testing.T) {
assert.Contains(t, hosts, "web1") 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 := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{ e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{ All: &InventoryGroup{
Hosts: map[string]*Host{ Hosts: map[string]*Host{
"prod-web-01": {}, "prod-web-01": {},
"prod-web-02": {}, "prod-web-02": {},
"staging-web-01": {}, "staging-web-01": {},
}, },
}, },
@ -139,18 +139,18 @@ func TestGetHosts_Infra_Good_LimitSubstringMatch(t *testing.T) {
// 1. Error Propagation — matchesTags // 1. Error Propagation — matchesTags
// =========================================================================== // ===========================================================================
func TestMatchesTags_Infra_Good_NoFiltersNoTags(t *testing.T) { func TestModulesInfra_MatchesTags_Good_NoFiltersNoTags(t *testing.T) {
e := NewExecutor("/tmp") e := NewExecutor("/tmp")
// No Tags, no SkipTags set // No Tags, no SkipTags set
assert.True(t, e.matchesTags(nil)) 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") e := NewExecutor("/tmp")
assert.True(t, e.matchesTags([]string{"deploy", "config"})) 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 := NewExecutor("/tmp")
e.Tags = []string{"deploy"} 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"})) 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 := NewExecutor("/tmp")
e.Tags = []string{"deploy"} e.Tags = []string{"deploy"}
assert.False(t, e.matchesTags([]string{"build", "test"})) 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 := NewExecutor("/tmp")
e.SkipTags = []string{"slow"} e.SkipTags = []string{"slow"}
@ -174,7 +174,7 @@ func TestMatchesTags_Infra_Good_SkipOverridesInclude(t *testing.T) {
assert.True(t, e.matchesTags([]string{"deploy", "fast"})) 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 := NewExecutor("/tmp")
e.Tags = []string{"deploy"} e.Tags = []string{"deploy"}
@ -183,7 +183,7 @@ func TestMatchesTags_Infra_Bad_IncludeFilterNoTaskTags(t *testing.T) {
assert.False(t, e.matchesTags([]string{})) 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 := NewExecutor("/tmp")
e.Tags = []string{"all"} e.Tags = []string{"all"}
@ -195,7 +195,7 @@ func TestMatchesTags_Infra_Good_AllTagMatchesEverything(t *testing.T) {
// 1. Error Propagation — evaluateWhen // 1. Error Propagation — evaluateWhen
// =========================================================================== // ===========================================================================
func TestEvaluateWhen_Infra_Good_DefinedCheck(t *testing.T) { func TestModulesInfra_EvaluateWhen_Good_DefinedCheck(t *testing.T) {
e := NewExecutor("/tmp") e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{ e.results["host1"] = map[string]*TaskResult{
"myresult": {Changed: true}, "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)) 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") e := NewExecutor("/tmp")
// No results registered for host1 // No results registered for host1
assert.True(t, e.evaluateWhen("missing_var is not defined", "host1", nil)) 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") e := NewExecutor("/tmp")
assert.True(t, e.evaluateWhen("some_var is undefined", "host1", nil)) 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 := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{ e.results["host1"] = map[string]*TaskResult{
"result": {Failed: false, Changed: true}, "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)) 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 := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{ e.results["host1"] = map[string]*TaskResult{
"result": {Failed: true}, "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)) 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 := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{ e.results["host1"] = map[string]*TaskResult{
"result": {Changed: true}, "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)) 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 := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{ e.results["host1"] = map[string]*TaskResult{
"result": {Skipped: true}, "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)) 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 := NewExecutor("/tmp")
e.vars["my_flag"] = true e.vars["my_flag"] = true
assert.True(t, e.evalCondition("my_flag", "host1")) 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 := NewExecutor("/tmp")
e.vars["my_flag"] = false e.vars["my_flag"] = false
assert.False(t, e.evalCondition("my_flag", "host1")) 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 := NewExecutor("/tmp")
e.vars["my_str"] = "hello" e.vars["my_str"] = "hello"
assert.True(t, e.evalCondition("my_str", "host1")) 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 := NewExecutor("/tmp")
e.vars["my_str"] = "" e.vars["my_str"] = ""
assert.False(t, e.evalCondition("my_str", "host1")) 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 := NewExecutor("/tmp")
e.vars["my_str"] = "false" e.vars["my_str"] = "false"
assert.False(t, e.evalCondition("my_str", "host1")) 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")) 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 := NewExecutor("/tmp")
e.vars["count"] = 42 e.vars["count"] = 42
assert.True(t, e.evalCondition("count", "host1")) 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 := NewExecutor("/tmp")
e.vars["count"] = 0 e.vars["count"] = 0
assert.False(t, e.evalCondition("count", "host1")) 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") e := NewExecutor("/tmp")
assert.False(t, e.evalCondition("not true", "host1")) assert.False(t, e.evalCondition("not true", "host1"))
assert.True(t, e.evalCondition("not false", "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 := NewExecutor("/tmp")
e.vars["enabled"] = true e.vars["enabled"] = true
e.results["host1"] = map[string]*TaskResult{ 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)) 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 := NewExecutor("/tmp")
e.vars["enabled"] = true 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)) 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") e := NewExecutor("/tmp")
// Condition with default filter should be satisfied // Condition with default filter should be satisfied
assert.True(t, e.evalCondition("my_var | default(true)", "host1")) 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 := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{ e.results["host1"] = map[string]*TaskResult{
"check_result": {Failed: false, Skipped: false}, "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")) 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 := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{ e.results["host1"] = map[string]*TaskResult{
"check_result": {Failed: true}, "check_result": {Failed: true},
@ -354,7 +354,7 @@ func TestEvaluateWhen_Infra_Bad_RegisteredVarFailedFalsy(t *testing.T) {
// 1. Error Propagation — templateString // 1. Error Propagation — templateString
// =========================================================================== // ===========================================================================
func TestTemplateString_Infra_Good_SimpleSubstitution(t *testing.T) { func TestModulesInfra_TemplateString_Good_SimpleSubstitution(t *testing.T) {
e := NewExecutor("/tmp") e := NewExecutor("/tmp")
e.vars["app_name"] = "myapp" e.vars["app_name"] = "myapp"
@ -362,7 +362,7 @@ func TestTemplateString_Infra_Good_SimpleSubstitution(t *testing.T) {
assert.Equal(t, "Deploying myapp", result) 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 := NewExecutor("/tmp")
e.vars["host"] = "db.example.com" e.vars["host"] = "db.example.com"
e.vars["port"] = 5432 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) 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") e := NewExecutor("/tmp")
result := e.templateString("{{ missing_var }}", "", nil) result := e.templateString("{{ missing_var }}", "", nil)
assert.Equal(t, "{{ missing_var }}", result) 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") e := NewExecutor("/tmp")
result := e.templateString("just a plain string", "", nil) result := e.templateString("just a plain string", "", nil)
assert.Equal(t, "just a plain string", result) 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 := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{ e.results["host1"] = map[string]*TaskResult{
"cmd_result": {Stdout: "42"}, "cmd_result": {Stdout: "42"},
@ -393,7 +393,7 @@ func TestTemplateString_Infra_Good_RegisteredVarStdout(t *testing.T) {
assert.Equal(t, "42", result) 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 := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{ e.results["host1"] = map[string]*TaskResult{
"cmd_result": {RC: 0}, "cmd_result": {RC: 0},
@ -403,7 +403,7 @@ func TestTemplateString_Infra_Good_RegisteredVarRC(t *testing.T) {
assert.Equal(t, "0", result) 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 := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{ e.results["host1"] = map[string]*TaskResult{
"cmd_result": {Changed: true}, "cmd_result": {Changed: true},
@ -413,7 +413,7 @@ func TestTemplateString_Infra_Good_RegisteredVarChanged(t *testing.T) {
assert.Equal(t, "true", result) 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 := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{ e.results["host1"] = map[string]*TaskResult{
"cmd_result": {Failed: true}, "cmd_result": {Failed: true},
@ -423,7 +423,7 @@ func TestTemplateString_Infra_Good_RegisteredVarFailed(t *testing.T) {
assert.Equal(t, "true", result) assert.Equal(t, "true", result)
} }
func TestTemplateString_Infra_Good_TaskVars(t *testing.T) { func TestModulesInfra_TemplateString_Good_TaskVars(t *testing.T) {
e := NewExecutor("/tmp") e := NewExecutor("/tmp")
task := &Task{ task := &Task{
Vars: map[string]any{ Vars: map[string]any{
@ -435,7 +435,7 @@ func TestTemplateString_Infra_Good_TaskVars(t *testing.T) {
assert.Equal(t, "task_value", result) 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 := NewExecutor("/tmp")
e.facts["host1"] = &Facts{ e.facts["host1"] = &Facts{
Hostname: "web1", Hostname: "web1",
@ -458,24 +458,24 @@ func TestTemplateString_Infra_Good_FactsResolution(t *testing.T) {
// 1. Error Propagation — applyFilter // 1. Error Propagation — applyFilter
// =========================================================================== // ===========================================================================
func TestApplyFilter_Infra_Good_DefaultWithValue(t *testing.T) { func TestModulesInfra_ApplyFilter_Good_DefaultWithValue(t *testing.T) {
e := NewExecutor("/tmp") e := NewExecutor("/tmp")
// When value is non-empty, default is not applied // When value is non-empty, default is not applied
assert.Equal(t, "hello", e.applyFilter("hello", "default('fallback')")) 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") e := NewExecutor("/tmp")
// When value is empty, default IS applied // When value is empty, default IS applied
assert.Equal(t, "fallback", e.applyFilter("", "default('fallback')")) 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") e := NewExecutor("/tmp")
assert.Equal(t, "fallback", e.applyFilter("", `default("fallback")`)) 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") e := NewExecutor("/tmp")
assert.Equal(t, "true", e.applyFilter("true", "bool")) assert.Equal(t, "true", e.applyFilter("true", "bool"))
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")) 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") e := NewExecutor("/tmp")
assert.Equal(t, "false", e.applyFilter("false", "bool")) assert.Equal(t, "false", e.applyFilter("false", "bool"))
assert.Equal(t, "false", e.applyFilter("no", "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")) 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") e := NewExecutor("/tmp")
assert.Equal(t, "hello", e.applyFilter(" hello ", "trim")) assert.Equal(t, "hello", e.applyFilter(" hello ", "trim"))
assert.Equal(t, "no spaces", e.applyFilter("no spaces", "trim")) assert.Equal(t, "no spaces", e.applyFilter("no spaces", "trim"))
assert.Equal(t, "", e.applyFilter(" ", "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") e := NewExecutor("/tmp")
// b64decode currently returns value unchanged (placeholder) // b64decode currently returns value unchanged (placeholder)
assert.Equal(t, "dGVzdA==", e.applyFilter("dGVzdA==", "b64decode")) 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") e := NewExecutor("/tmp")
// Unknown filters return value unchanged // Unknown filters return value unchanged
assert.Equal(t, "hello", e.applyFilter("hello", "nonexistent_filter")) 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") e := NewExecutor("/tmp")
// When a var is defined, the filter passes through // When a var is defined, the filter passes through
e.vars["defined_var"] = "hello" e.vars["defined_var"] = "hello"
@ -519,7 +519,7 @@ func TestTemplateString_Infra_Good_FilterInTemplate(t *testing.T) {
assert.Equal(t, "hello", result) 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 := NewExecutor("/tmp")
e.vars["empty_var"] = "" e.vars["empty_var"] = ""
// When var is empty string, default filter applies // 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) 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 := NewExecutor("/tmp")
e.vars["flag"] = "yes" e.vars["flag"] = "yes"
@ -535,7 +535,7 @@ func TestTemplateString_Infra_Good_BoolFilterInTemplate(t *testing.T) {
assert.Equal(t, "true", result) 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 := NewExecutor("/tmp")
e.vars["padded"] = " trimmed " e.vars["padded"] = " trimmed "
@ -547,7 +547,7 @@ func TestTemplateString_Infra_Good_TrimFilterInTemplate(t *testing.T) {
// 1. Error Propagation — resolveLoop // 1. Error Propagation — resolveLoop
// =========================================================================== // ===========================================================================
func TestResolveLoop_Infra_Good_SliceAny(t *testing.T) { func TestModulesInfra_ResolveLoop_Good_SliceAny(t *testing.T) {
e := NewExecutor("/tmp") e := NewExecutor("/tmp")
items := e.resolveLoop([]any{"a", "b", "c"}, "host1") items := e.resolveLoop([]any{"a", "b", "c"}, "host1")
assert.Len(t, items, 3) assert.Len(t, items, 3)
@ -556,7 +556,7 @@ func TestResolveLoop_Infra_Good_SliceAny(t *testing.T) {
assert.Equal(t, "c", items[2]) 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") e := NewExecutor("/tmp")
items := e.resolveLoop([]string{"x", "y"}, "host1") items := e.resolveLoop([]string{"x", "y"}, "host1")
assert.Len(t, items, 2) assert.Len(t, items, 2)
@ -564,13 +564,13 @@ func TestResolveLoop_Infra_Good_SliceString(t *testing.T) {
assert.Equal(t, "y", items[1]) 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") e := NewExecutor("/tmp")
items := e.resolveLoop(nil, "host1") items := e.resolveLoop(nil, "host1")
assert.Nil(t, items) assert.Nil(t, items)
} }
func TestResolveLoop_Infra_Good_VarReference(t *testing.T) { func TestModulesInfra_ResolveLoop_Good_VarReference(t *testing.T) {
e := NewExecutor("/tmp") e := NewExecutor("/tmp")
e.vars["my_list"] = []any{"item1", "item2", "item3"} e.vars["my_list"] = []any{"item1", "item2", "item3"}
@ -585,7 +585,7 @@ func TestResolveLoop_Infra_Good_VarReference(t *testing.T) {
assert.Nil(t, items) assert.Nil(t, items)
} }
func TestResolveLoop_Infra_Good_MixedTypes(t *testing.T) { func TestModulesInfra_ResolveLoop_Good_MixedTypes(t *testing.T) {
e := NewExecutor("/tmp") e := NewExecutor("/tmp")
items := e.resolveLoop([]any{"str", 42, true, map[string]any{"key": "val"}}, "host1") items := e.resolveLoop([]any{"str", 42, true, map[string]any{"key": "val"}}, "host1")
assert.Len(t, items, 4) assert.Len(t, items, 4)
@ -598,7 +598,7 @@ func TestResolveLoop_Infra_Good_MixedTypes(t *testing.T) {
// 1. Error Propagation — handleNotify // 1. Error Propagation — handleNotify
// =========================================================================== // ===========================================================================
func TestHandleNotify_Infra_Good_SingleString(t *testing.T) { func TestModulesInfra_HandleNotify_Good_SingleString(t *testing.T) {
e := NewExecutor("/tmp") e := NewExecutor("/tmp")
e.handleNotify("restart nginx") e.handleNotify("restart nginx")
@ -606,7 +606,7 @@ func TestHandleNotify_Infra_Good_SingleString(t *testing.T) {
assert.False(t, e.notified["restart apache"]) 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 := NewExecutor("/tmp")
e.handleNotify([]string{"restart nginx", "reload haproxy"}) 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"]) 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 := NewExecutor("/tmp")
e.handleNotify([]any{"handler1", "handler2", "handler3"}) e.handleNotify([]any{"handler1", "handler2", "handler3"})
@ -623,14 +623,14 @@ func TestHandleNotify_Infra_Good_AnySlice(t *testing.T) {
assert.True(t, e.notified["handler3"]) 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") e := NewExecutor("/tmp")
// Should not panic // Should not panic
e.handleNotify(nil) e.handleNotify(nil)
assert.Empty(t, e.notified) 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 := NewExecutor("/tmp")
e.handleNotify("handler1") e.handleNotify("handler1")
e.handleNotify("handler2") e.handleNotify("handler2")
@ -643,33 +643,33 @@ func TestHandleNotify_Infra_Good_MultipleCallsAccumulate(t *testing.T) {
// 1. Error Propagation — normalizeConditions // 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") result := normalizeConditions("my_var is defined")
assert.Equal(t, []string{"my_var is defined"}, result) 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"}) result := normalizeConditions([]string{"cond1", "cond2"})
assert.Equal(t, []string{"cond1", "cond2"}, result) 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"}) result := normalizeConditions([]any{"cond1", "cond2"})
assert.Equal(t, []string{"cond1", "cond2"}, result) 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) result := normalizeConditions(nil)
assert.Nil(t, result) 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 // Non-string types in any slice are silently skipped
result := normalizeConditions([]any{"cond1", 42}) result := normalizeConditions([]any{"cond1", 42})
assert.Equal(t, []string{"cond1"}, result) 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) result := normalizeConditions(42)
assert.Nil(t, result) assert.Nil(t, result)
} }
@ -678,7 +678,7 @@ func TestNormalizeConditions_Infra_Good_UnsupportedType(t *testing.T) {
// 2. Become/Sudo // 2. Become/Sudo
// =========================================================================== // ===========================================================================
func TestBecome_Infra_Good_SetBecomeTrue(t *testing.T) { func TestModulesInfra_Become_Good_SetBecomeTrue(t *testing.T) {
cfg := SSHConfig{ cfg := SSHConfig{
Host: "test-host", Host: "test-host",
Port: 22, Port: 22,
@ -695,7 +695,7 @@ func TestBecome_Infra_Good_SetBecomeTrue(t *testing.T) {
assert.Equal(t, "secret", client.becomePass) assert.Equal(t, "secret", client.becomePass)
} }
func TestBecome_Infra_Good_SetBecomeFalse(t *testing.T) { func TestModulesInfra_Become_Good_SetBecomeFalse(t *testing.T) {
cfg := SSHConfig{ cfg := SSHConfig{
Host: "test-host", Host: "test-host",
Port: 22, Port: 22,
@ -709,7 +709,7 @@ func TestBecome_Infra_Good_SetBecomeFalse(t *testing.T) {
assert.Empty(t, client.becomePass) 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"} cfg := SSHConfig{Host: "test-host"}
client, err := NewSSHClient(cfg) client, err := NewSSHClient(cfg)
require.NoError(t, err) require.NoError(t, err)
@ -722,7 +722,7 @@ func TestBecome_Infra_Good_SetBecomeMethod(t *testing.T) {
assert.Equal(t, "pass123", client.becomePass) 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"} cfg := SSHConfig{Host: "test-host"}
client, err := NewSSHClient(cfg) client, err := NewSSHClient(cfg)
require.NoError(t, err) require.NoError(t, err)
@ -737,7 +737,7 @@ func TestBecome_Infra_Good_DisableAfterEnable(t *testing.T) {
assert.Equal(t, "secret", client.becomePass) assert.Equal(t, "secret", client.becomePass)
} }
func TestBecome_Infra_Good_MockBecomeTracking(t *testing.T) { func TestModulesInfra_Become_Good_MockBecomeTracking(t *testing.T) {
mock := NewMockSSHClient() mock := NewMockSSHClient()
assert.False(t, mock.become) assert.False(t, mock.become)
@ -747,7 +747,7 @@ func TestBecome_Infra_Good_MockBecomeTracking(t *testing.T) {
assert.Equal(t, "password", mock.becomePass) 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 // When become is true but no user specified, it defaults to root in the Run method
cfg := SSHConfig{ cfg := SSHConfig{
Host: "test-host", 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 // 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{ cfg := SSHConfig{
Host: "test-host", Host: "test-host",
Become: true, Become: true,
@ -777,7 +777,7 @@ func TestBecome_Infra_Good_PasswordlessBecome(t *testing.T) {
assert.Empty(t, client.password) 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 // Test that getClient applies play-level become settings
e := NewExecutor("/tmp") e := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{ e.SetInventoryDirect(&Inventory{
@ -806,7 +806,7 @@ func TestBecome_Infra_Good_ExecutorPlayBecome(t *testing.T) {
// 3. Fact Gathering // 3. Fact Gathering
// =========================================================================== // ===========================================================================
func TestFacts_Infra_Good_UbuntuParsing(t *testing.T) { func TestModulesInfra_Facts_Good_UbuntuParsing(t *testing.T) {
e, mock := newTestExecutorWithMock("web1") e, mock := newTestExecutorWithMock("web1")
// Mock os-release output for Ubuntu // Mock os-release output for Ubuntu
@ -821,10 +821,10 @@ func TestFacts_Infra_Good_UbuntuParsing(t *testing.T) {
facts := &Facts{} facts := &Facts{}
stdout, _, _, _ := mock.Run(nil, "hostname -f 2>/dev/null || hostname") 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") 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") stdout, _, _, _ = mock.Run(nil, "cat /etc/os-release 2>/dev/null | grep -E '^(ID|VERSION_ID)=' | head -2")
for _, line := range splitLines(stdout) { for _, line := range splitLines(stdout) {
@ -837,10 +837,10 @@ func TestFacts_Infra_Good_UbuntuParsing(t *testing.T) {
} }
stdout, _, _, _ = mock.Run(nil, "uname -m") stdout, _, _, _ = mock.Run(nil, "uname -m")
facts.Architecture = trimSpace(stdout) facts.Architecture = trimFactSpace(stdout)
stdout, _, _, _ = mock.Run(nil, "uname -r") stdout, _, _, _ = mock.Run(nil, "uname -r")
facts.Kernel = trimSpace(stdout) facts.Kernel = trimFactSpace(stdout)
e.facts["web1"] = facts e.facts["web1"] = facts
@ -859,7 +859,7 @@ func TestFacts_Infra_Good_UbuntuParsing(t *testing.T) {
assert.Equal(t, "ubuntu", result) assert.Equal(t, "ubuntu", result)
} }
func TestFacts_Infra_Good_CentOSParsing(t *testing.T) { func TestModulesInfra_Facts_Good_CentOSParsing(t *testing.T) {
facts := &Facts{} facts := &Facts{}
osRelease := "ID=centos\nVERSION_ID=\"8\"\n" 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) assert.Equal(t, "8", facts.Version)
} }
func TestFacts_Infra_Good_AlpineParsing(t *testing.T) { func TestModulesInfra_Facts_Good_AlpineParsing(t *testing.T) {
facts := &Facts{} facts := &Facts{}
osRelease := "ID=alpine\nVERSION_ID=3.19.1\n" 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) 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{} facts := &Facts{}
osRelease := "ID=debian\nVERSION_ID=\"12\"\n" 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) 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 := NewExecutor("/tmp")
e.facts["host1"] = &Facts{ e.facts["host1"] = &Facts{
Hostname: "myserver", 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)) 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 := NewExecutor("/tmp")
e.facts["host1"] = &Facts{ e.facts["host1"] = &Facts{
Architecture: "aarch64", Architecture: "aarch64",
@ -931,7 +931,7 @@ func TestFacts_Infra_Good_ArchitectureResolution(t *testing.T) {
assert.Equal(t, "aarch64", result) 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 := NewExecutor("/tmp")
e.facts["host1"] = &Facts{ e.facts["host1"] = &Facts{
Kernel: "5.15.0-91-generic", 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) 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") e := NewExecutor("/tmp")
// No facts gathered for host1 // No facts gathered for host1
result := e.templateString("{{ ansible_hostname }}", "host1", nil) 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) 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 // When connection is local, gatherFacts sets minimal facts
e := NewExecutor("/tmp") e := NewExecutor("/tmp")
e.facts["localhost"] = &Facts{ e.facts["localhost"] = &Facts{
@ -964,7 +964,7 @@ func TestFacts_Infra_Good_LocalhostFacts(t *testing.T) {
// 4. Idempotency // 4. Idempotency
// =========================================================================== // ===========================================================================
func TestIdempotency_Infra_Good_GroupAlreadyExists(t *testing.T) { func TestModulesInfra_Idempotency_Good_GroupAlreadyExists(t *testing.T) {
_, mock := newTestExecutorWithMock("host1") _, mock := newTestExecutorWithMock("host1")
// Mock: getent group docker succeeds (group exists) — the || means groupadd is skipped // 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) assert.False(t, result.Failed)
} }
func TestIdempotency_Infra_Good_AuthorizedKeyAlreadyPresent(t *testing.T) { func TestModulesInfra_Idempotency_Good_AuthorizedKeyAlreadyPresent(t *testing.T) {
_, mock := newTestExecutorWithMock("host1") _, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7xfG..." + testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQC7xfG..." +
@ -1020,7 +1020,7 @@ func TestIdempotency_Infra_Good_AuthorizedKeyAlreadyPresent(t *testing.T) {
assert.False(t, result.Failed) 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 := newTestExecutorWithMock("host1")
// Mock: docker compose up -d returns "Up to date" in stdout // 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) 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 := newTestExecutorWithMock("host1")
// Mock: docker compose up -d with actual changes // 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) assert.False(t, result.Failed)
} }
func TestIdempotency_Infra_Good_DockerComposeUpToDateInStderr(t *testing.T) { func TestModulesInfra_Idempotency_Good_DockerComposeUpToDateInStderr(t *testing.T) {
_, mock := newTestExecutorWithMock("host1") _, mock := newTestExecutorWithMock("host1")
// Some versions of docker compose output status to stderr // 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) 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 := newTestExecutorWithMock("host1")
// Mock: getent fails (group does not exist), groupadd succeeds // 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) 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 := newTestExecutorWithMock("host1")
// Mock: stat reports the file exists // Mock: stat reports the file exists
@ -1110,7 +1110,7 @@ func TestIdempotency_Infra_Good_ServiceStatChanged(t *testing.T) {
assert.True(t, stat["exists"].(bool)) 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") _, mock := newTestExecutorWithMock("host1")
// No stat info added — will return exists=false from mock // 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 // 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 := NewExecutor("/tmp")
e.SetInventoryDirect(&Inventory{ e.SetInventoryDirect(&Inventory{
All: &InventoryGroup{ All: &InventoryGroup{
@ -1148,7 +1148,7 @@ func TestResolveExpr_Infra_Good_HostVars(t *testing.T) {
assert.Equal(t, "custom_value", result) 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") e := NewExecutor("/tmp")
args := map[string]any{ args := map[string]any{
@ -1159,13 +1159,13 @@ func TestTemplateArgs_Infra_Good_InventoryHostname(t *testing.T) {
assert.Equal(t, "web1", result["hostname"]) 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") e := NewExecutor("/tmp")
// Unknown conditions default to true (permissive) // Unknown conditions default to true (permissive)
assert.True(t, e.evalCondition("some_complex_expression == 'value'", "host1")) 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 := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{ e.results["host1"] = map[string]*TaskResult{
"my_cmd": {Stdout: "output_text", RC: 0}, "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) 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") e := NewExecutor("/tmp")
result := e.getRegisteredVar("host1", "nonexistent") result := e.getRegisteredVar("host1", "nonexistent")
assert.Nil(t, result) assert.Nil(t, result)
} }
func TestGetRegisteredVar_Infra_Bad_WrongHost(t *testing.T) { func TestModulesInfra_GetRegisteredVar_Bad_WrongHost(t *testing.T) {
e := NewExecutor("/tmp") e := NewExecutor("/tmp")
e.results["host1"] = map[string]*TaskResult{ e.results["host1"] = map[string]*TaskResult{
"my_cmd": {Stdout: "output"}, "my_cmd": {Stdout: "output"},
@ -1200,7 +1200,7 @@ func TestGetRegisteredVar_Infra_Bad_WrongHost(t *testing.T) {
// String helper utilities used by fact tests // String helper utilities used by fact tests
// =========================================================================== // ===========================================================================
func trimSpace(s string) string { func trimFactSpace(s string) string {
result := "" result := ""
for _, c := range s { for _, c := range s {
if c != '\n' && c != '\r' && c != ' ' && c != '\t' { if c != '\n' && c != '\r' && c != ' ' && c != '\t' {

View file

@ -13,7 +13,7 @@ import (
// --- service module --- // --- service module ---
func TestModuleService_Good_Start(t *testing.T) { func TestModulesSvc_ModuleService_Good_Start(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl start nginx`, "Started", "", 0) mock.expectCommand(`systemctl start nginx`, "Started", "", 0)
@ -29,7 +29,7 @@ func TestModuleService_Good_Start(t *testing.T) {
assert.Equal(t, 1, mock.commandCount()) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl stop nginx`, "", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl restart docker`, "", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl reload nginx`, "", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl enable nginx`, "", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl disable nginx`, "", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl start nginx`, "", "", 0) mock.expectCommand(`systemctl start nginx`, "", "", 0)
mock.expectCommand(`systemctl enable 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl restart sshd`, "", "", 0) mock.expectCommand(`systemctl restart sshd`, "", "", 0)
mock.expectCommand(`systemctl disable 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`)) 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") e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient() mock := NewMockSSHClient()
@ -154,7 +154,7 @@ func TestModuleService_Bad_MissingName(t *testing.T) {
assert.Contains(t, err.Error(), "name required") 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 // When neither state nor enabled is provided, no commands run
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
@ -168,7 +168,7 @@ func TestModuleService_Good_NoStateNoEnabled(t *testing.T) {
assert.Equal(t, 0, mock.commandCount()) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl start.*`, "", "Failed to start nginx.service", 1) 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) 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 // When state command fails, enable should not run
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl start`, "", "unit not found", 5) mock.expectCommand(`systemctl start`, "", "unit not found", 5)
@ -203,7 +203,7 @@ func TestModuleService_Good_FirstCommandFailsSkipsRest(t *testing.T) {
// --- systemd module --- // --- systemd module ---
func TestModuleSystemd_Good_DaemonReloadThenStart(t *testing.T) { func TestModulesSvc_ModuleSystemd_Good_DaemonReloadThenStart(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl daemon-reload`, "", "", 0) mock.expectCommand(`systemctl daemon-reload`, "", "", 0)
mock.expectCommand(`systemctl start nginx`, "", "", 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") 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl daemon-reload`, "", "", 0) 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`)) 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 // Without daemon_reload, systemd delegates entirely to service
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl restart docker`, "", "", 0) mock.expectCommand(`systemctl restart docker`, "", "", 0)
@ -261,7 +261,7 @@ func TestModuleSystemd_Good_DelegationToService(t *testing.T) {
assert.False(t, mock.hasExecuted(`daemon-reload`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl daemon-reload`, "", "", 0) mock.expectCommand(`systemctl daemon-reload`, "", "", 0)
mock.expectCommand(`systemctl enable myapp`, "", "", 0) mock.expectCommand(`systemctl enable myapp`, "", "", 0)
@ -281,7 +281,7 @@ func TestModuleSystemd_Good_DaemonReloadWithEnable(t *testing.T) {
// --- apt module --- // --- apt module ---
func TestModuleApt_Good_InstallPresent(t *testing.T) { func TestModulesSvc_ModuleApt_Good_InstallPresent(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get install -y -qq nginx`, "installed", "", 0) 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`)) 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 // state=installed is an alias for present
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get install -y -qq curl`, "", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get remove -y -qq nginx`, "", "", 0) 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`)) 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 // state=removed is an alias for absent
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get remove -y -qq nginx`, "", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get install -y -qq --only-upgrade nginx`, "", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get update`, "", "", 0) mock.expectCommand(`apt-get update`, "", "", 0)
mock.expectCommand(`apt-get install -y -qq nginx`, "", "", 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") 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 // update_cache with no name means update only, no install
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get update`, "", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get install`, "", "E: Unable to locate package badpkg", 100) 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) 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) // If no state is given, default is "present" (install)
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get install -y -qq vim`, "", "", 0) mock.expectCommand(`apt-get install -y -qq vim`, "", "", 0)
@ -426,7 +426,7 @@ func TestModuleApt_Good_DefaultStateIsPresent(t *testing.T) {
// --- apt_key module --- // --- apt_key module ---
func TestModuleAptKey_Good_AddWithKeyring(t *testing.T) { func TestModulesSvc_ModuleAptKey_Good_AddWithKeyring(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl -fsSL.*gpg --dearmor`, "", "", 0) 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")) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl -fsSL.*apt-key add -`, "", "", 0) 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 -`)) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleAptKeyWithClient(e, mock, map[string]any{ 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")) 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 // Absent with no keyring — still succeeds, just no rm command
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
@ -485,7 +485,7 @@ func TestModuleAptKey_Good_RemoveWithoutKeyring(t *testing.T) {
assert.Equal(t, 0, mock.commandCount()) assert.Equal(t, 0, mock.commandCount())
} }
func TestModuleAptKey_Bad_MissingURL(t *testing.T) { func TestModulesSvc_ModuleAptKey_Bad_MissingURL(t *testing.T) {
e, _ := newTestExecutorWithMock("host1") e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient() mock := NewMockSSHClient()
@ -497,7 +497,7 @@ func TestModuleAptKey_Bad_MissingURL(t *testing.T) {
assert.Contains(t, err.Error(), "url required") 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl`, "", "curl: (22) 404 Not Found", 22) mock.expectCommand(`curl`, "", "curl: (22) 404 Not Found", 22)
@ -513,7 +513,7 @@ func TestModuleAptKey_Good_CommandFailure(t *testing.T) {
// --- apt_repository module --- // --- apt_repository module ---
func TestModuleAptRepository_Good_AddRepository(t *testing.T) { func TestModulesSvc_ModuleAptRepository_Good_AddRepository(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`echo.*sources\.list\.d`, "", "", 0) mock.expectCommand(`echo.*sources\.list\.d`, "", "", 0)
mock.expectCommand(`apt-get update`, "", "", 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")) 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") e, mock := newTestExecutorWithMock("host1")
result, err := moduleAptRepositoryWithClient(e, mock, map[string]any{ 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")) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`echo`, "", "", 0) mock.expectCommand(`echo`, "", "", 0)
mock.expectCommand(`apt-get update`, "", "", 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`echo`, "", "", 0) mock.expectCommand(`echo`, "", "", 0)
@ -582,7 +582,7 @@ func TestModuleAptRepository_Good_AddWithoutUpdateCache(t *testing.T) {
assert.False(t, mock.hasExecuted(`apt-get update`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`echo`, "", "", 0) mock.expectCommand(`echo`, "", "", 0)
mock.expectCommand(`apt-get update`, "", "", 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")) 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 // When no filename is given, it auto-generates from the repo string
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`echo`, "", "", 0) 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/")) 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") e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient() mock := NewMockSSHClient()
@ -625,7 +625,7 @@ func TestModuleAptRepository_Bad_MissingRepo(t *testing.T) {
assert.Contains(t, err.Error(), "repo required") 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`echo`, "", "permission denied", 1) mock.expectCommand(`echo`, "", "permission denied", 1)
@ -641,7 +641,7 @@ func TestModuleAptRepository_Good_WriteFailure(t *testing.T) {
// --- package module --- // --- package module ---
func TestModulePackage_Good_DetectAptAndDelegate(t *testing.T) { func TestModulesSvc_ModulePackage_Good_DetectAptAndDelegate(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
// First command: which apt-get returns the path // First command: which apt-get returns the path
mock.expectCommand(`which apt-get`, "/usr/bin/apt-get", "", 0) 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`)) 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 // When which returns nothing (no package manager found), still falls back to apt
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`which apt-get`, "", "", 1) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`which apt-get`, "/usr/bin/apt-get", "", 0) mock.expectCommand(`which apt-get`, "/usr/bin/apt-get", "", 0)
mock.expectCommand(`apt-get remove -y -qq nano`, "", "", 0) mock.expectCommand(`apt-get remove -y -qq nano`, "", "", 0)
@ -694,7 +694,7 @@ func TestModulePackage_Good_RemovePackage(t *testing.T) {
// --- pip module --- // --- pip module ---
func TestModulePip_Good_InstallPresent(t *testing.T) { func TestModulesSvc_ModulePip_Good_InstallPresent(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`pip3 install flask`, "Successfully installed", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`pip3 uninstall -y flask`, "Successfully uninstalled", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`pip3 install --upgrade flask`, "Successfully installed", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`/opt/venv/bin/pip install requests`, "", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`pip3 install django`, "", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`pip3 install`, "", "ERROR: No matching distribution found", 1) 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") 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 // state=installed is an alias for present
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`pip3 install boto3`, "", "", 0) 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`)) 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 // state=removed is an alias for absent
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`pip3 uninstall -y boto3`, "", "", 0) mock.expectCommand(`pip3 uninstall -y boto3`, "", "", 0)
@ -814,7 +814,7 @@ func TestModulePip_Good_RemovedAlias(t *testing.T) {
// --- Cross-module dispatch tests --- // --- Cross-module dispatch tests ---
func TestExecuteModuleWithMock_Good_DispatchService(t *testing.T) { func TestModulesSvc_ExecuteModuleWithMock_Good_DispatchService(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl restart nginx`, "", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`systemctl daemon-reload`, "", "", 0) mock.expectCommand(`systemctl daemon-reload`, "", "", 0)
mock.expectCommand(`systemctl start myapp`, "", "", 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`apt-get install -y -qq nginx`, "", "", 0) 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`)) 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") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl.*gpg`, "", "", 0) mock.expectCommand(`curl.*gpg`, "", "", 0)
@ -892,7 +892,7 @@ func TestExecuteModuleWithMock_Good_DispatchAptKey(t *testing.T) {
assert.True(t, result.Changed) assert.True(t, result.Changed)
} }
func TestExecuteModuleWithMock_Good_DispatchAptRepository(t *testing.T) { func TestModulesSvc_ExecuteModuleWithMock_Good_DispatchAptRepository(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`echo`, "", "", 0) mock.expectCommand(`echo`, "", "", 0)
mock.expectCommand(`apt-get update`, "", "", 0) mock.expectCommand(`apt-get update`, "", "", 0)
@ -911,7 +911,7 @@ func TestExecuteModuleWithMock_Good_DispatchAptRepository(t *testing.T) {
assert.True(t, result.Changed) assert.True(t, result.Changed)
} }
func TestExecuteModuleWithMock_Good_DispatchPackage(t *testing.T) { func TestModulesSvc_ExecuteModuleWithMock_Good_DispatchPackage(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`which apt-get`, "/usr/bin/apt-get", "", 0) mock.expectCommand(`which apt-get`, "/usr/bin/apt-get", "", 0)
mock.expectCommand(`apt-get install -y -qq git`, "", "", 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) assert.True(t, result.Changed)
} }
func TestExecuteModuleWithMock_Good_DispatchPip(t *testing.T) { func TestModulesSvc_ExecuteModuleWithMock_Good_DispatchPip(t *testing.T) {
e, mock := newTestExecutorWithMock("host1") e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`pip3 install ansible`, "", "", 0) mock.expectCommand(`pip3 install ansible`, "", "", 0)

View file

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

View file

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

52
ssh.go
View file

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

View file

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

23
test_primitives_test.go Normal file
View file

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

View file

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

View file

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