test(ansible): Phase 1 Step 1.2 — file operation module tests
54 new tests for copy/file/lineinfile/blockinfile/stat/template modules. Extended mock_ssh_test.go with sshFileRunner interface and 6 module shims. Fixed unsupported module test (copy→hostname now that copy is supported). Total ansible tests: 208. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
fd46e82297
commit
c7da9ad16c
3 changed files with 1177 additions and 1 deletions
|
|
@ -6,6 +6,7 @@ import (
|
|||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
|
@ -359,6 +360,18 @@ func executeModuleWithMock(e *Executor, mock *MockSSHClient, host string, task *
|
|||
return moduleRawWithClient(e, mock, args)
|
||||
case "ansible.builtin.script":
|
||||
return moduleScriptWithClient(e, mock, args)
|
||||
case "ansible.builtin.copy":
|
||||
return moduleCopyWithClient(e, mock, args, host, task)
|
||||
case "ansible.builtin.template":
|
||||
return moduleTemplateWithClient(e, mock, args, host, task)
|
||||
case "ansible.builtin.file":
|
||||
return moduleFileWithClient(e, mock, args)
|
||||
case "ansible.builtin.lineinfile":
|
||||
return moduleLineinfileWithClient(e, mock, args)
|
||||
case "ansible.builtin.blockinfile":
|
||||
return moduleBlockinfileWithClient(e, mock, args)
|
||||
case "ansible.builtin.stat":
|
||||
return moduleStatWithClient(e, mock, args)
|
||||
default:
|
||||
return nil, fmt.Errorf("mock dispatch: unsupported module %s", module)
|
||||
}
|
||||
|
|
@ -470,6 +483,270 @@ func moduleScriptWithClient(_ *Executor, client sshRunner, args map[string]any)
|
|||
}, nil
|
||||
}
|
||||
|
||||
// --- Extended interface for file operations ---
|
||||
// File modules need Upload, Stat, FileExists in addition to Run/RunScript.
|
||||
|
||||
type sshFileRunner interface {
|
||||
sshRunner
|
||||
Upload(ctx context.Context, local io.Reader, remote string, mode os.FileMode) error
|
||||
Stat(ctx context.Context, path string) (map[string]any, error)
|
||||
FileExists(ctx context.Context, path string) (bool, error)
|
||||
}
|
||||
|
||||
// --- File module shims ---
|
||||
|
||||
func moduleCopyWithClient(e *Executor, client sshFileRunner, args map[string]any, host string, task *Task) (*TaskResult, error) {
|
||||
dest := getStringArg(args, "dest", "")
|
||||
if dest == "" {
|
||||
return nil, fmt.Errorf("copy: dest required")
|
||||
}
|
||||
|
||||
var content []byte
|
||||
var err error
|
||||
|
||||
if src := getStringArg(args, "src", ""); src != "" {
|
||||
content, err = os.ReadFile(src)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read src: %w", err)
|
||||
}
|
||||
} else if c := getStringArg(args, "content", ""); c != "" {
|
||||
content = []byte(c)
|
||||
} else {
|
||||
return nil, fmt.Errorf("copy: src or content required")
|
||||
}
|
||||
|
||||
mode := os.FileMode(0644)
|
||||
if m := getStringArg(args, "mode", ""); m != "" {
|
||||
if parsed, parseErr := strconv.ParseInt(m, 8, 32); parseErr == nil {
|
||||
mode = os.FileMode(parsed)
|
||||
}
|
||||
}
|
||||
|
||||
err = client.Upload(context.Background(), strings.NewReader(string(content)), dest, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Handle owner/group (best-effort, errors ignored)
|
||||
if owner := getStringArg(args, "owner", ""); owner != "" {
|
||||
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("chown %s %q", owner, dest))
|
||||
}
|
||||
if group := getStringArg(args, "group", ""); group != "" {
|
||||
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("chgrp %s %q", group, dest))
|
||||
}
|
||||
|
||||
return &TaskResult{Changed: true, Msg: fmt.Sprintf("copied to %s", dest)}, nil
|
||||
}
|
||||
|
||||
func moduleTemplateWithClient(e *Executor, client sshFileRunner, args map[string]any, host string, task *Task) (*TaskResult, error) {
|
||||
src := getStringArg(args, "src", "")
|
||||
dest := getStringArg(args, "dest", "")
|
||||
if src == "" || dest == "" {
|
||||
return nil, fmt.Errorf("template: src and dest required")
|
||||
}
|
||||
|
||||
// Process template
|
||||
content, err := e.TemplateFile(src, host, task)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("template: %w", err)
|
||||
}
|
||||
|
||||
mode := os.FileMode(0644)
|
||||
if m := getStringArg(args, "mode", ""); m != "" {
|
||||
if parsed, parseErr := strconv.ParseInt(m, 8, 32); parseErr == nil {
|
||||
mode = os.FileMode(parsed)
|
||||
}
|
||||
}
|
||||
|
||||
err = client.Upload(context.Background(), strings.NewReader(content), dest, mode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TaskResult{Changed: true, Msg: fmt.Sprintf("templated to %s", dest)}, nil
|
||||
}
|
||||
|
||||
func moduleFileWithClient(_ *Executor, client sshFileRunner, args map[string]any) (*TaskResult, error) {
|
||||
path := getStringArg(args, "path", "")
|
||||
if path == "" {
|
||||
path = getStringArg(args, "dest", "")
|
||||
}
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("file: path required")
|
||||
}
|
||||
|
||||
state := getStringArg(args, "state", "file")
|
||||
|
||||
switch state {
|
||||
case "directory":
|
||||
mode := getStringArg(args, "mode", "0755")
|
||||
cmd := fmt.Sprintf("mkdir -p %q && chmod %s %q", path, mode, path)
|
||||
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
|
||||
if err != nil || rc != 0 {
|
||||
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
||||
}
|
||||
|
||||
case "absent":
|
||||
cmd := fmt.Sprintf("rm -rf %q", path)
|
||||
_, stderr, rc, err := client.Run(context.Background(), cmd)
|
||||
if err != nil || rc != 0 {
|
||||
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
|
||||
}
|
||||
|
||||
case "touch":
|
||||
cmd := fmt.Sprintf("touch %q", path)
|
||||
_, stderr, rc, err := client.Run(context.Background(), cmd)
|
||||
if err != nil || rc != 0 {
|
||||
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
|
||||
}
|
||||
|
||||
case "link":
|
||||
src := getStringArg(args, "src", "")
|
||||
if src == "" {
|
||||
return nil, fmt.Errorf("file: src required for link state")
|
||||
}
|
||||
cmd := fmt.Sprintf("ln -sf %q %q", src, path)
|
||||
_, stderr, rc, err := client.Run(context.Background(), cmd)
|
||||
if err != nil || rc != 0 {
|
||||
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
|
||||
}
|
||||
|
||||
case "file":
|
||||
// Ensure file exists and set permissions
|
||||
if mode := getStringArg(args, "mode", ""); mode != "" {
|
||||
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("chmod %s %q", mode, path))
|
||||
}
|
||||
}
|
||||
|
||||
// Handle owner/group (best-effort, errors ignored)
|
||||
if owner := getStringArg(args, "owner", ""); owner != "" {
|
||||
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("chown %s %q", owner, path))
|
||||
}
|
||||
if group := getStringArg(args, "group", ""); group != "" {
|
||||
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("chgrp %s %q", group, path))
|
||||
}
|
||||
if recurse := getBoolArg(args, "recurse", false); recurse {
|
||||
if owner := getStringArg(args, "owner", ""); owner != "" {
|
||||
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("chown -R %s %q", owner, path))
|
||||
}
|
||||
}
|
||||
|
||||
return &TaskResult{Changed: true}, nil
|
||||
}
|
||||
|
||||
func moduleLineinfileWithClient(_ *Executor, client sshRunner, args map[string]any) (*TaskResult, error) {
|
||||
path := getStringArg(args, "path", "")
|
||||
if path == "" {
|
||||
path = getStringArg(args, "dest", "")
|
||||
}
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("lineinfile: path required")
|
||||
}
|
||||
|
||||
line := getStringArg(args, "line", "")
|
||||
regexpArg := getStringArg(args, "regexp", "")
|
||||
state := getStringArg(args, "state", "present")
|
||||
|
||||
if state == "absent" {
|
||||
if regexpArg != "" {
|
||||
cmd := fmt.Sprintf("sed -i '/%s/d' %q", regexpArg, path)
|
||||
_, stderr, rc, _ := client.Run(context.Background(), cmd)
|
||||
if rc != 0 {
|
||||
return &TaskResult{Failed: true, Msg: stderr, RC: rc}, nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// state == present
|
||||
if regexpArg != "" {
|
||||
// Replace line matching regexp
|
||||
escapedLine := strings.ReplaceAll(line, "/", "\\/")
|
||||
cmd := fmt.Sprintf("sed -i 's/%s/%s/' %q", regexpArg, escapedLine, path)
|
||||
_, _, rc, _ := client.Run(context.Background(), cmd)
|
||||
if rc != 0 {
|
||||
// Line not found, append
|
||||
cmd = fmt.Sprintf("echo %q >> %q", line, path)
|
||||
_, _, _, _ = client.Run(context.Background(), cmd)
|
||||
}
|
||||
} else if line != "" {
|
||||
// Ensure line is present
|
||||
cmd := fmt.Sprintf("grep -qxF %q %q || echo %q >> %q", line, path, line, path)
|
||||
_, _, _, _ = client.Run(context.Background(), cmd)
|
||||
}
|
||||
}
|
||||
|
||||
return &TaskResult{Changed: true}, nil
|
||||
}
|
||||
|
||||
func moduleBlockinfileWithClient(_ *Executor, client sshFileRunner, args map[string]any) (*TaskResult, error) {
|
||||
path := getStringArg(args, "path", "")
|
||||
if path == "" {
|
||||
path = getStringArg(args, "dest", "")
|
||||
}
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("blockinfile: path required")
|
||||
}
|
||||
|
||||
block := getStringArg(args, "block", "")
|
||||
marker := getStringArg(args, "marker", "# {mark} ANSIBLE MANAGED BLOCK")
|
||||
state := getStringArg(args, "state", "present")
|
||||
create := getBoolArg(args, "create", false)
|
||||
|
||||
beginMarker := strings.Replace(marker, "{mark}", "BEGIN", 1)
|
||||
endMarker := strings.Replace(marker, "{mark}", "END", 1)
|
||||
|
||||
if state == "absent" {
|
||||
// Remove block
|
||||
cmd := fmt.Sprintf("sed -i '/%s/,/%s/d' %q",
|
||||
strings.ReplaceAll(beginMarker, "/", "\\/"),
|
||||
strings.ReplaceAll(endMarker, "/", "\\/"),
|
||||
path)
|
||||
_, _, _, _ = client.Run(context.Background(), cmd)
|
||||
return &TaskResult{Changed: true}, nil
|
||||
}
|
||||
|
||||
// Create file if needed (best-effort)
|
||||
if create {
|
||||
_, _, _, _ = client.Run(context.Background(), fmt.Sprintf("touch %q", path))
|
||||
}
|
||||
|
||||
// Remove existing block and add new one
|
||||
escapedBlock := strings.ReplaceAll(block, "'", "'\\''")
|
||||
cmd := fmt.Sprintf(`
|
||||
sed -i '/%s/,/%s/d' %q 2>/dev/null || true
|
||||
cat >> %q << 'BLOCK_EOF'
|
||||
%s
|
||||
%s
|
||||
%s
|
||||
BLOCK_EOF
|
||||
`, strings.ReplaceAll(beginMarker, "/", "\\/"),
|
||||
strings.ReplaceAll(endMarker, "/", "\\/"),
|
||||
path, path, beginMarker, escapedBlock, endMarker)
|
||||
|
||||
stdout, stderr, rc, err := client.RunScript(context.Background(), cmd)
|
||||
if err != nil || rc != 0 {
|
||||
return &TaskResult{Failed: true, Msg: stderr, Stdout: stdout, RC: rc}, nil
|
||||
}
|
||||
|
||||
return &TaskResult{Changed: true}, nil
|
||||
}
|
||||
|
||||
func moduleStatWithClient(_ *Executor, client sshFileRunner, args map[string]any) (*TaskResult, error) {
|
||||
path := getStringArg(args, "path", "")
|
||||
if path == "" {
|
||||
return nil, fmt.Errorf("stat: path required")
|
||||
}
|
||||
|
||||
stat, err := client.Stat(context.Background(), path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &TaskResult{
|
||||
Changed: false,
|
||||
Data: map[string]any{"stat": stat},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// --- String helpers for assertions ---
|
||||
|
||||
// containsSubstring checks if any executed command contains the given substring.
|
||||
|
|
|
|||
|
|
@ -690,7 +690,7 @@ func TestExecuteModuleWithMock_Bad_UnsupportedModule(t *testing.T) {
|
|||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
task := &Task{
|
||||
Module: "ansible.builtin.copy",
|
||||
Module: "ansible.builtin.hostname",
|
||||
Args: map[string]any{},
|
||||
}
|
||||
|
||||
|
|
|
|||
899
ansible/modules_file_test.go
Normal file
899
ansible/modules_file_test.go
Normal file
|
|
@ -0,0 +1,899 @@
|
|||
package ansible
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// Step 1.2: copy / template / file / lineinfile / blockinfile / stat module tests
|
||||
// ============================================================
|
||||
|
||||
// --- copy module ---
|
||||
|
||||
func TestModuleCopy_Good_ContentUpload(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleCopyWithClient(e, mock, map[string]any{
|
||||
"content": "server_name=web01",
|
||||
"dest": "/etc/app/config",
|
||||
}, "host1", &Task{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.Contains(t, result.Msg, "copied to /etc/app/config")
|
||||
|
||||
// Verify upload was performed
|
||||
assert.Equal(t, 1, mock.uploadCount())
|
||||
up := mock.lastUpload()
|
||||
require.NotNil(t, up)
|
||||
assert.Equal(t, "/etc/app/config", up.Remote)
|
||||
assert.Equal(t, []byte("server_name=web01"), up.Content)
|
||||
assert.Equal(t, os.FileMode(0644), up.Mode)
|
||||
}
|
||||
|
||||
func TestModuleCopy_Good_SrcFile(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
srcPath := filepath.Join(tmpDir, "nginx.conf")
|
||||
require.NoError(t, os.WriteFile(srcPath, []byte("worker_processes auto;"), 0644))
|
||||
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleCopyWithClient(e, mock, map[string]any{
|
||||
"src": srcPath,
|
||||
"dest": "/etc/nginx/nginx.conf",
|
||||
}, "host1", &Task{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
up := mock.lastUpload()
|
||||
require.NotNil(t, up)
|
||||
assert.Equal(t, "/etc/nginx/nginx.conf", up.Remote)
|
||||
assert.Equal(t, []byte("worker_processes auto;"), up.Content)
|
||||
}
|
||||
|
||||
func TestModuleCopy_Good_OwnerGroup(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleCopyWithClient(e, mock, map[string]any{
|
||||
"content": "data",
|
||||
"dest": "/opt/app/data.txt",
|
||||
"owner": "appuser",
|
||||
"group": "appgroup",
|
||||
}, "host1", &Task{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
// Upload + chown + chgrp = 1 upload + 2 Run calls
|
||||
assert.Equal(t, 1, mock.uploadCount())
|
||||
assert.True(t, mock.hasExecuted(`chown appuser "/opt/app/data.txt"`))
|
||||
assert.True(t, mock.hasExecuted(`chgrp appgroup "/opt/app/data.txt"`))
|
||||
}
|
||||
|
||||
func TestModuleCopy_Good_CustomMode(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleCopyWithClient(e, mock, map[string]any{
|
||||
"content": "#!/bin/bash\necho hello",
|
||||
"dest": "/usr/local/bin/hello.sh",
|
||||
"mode": "0755",
|
||||
}, "host1", &Task{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
up := mock.lastUpload()
|
||||
require.NotNil(t, up)
|
||||
assert.Equal(t, os.FileMode(0755), up.Mode)
|
||||
}
|
||||
|
||||
func TestModuleCopy_Bad_MissingDest(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
_, err := moduleCopyWithClient(e, mock, map[string]any{
|
||||
"content": "data",
|
||||
}, "host1", &Task{})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "dest required")
|
||||
}
|
||||
|
||||
func TestModuleCopy_Bad_MissingSrcAndContent(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
_, err := moduleCopyWithClient(e, mock, map[string]any{
|
||||
"dest": "/tmp/out",
|
||||
}, "host1", &Task{})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "src or content required")
|
||||
}
|
||||
|
||||
func TestModuleCopy_Bad_SrcFileNotFound(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
_, err := moduleCopyWithClient(e, mock, map[string]any{
|
||||
"src": "/nonexistent/file.txt",
|
||||
"dest": "/tmp/out",
|
||||
}, "host1", &Task{})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "read src")
|
||||
}
|
||||
|
||||
func TestModuleCopy_Good_ContentTakesPrecedenceOverSrc(t *testing.T) {
|
||||
// When both content and src are given, src is checked first in the implementation
|
||||
// but if src is empty string, content is used
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleCopyWithClient(e, mock, map[string]any{
|
||||
"content": "from_content",
|
||||
"dest": "/tmp/out",
|
||||
}, "host1", &Task{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
up := mock.lastUpload()
|
||||
assert.Equal(t, []byte("from_content"), up.Content)
|
||||
}
|
||||
|
||||
// --- file module ---
|
||||
|
||||
func TestModuleFile_Good_StateDirectory(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleFileWithClient(e, mock, map[string]any{
|
||||
"path": "/var/lib/app",
|
||||
"state": "directory",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
// Should execute mkdir -p with default mode 0755
|
||||
assert.True(t, mock.hasExecuted(`mkdir -p "/var/lib/app"`))
|
||||
assert.True(t, mock.hasExecuted(`chmod 0755`))
|
||||
}
|
||||
|
||||
func TestModuleFile_Good_StateDirectoryCustomMode(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleFileWithClient(e, mock, map[string]any{
|
||||
"path": "/opt/data",
|
||||
"state": "directory",
|
||||
"mode": "0700",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.True(t, mock.hasExecuted(`mkdir -p "/opt/data" && chmod 0700 "/opt/data"`))
|
||||
}
|
||||
|
||||
func TestModuleFile_Good_StateAbsent(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleFileWithClient(e, mock, map[string]any{
|
||||
"path": "/tmp/old-dir",
|
||||
"state": "absent",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.True(t, mock.hasExecuted(`rm -rf "/tmp/old-dir"`))
|
||||
}
|
||||
|
||||
func TestModuleFile_Good_StateTouch(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleFileWithClient(e, mock, map[string]any{
|
||||
"path": "/var/log/app.log",
|
||||
"state": "touch",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.True(t, mock.hasExecuted(`touch "/var/log/app.log"`))
|
||||
}
|
||||
|
||||
func TestModuleFile_Good_StateLink(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleFileWithClient(e, mock, map[string]any{
|
||||
"path": "/usr/local/bin/node",
|
||||
"state": "link",
|
||||
"src": "/opt/node/bin/node",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.True(t, mock.hasExecuted(`ln -sf "/opt/node/bin/node" "/usr/local/bin/node"`))
|
||||
}
|
||||
|
||||
func TestModuleFile_Bad_LinkMissingSrc(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
_, err := moduleFileWithClient(e, mock, map[string]any{
|
||||
"path": "/usr/local/bin/node",
|
||||
"state": "link",
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "src required for link state")
|
||||
}
|
||||
|
||||
func TestModuleFile_Good_OwnerGroupMode(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleFileWithClient(e, mock, map[string]any{
|
||||
"path": "/var/lib/app/data",
|
||||
"state": "directory",
|
||||
"owner": "www-data",
|
||||
"group": "www-data",
|
||||
"mode": "0775",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
// Should have mkdir, chmod in the directory command, then chown and chgrp
|
||||
assert.True(t, mock.hasExecuted(`mkdir -p "/var/lib/app/data" && chmod 0775 "/var/lib/app/data"`))
|
||||
assert.True(t, mock.hasExecuted(`chown 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) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleFileWithClient(e, mock, map[string]any{
|
||||
"path": "/var/www",
|
||||
"state": "directory",
|
||||
"owner": "www-data",
|
||||
"recurse": true,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
// Should have both regular chown and recursive chown
|
||||
assert.True(t, mock.hasExecuted(`chown www-data "/var/www"`))
|
||||
assert.True(t, mock.hasExecuted(`chown -R www-data "/var/www"`))
|
||||
}
|
||||
|
||||
func TestModuleFile_Bad_MissingPath(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
_, err := moduleFileWithClient(e, mock, map[string]any{
|
||||
"state": "directory",
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "path required")
|
||||
}
|
||||
|
||||
func TestModuleFile_Good_DestAliasForPath(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleFileWithClient(e, mock, map[string]any{
|
||||
"dest": "/opt/myapp",
|
||||
"state": "directory",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.True(t, mock.hasExecuted(`mkdir -p "/opt/myapp"`))
|
||||
}
|
||||
|
||||
func TestModuleFile_Good_StateFileWithMode(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleFileWithClient(e, mock, map[string]any{
|
||||
"path": "/etc/config.yml",
|
||||
"state": "file",
|
||||
"mode": "0600",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.True(t, mock.hasExecuted(`chmod 0600 "/etc/config.yml"`))
|
||||
}
|
||||
|
||||
func TestModuleFile_Good_DirectoryCommandFailure(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("mkdir", "", "permission denied", 1)
|
||||
|
||||
result, err := moduleFileWithClient(e, mock, map[string]any{
|
||||
"path": "/root/protected",
|
||||
"state": "directory",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Failed)
|
||||
assert.Contains(t, result.Msg, "permission denied")
|
||||
}
|
||||
|
||||
// --- lineinfile module ---
|
||||
|
||||
func TestModuleLineinfile_Good_InsertLine(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleLineinfileWithClient(e, mock, map[string]any{
|
||||
"path": "/etc/hosts",
|
||||
"line": "192.168.1.100 web01",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
// Should use grep -qxF to check and echo to append
|
||||
assert.True(t, mock.hasExecuted(`grep -qxF`))
|
||||
assert.True(t, mock.hasExecuted(`192.168.1.100 web01`))
|
||||
}
|
||||
|
||||
func TestModuleLineinfile_Good_ReplaceRegexp(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleLineinfileWithClient(e, mock, map[string]any{
|
||||
"path": "/etc/ssh/sshd_config",
|
||||
"regexp": "^#?PermitRootLogin",
|
||||
"line": "PermitRootLogin no",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
// Should use sed to replace
|
||||
assert.True(t, mock.hasExecuted(`sed -i 's/\^#\?PermitRootLogin/PermitRootLogin no/'`))
|
||||
}
|
||||
|
||||
func TestModuleLineinfile_Good_RemoveLine(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleLineinfileWithClient(e, mock, map[string]any{
|
||||
"path": "/etc/hosts",
|
||||
"regexp": "^192\\.168\\.1\\.100",
|
||||
"state": "absent",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
// Should use sed to delete matching lines
|
||||
assert.True(t, mock.hasExecuted(`sed -i '/\^192`))
|
||||
assert.True(t, mock.hasExecuted(`/d'`))
|
||||
}
|
||||
|
||||
func TestModuleLineinfile_Good_RegexpFallsBackToAppend(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
// Simulate sed returning non-zero (pattern not found)
|
||||
mock.expectCommand("sed -i", "", "", 1)
|
||||
|
||||
result, err := moduleLineinfileWithClient(e, mock, map[string]any{
|
||||
"path": "/etc/config",
|
||||
"regexp": "^setting=",
|
||||
"line": "setting=value",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
// Should have attempted sed, then fallen back to echo append
|
||||
cmds := mock.executedCommands()
|
||||
assert.GreaterOrEqual(t, len(cmds), 2)
|
||||
assert.True(t, mock.hasExecuted(`echo`))
|
||||
}
|
||||
|
||||
func TestModuleLineinfile_Bad_MissingPath(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
_, err := moduleLineinfileWithClient(e, mock, map[string]any{
|
||||
"line": "test",
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "path required")
|
||||
}
|
||||
|
||||
func TestModuleLineinfile_Good_DestAliasForPath(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleLineinfileWithClient(e, mock, map[string]any{
|
||||
"dest": "/etc/config",
|
||||
"line": "key=value",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.True(t, mock.hasExecuted(`/etc/config`))
|
||||
}
|
||||
|
||||
func TestModuleLineinfile_Good_AbsentWithNoRegexp(t *testing.T) {
|
||||
// When state=absent but no regexp, nothing happens (no commands)
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleLineinfileWithClient(e, mock, map[string]any{
|
||||
"path": "/etc/config",
|
||||
"state": "absent",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.Equal(t, 0, mock.commandCount())
|
||||
}
|
||||
|
||||
func TestModuleLineinfile_Good_LineWithSlashes(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleLineinfileWithClient(e, mock, map[string]any{
|
||||
"path": "/etc/nginx/conf.d/default.conf",
|
||||
"regexp": "^root /",
|
||||
"line": "root /var/www/html;",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
// Slashes in the line should be escaped
|
||||
assert.True(t, mock.hasExecuted(`root \\/var\\/www\\/html;`))
|
||||
}
|
||||
|
||||
// --- blockinfile module ---
|
||||
|
||||
func TestModuleBlockinfile_Good_InsertBlock(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleBlockinfileWithClient(e, mock, map[string]any{
|
||||
"path": "/etc/nginx/conf.d/upstream.conf",
|
||||
"block": "server 10.0.0.1:8080;\nserver 10.0.0.2:8080;",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
// Should use RunScript for the heredoc approach
|
||||
assert.True(t, mock.hasExecutedMethod("RunScript", "BEGIN ANSIBLE MANAGED BLOCK"))
|
||||
assert.True(t, mock.hasExecutedMethod("RunScript", "END ANSIBLE MANAGED BLOCK"))
|
||||
assert.True(t, mock.hasExecutedMethod("RunScript", "10\\.0\\.0\\.1"))
|
||||
}
|
||||
|
||||
func TestModuleBlockinfile_Good_CustomMarkers(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleBlockinfileWithClient(e, mock, map[string]any{
|
||||
"path": "/etc/hosts",
|
||||
"block": "10.0.0.5 db01",
|
||||
"marker": "# {mark} managed by devops",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
// Should use custom markers instead of default
|
||||
assert.True(t, mock.hasExecutedMethod("RunScript", "# BEGIN managed by devops"))
|
||||
assert.True(t, mock.hasExecutedMethod("RunScript", "# END managed by devops"))
|
||||
}
|
||||
|
||||
func TestModuleBlockinfile_Good_RemoveBlock(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleBlockinfileWithClient(e, mock, map[string]any{
|
||||
"path": "/etc/config",
|
||||
"state": "absent",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
// Should use sed to remove the block between markers
|
||||
assert.True(t, mock.hasExecuted(`sed -i '/.*BEGIN ANSIBLE MANAGED BLOCK/,/.*END ANSIBLE MANAGED BLOCK/d'`))
|
||||
}
|
||||
|
||||
func TestModuleBlockinfile_Good_CreateFile(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleBlockinfileWithClient(e, mock, map[string]any{
|
||||
"path": "/etc/new-config",
|
||||
"block": "setting=value",
|
||||
"create": true,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
// Should touch the file first when create=true
|
||||
assert.True(t, mock.hasExecuted(`touch "/etc/new-config"`))
|
||||
}
|
||||
|
||||
func TestModuleBlockinfile_Bad_MissingPath(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
_, err := moduleBlockinfileWithClient(e, mock, map[string]any{
|
||||
"block": "content",
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "path required")
|
||||
}
|
||||
|
||||
func TestModuleBlockinfile_Good_DestAliasForPath(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleBlockinfileWithClient(e, mock, map[string]any{
|
||||
"dest": "/etc/config",
|
||||
"block": "data",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
}
|
||||
|
||||
func TestModuleBlockinfile_Good_ScriptFailure(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("BLOCK_EOF", "", "write error", 1)
|
||||
|
||||
result, err := moduleBlockinfileWithClient(e, mock, map[string]any{
|
||||
"path": "/etc/config",
|
||||
"block": "data",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Failed)
|
||||
assert.Contains(t, result.Msg, "write error")
|
||||
}
|
||||
|
||||
// --- stat module ---
|
||||
|
||||
func TestModuleStat_Good_ExistingFile(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.addStat("/etc/nginx/nginx.conf", map[string]any{
|
||||
"exists": true,
|
||||
"isdir": false,
|
||||
"mode": "0644",
|
||||
"size": 1234,
|
||||
"uid": 0,
|
||||
"gid": 0,
|
||||
})
|
||||
|
||||
result, err := moduleStatWithClient(e, mock, map[string]any{
|
||||
"path": "/etc/nginx/nginx.conf",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.False(t, result.Changed) // stat never changes anything
|
||||
require.NotNil(t, result.Data)
|
||||
|
||||
stat, ok := result.Data["stat"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, true, stat["exists"])
|
||||
assert.Equal(t, false, stat["isdir"])
|
||||
assert.Equal(t, "0644", stat["mode"])
|
||||
assert.Equal(t, 1234, stat["size"])
|
||||
}
|
||||
|
||||
func TestModuleStat_Good_MissingFile(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleStatWithClient(e, mock, map[string]any{
|
||||
"path": "/nonexistent/file.txt",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.False(t, result.Changed)
|
||||
require.NotNil(t, result.Data)
|
||||
|
||||
stat, ok := result.Data["stat"].(map[string]any)
|
||||
require.True(t, ok)
|
||||
assert.Equal(t, false, stat["exists"])
|
||||
}
|
||||
|
||||
func TestModuleStat_Good_Directory(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.addStat("/var/log", map[string]any{
|
||||
"exists": true,
|
||||
"isdir": true,
|
||||
"mode": "0755",
|
||||
})
|
||||
|
||||
result, err := moduleStatWithClient(e, mock, map[string]any{
|
||||
"path": "/var/log",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
stat := result.Data["stat"].(map[string]any)
|
||||
assert.Equal(t, true, stat["exists"])
|
||||
assert.Equal(t, true, stat["isdir"])
|
||||
}
|
||||
|
||||
func TestModuleStat_Good_FallbackFromFileSystem(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
// No explicit stat, but add a file — stat falls back to file existence
|
||||
mock.addFile("/etc/hosts", []byte("127.0.0.1 localhost"))
|
||||
|
||||
result, err := moduleStatWithClient(e, mock, map[string]any{
|
||||
"path": "/etc/hosts",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
stat := result.Data["stat"].(map[string]any)
|
||||
assert.Equal(t, true, stat["exists"])
|
||||
assert.Equal(t, false, stat["isdir"])
|
||||
}
|
||||
|
||||
func TestModuleStat_Bad_MissingPath(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
_, err := moduleStatWithClient(e, mock, map[string]any{})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "path required")
|
||||
}
|
||||
|
||||
// --- template module ---
|
||||
|
||||
func TestModuleTemplate_Good_BasicTemplate(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
srcPath := filepath.Join(tmpDir, "app.conf.j2")
|
||||
require.NoError(t, os.WriteFile(srcPath, []byte("server_name={{ server_name }};"), 0644))
|
||||
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
e.SetVar("server_name", "web01.example.com")
|
||||
|
||||
result, err := moduleTemplateWithClient(e, mock, map[string]any{
|
||||
"src": srcPath,
|
||||
"dest": "/etc/nginx/conf.d/app.conf",
|
||||
}, "host1", &Task{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.Contains(t, result.Msg, "templated to /etc/nginx/conf.d/app.conf")
|
||||
|
||||
// Verify upload was performed with templated content
|
||||
assert.Equal(t, 1, mock.uploadCount())
|
||||
up := mock.lastUpload()
|
||||
require.NotNil(t, up)
|
||||
assert.Equal(t, "/etc/nginx/conf.d/app.conf", up.Remote)
|
||||
// Template replaces {{ var }} — the TemplateFile does Jinja2 to Go conversion
|
||||
assert.Contains(t, string(up.Content), "web01.example.com")
|
||||
}
|
||||
|
||||
func TestModuleTemplate_Good_CustomMode(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
srcPath := filepath.Join(tmpDir, "script.sh.j2")
|
||||
require.NoError(t, os.WriteFile(srcPath, []byte("#!/bin/bash\necho done"), 0644))
|
||||
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleTemplateWithClient(e, mock, map[string]any{
|
||||
"src": srcPath,
|
||||
"dest": "/usr/local/bin/run.sh",
|
||||
"mode": "0755",
|
||||
}, "host1", &Task{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
up := mock.lastUpload()
|
||||
require.NotNil(t, up)
|
||||
assert.Equal(t, os.FileMode(0755), up.Mode)
|
||||
}
|
||||
|
||||
func TestModuleTemplate_Bad_MissingSrc(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
_, err := moduleTemplateWithClient(e, mock, map[string]any{
|
||||
"dest": "/tmp/out",
|
||||
}, "host1", &Task{})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "src and dest required")
|
||||
}
|
||||
|
||||
func TestModuleTemplate_Bad_MissingDest(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
_, err := moduleTemplateWithClient(e, mock, map[string]any{
|
||||
"src": "/tmp/in.j2",
|
||||
}, "host1", &Task{})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "src and dest required")
|
||||
}
|
||||
|
||||
func TestModuleTemplate_Bad_SrcFileNotFound(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
_, err := moduleTemplateWithClient(e, mock, map[string]any{
|
||||
"src": "/nonexistent/template.j2",
|
||||
"dest": "/tmp/out",
|
||||
}, "host1", &Task{})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "template")
|
||||
}
|
||||
|
||||
func TestModuleTemplate_Good_PlainTextNoVars(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
srcPath := filepath.Join(tmpDir, "static.conf")
|
||||
content := "listen 80;\nserver_name localhost;"
|
||||
require.NoError(t, os.WriteFile(srcPath, []byte(content), 0644))
|
||||
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
result, err := moduleTemplateWithClient(e, mock, map[string]any{
|
||||
"src": srcPath,
|
||||
"dest": "/etc/config",
|
||||
}, "host1", &Task{})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
up := mock.lastUpload()
|
||||
require.NotNil(t, up)
|
||||
assert.Equal(t, content, string(up.Content))
|
||||
}
|
||||
|
||||
// --- Cross-module dispatch tests for file modules ---
|
||||
|
||||
func TestExecuteModuleWithMock_Good_DispatchCopy(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
task := &Task{
|
||||
Module: "copy",
|
||||
Args: map[string]any{
|
||||
"content": "hello world",
|
||||
"dest": "/tmp/hello.txt",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := executeModuleWithMock(e, mock, "host1", task)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.Equal(t, 1, mock.uploadCount())
|
||||
}
|
||||
|
||||
func TestExecuteModuleWithMock_Good_DispatchFile(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
task := &Task{
|
||||
Module: "file",
|
||||
Args: map[string]any{
|
||||
"path": "/opt/data",
|
||||
"state": "directory",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := executeModuleWithMock(e, mock, "host1", task)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.True(t, mock.hasExecuted("mkdir"))
|
||||
}
|
||||
|
||||
func TestExecuteModuleWithMock_Good_DispatchStat(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.addStat("/etc/hosts", map[string]any{"exists": true, "isdir": false})
|
||||
|
||||
task := &Task{
|
||||
Module: "stat",
|
||||
Args: map[string]any{
|
||||
"path": "/etc/hosts",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := executeModuleWithMock(e, mock, "host1", task)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.False(t, result.Changed)
|
||||
stat := result.Data["stat"].(map[string]any)
|
||||
assert.Equal(t, true, stat["exists"])
|
||||
}
|
||||
|
||||
func TestExecuteModuleWithMock_Good_DispatchLineinfile(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
task := &Task{
|
||||
Module: "lineinfile",
|
||||
Args: map[string]any{
|
||||
"path": "/etc/hosts",
|
||||
"line": "10.0.0.1 dbhost",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := executeModuleWithMock(e, mock, "host1", task)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
}
|
||||
|
||||
func TestExecuteModuleWithMock_Good_DispatchBlockinfile(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
task := &Task{
|
||||
Module: "blockinfile",
|
||||
Args: map[string]any{
|
||||
"path": "/etc/config",
|
||||
"block": "key=value",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := executeModuleWithMock(e, mock, "host1", task)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
}
|
||||
|
||||
func TestExecuteModuleWithMock_Good_DispatchTemplate(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
srcPath := filepath.Join(tmpDir, "test.j2")
|
||||
require.NoError(t, os.WriteFile(srcPath, []byte("static content"), 0644))
|
||||
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
task := &Task{
|
||||
Module: "template",
|
||||
Args: map[string]any{
|
||||
"src": srcPath,
|
||||
"dest": "/etc/out.conf",
|
||||
},
|
||||
}
|
||||
|
||||
result, err := executeModuleWithMock(e, mock, "host1", task)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.Equal(t, 1, mock.uploadCount())
|
||||
}
|
||||
|
||||
// --- Template variable resolution integration ---
|
||||
|
||||
func TestModuleCopy_Good_TemplatedArgs(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
e.SetVar("deploy_path", "/opt/myapp")
|
||||
|
||||
task := &Task{
|
||||
Module: "copy",
|
||||
Args: map[string]any{
|
||||
"content": "deployed",
|
||||
"dest": "{{ deploy_path }}/config.yml",
|
||||
},
|
||||
}
|
||||
|
||||
// Template the args as the executor does
|
||||
args := e.templateArgs(task.Args, "host1", task)
|
||||
result, err := moduleCopyWithClient(e, mock, args, "host1", task)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
up := mock.lastUpload()
|
||||
require.NotNil(t, up)
|
||||
assert.Equal(t, "/opt/myapp/config.yml", up.Remote)
|
||||
}
|
||||
|
||||
func TestModuleFile_Good_TemplatedPath(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
e.SetVar("app_dir", "/var/www/html")
|
||||
|
||||
task := &Task{
|
||||
Module: "file",
|
||||
Args: map[string]any{
|
||||
"path": "{{ app_dir }}/uploads",
|
||||
"state": "directory",
|
||||
"owner": "www-data",
|
||||
},
|
||||
}
|
||||
|
||||
args := e.templateArgs(task.Args, "host1", task)
|
||||
result, err := moduleFileWithClient(e, mock, args)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.True(t, mock.hasExecuted(`mkdir -p "/var/www/html/uploads"`))
|
||||
assert.True(t, mock.hasExecuted(`chown www-data "/var/www/html/uploads"`))
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue