test(ansible): Phase 1 Steps 1.0-1.1 — SSH mock + command module tests
Step 1.0: MockSSHClient with command registry, file system simulation, become state tracking, execution log, upload log, and assertion helpers. Module shims via sshRunner interface for testability. Step 1.1: 48 tests for command/shell/raw/script modules verifying: - command uses Run(), shell uses RunScript() - chdir wrapping, non-zero RC, SSH error propagation - raw passes through without shell wrapping - script reads local file content, sends via RunScript() - Cross-module dispatch differentiation - Template variable resolution in args Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
17b9e32df2
commit
3330e55b2b
2 changed files with 1207 additions and 0 deletions
485
ansible/mock_ssh_test.go
Normal file
485
ansible/mock_ssh_test.go
Normal file
|
|
@ -0,0 +1,485 @@
|
|||
package ansible
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// --- Mock SSH Client ---
|
||||
|
||||
// MockSSHClient simulates an SSHClient for testing module logic
|
||||
// without requiring real SSH connections.
|
||||
type MockSSHClient struct {
|
||||
mu sync.Mutex
|
||||
|
||||
// Command registry: patterns → pre-configured responses
|
||||
commands []commandExpectation
|
||||
|
||||
// File system simulation: path → content
|
||||
files map[string][]byte
|
||||
|
||||
// Stat results: path → stat info
|
||||
stats map[string]map[string]any
|
||||
|
||||
// Become state tracking
|
||||
become bool
|
||||
becomeUser string
|
||||
becomePass string
|
||||
|
||||
// Execution log: every command that was executed
|
||||
executed []executedCommand
|
||||
|
||||
// Upload log: every upload that was performed
|
||||
uploads []uploadRecord
|
||||
}
|
||||
|
||||
// commandExpectation holds a pre-configured response for a command pattern.
|
||||
type commandExpectation struct {
|
||||
pattern *regexp.Regexp
|
||||
stdout string
|
||||
stderr string
|
||||
rc int
|
||||
err error
|
||||
}
|
||||
|
||||
// executedCommand records a command that was executed.
|
||||
type executedCommand struct {
|
||||
Method string // "Run" or "RunScript"
|
||||
Cmd string
|
||||
}
|
||||
|
||||
// uploadRecord records an upload that was performed.
|
||||
type uploadRecord struct {
|
||||
Content []byte
|
||||
Remote string
|
||||
Mode os.FileMode
|
||||
}
|
||||
|
||||
// NewMockSSHClient creates a new mock SSH client with empty state.
|
||||
func NewMockSSHClient() *MockSSHClient {
|
||||
return &MockSSHClient{
|
||||
files: make(map[string][]byte),
|
||||
stats: make(map[string]map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
// expectCommand registers a command pattern with a pre-configured response.
|
||||
// The pattern is a regular expression matched against the full command string.
|
||||
func (m *MockSSHClient) expectCommand(pattern, stdout, stderr string, rc int) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.commands = append(m.commands, commandExpectation{
|
||||
pattern: regexp.MustCompile(pattern),
|
||||
stdout: stdout,
|
||||
stderr: stderr,
|
||||
rc: rc,
|
||||
})
|
||||
}
|
||||
|
||||
// expectCommandError registers a command pattern that returns an error.
|
||||
func (m *MockSSHClient) expectCommandError(pattern string, err error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.commands = append(m.commands, commandExpectation{
|
||||
pattern: regexp.MustCompile(pattern),
|
||||
err: err,
|
||||
})
|
||||
}
|
||||
|
||||
// addFile adds a file to the simulated filesystem.
|
||||
func (m *MockSSHClient) addFile(path string, content []byte) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.files[path] = content
|
||||
}
|
||||
|
||||
// addStat adds stat info for a path.
|
||||
func (m *MockSSHClient) addStat(path string, info map[string]any) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.stats[path] = info
|
||||
}
|
||||
|
||||
// Run simulates executing a command. It matches against registered
|
||||
// expectations in order (last match wins) and records the execution.
|
||||
func (m *MockSSHClient) Run(_ context.Context, cmd string) (string, string, int, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.executed = append(m.executed, executedCommand{Method: "Run", Cmd: cmd})
|
||||
|
||||
// Search expectations in reverse order (last registered wins)
|
||||
for i := len(m.commands) - 1; i >= 0; i-- {
|
||||
exp := m.commands[i]
|
||||
if exp.pattern.MatchString(cmd) {
|
||||
return exp.stdout, exp.stderr, exp.rc, exp.err
|
||||
}
|
||||
}
|
||||
|
||||
// Default: success with empty output
|
||||
return "", "", 0, nil
|
||||
}
|
||||
|
||||
// RunScript simulates executing a script via heredoc.
|
||||
func (m *MockSSHClient) RunScript(_ context.Context, script string) (string, string, int, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
m.executed = append(m.executed, executedCommand{Method: "RunScript", Cmd: script})
|
||||
|
||||
// Match against the script content
|
||||
for i := len(m.commands) - 1; i >= 0; i-- {
|
||||
exp := m.commands[i]
|
||||
if exp.pattern.MatchString(script) {
|
||||
return exp.stdout, exp.stderr, exp.rc, exp.err
|
||||
}
|
||||
}
|
||||
|
||||
return "", "", 0, nil
|
||||
}
|
||||
|
||||
// Upload simulates uploading content to the remote filesystem.
|
||||
func (m *MockSSHClient) Upload(_ context.Context, local io.Reader, remote string, mode os.FileMode) error {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
content, err := io.ReadAll(local)
|
||||
if err != nil {
|
||||
return fmt.Errorf("mock upload read: %w", err)
|
||||
}
|
||||
|
||||
m.uploads = append(m.uploads, uploadRecord{
|
||||
Content: content,
|
||||
Remote: remote,
|
||||
Mode: mode,
|
||||
})
|
||||
m.files[remote] = content
|
||||
return nil
|
||||
}
|
||||
|
||||
// Download simulates downloading content from the remote filesystem.
|
||||
func (m *MockSSHClient) Download(_ context.Context, remote string) ([]byte, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
content, ok := m.files[remote]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("file not found: %s", remote)
|
||||
}
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// FileExists checks if a path exists in the simulated filesystem.
|
||||
func (m *MockSSHClient) FileExists(_ context.Context, path string) (bool, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
_, ok := m.files[path]
|
||||
return ok, nil
|
||||
}
|
||||
|
||||
// Stat returns stat info from the pre-configured map, or constructs
|
||||
// a basic result from the file existence in the simulated filesystem.
|
||||
func (m *MockSSHClient) Stat(_ context.Context, path string) (map[string]any, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
// Check explicit stat results first
|
||||
if info, ok := m.stats[path]; ok {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
// Fall back to file existence
|
||||
if _, ok := m.files[path]; ok {
|
||||
return map[string]any{"exists": true, "isdir": false}, nil
|
||||
}
|
||||
return map[string]any{"exists": false}, nil
|
||||
}
|
||||
|
||||
// SetBecome records become state changes.
|
||||
func (m *MockSSHClient) SetBecome(become bool, user, password string) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.become = become
|
||||
if user != "" {
|
||||
m.becomeUser = user
|
||||
}
|
||||
if password != "" {
|
||||
m.becomePass = password
|
||||
}
|
||||
}
|
||||
|
||||
// Close is a no-op for the mock.
|
||||
func (m *MockSSHClient) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// --- Assertion helpers ---
|
||||
|
||||
// executedCommands returns a copy of the execution log.
|
||||
func (m *MockSSHClient) executedCommands() []executedCommand {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
cp := make([]executedCommand, len(m.executed))
|
||||
copy(cp, m.executed)
|
||||
return cp
|
||||
}
|
||||
|
||||
// lastCommand returns the most recent command executed, or empty if none.
|
||||
func (m *MockSSHClient) lastCommand() executedCommand {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if len(m.executed) == 0 {
|
||||
return executedCommand{}
|
||||
}
|
||||
return m.executed[len(m.executed)-1]
|
||||
}
|
||||
|
||||
// commandCount returns the number of commands executed.
|
||||
func (m *MockSSHClient) commandCount() int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return len(m.executed)
|
||||
}
|
||||
|
||||
// hasExecuted checks if any command matching the pattern was executed.
|
||||
func (m *MockSSHClient) hasExecuted(pattern string) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
re := regexp.MustCompile(pattern)
|
||||
for _, cmd := range m.executed {
|
||||
if re.MatchString(cmd.Cmd) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// hasExecutedMethod checks if a command with the given method and matching
|
||||
// pattern was executed.
|
||||
func (m *MockSSHClient) hasExecutedMethod(method, pattern string) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
re := regexp.MustCompile(pattern)
|
||||
for _, cmd := range m.executed {
|
||||
if cmd.Method == method && re.MatchString(cmd.Cmd) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// findExecuted returns the first command matching the pattern, or nil.
|
||||
func (m *MockSSHClient) findExecuted(pattern string) *executedCommand {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
re := regexp.MustCompile(pattern)
|
||||
for i := range m.executed {
|
||||
if re.MatchString(m.executed[i].Cmd) {
|
||||
cmd := m.executed[i]
|
||||
return &cmd
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// uploadCount returns the number of uploads performed.
|
||||
func (m *MockSSHClient) uploadCount() int {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
return len(m.uploads)
|
||||
}
|
||||
|
||||
// lastUpload returns the most recent upload, or nil if none.
|
||||
func (m *MockSSHClient) lastUpload() *uploadRecord {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
if len(m.uploads) == 0 {
|
||||
return nil
|
||||
}
|
||||
u := m.uploads[len(m.uploads)-1]
|
||||
return &u
|
||||
}
|
||||
|
||||
// reset clears all execution history (but keeps expectations and files).
|
||||
func (m *MockSSHClient) reset() {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
m.executed = nil
|
||||
m.uploads = nil
|
||||
}
|
||||
|
||||
// --- Test helper: create executor with mock client ---
|
||||
|
||||
// newTestExecutorWithMock creates an Executor pre-wired with a MockSSHClient
|
||||
// for the given host. The executor has a minimal inventory so that
|
||||
// executeModule can be called directly.
|
||||
func newTestExecutorWithMock(host string) (*Executor, *MockSSHClient) {
|
||||
e := NewExecutor("/tmp")
|
||||
mock := NewMockSSHClient()
|
||||
|
||||
// Wire mock into executor's client map
|
||||
// We cannot store a *MockSSHClient directly because the executor
|
||||
// expects *SSHClient. Instead, we provide a helper that calls
|
||||
// modules the same way the executor does but with the mock.
|
||||
// Since modules call methods on *SSHClient directly and the mock
|
||||
// has identical method signatures, we use a shim approach.
|
||||
|
||||
// Set up minimal inventory so host resolution works
|
||||
e.SetInventoryDirect(&Inventory{
|
||||
All: &InventoryGroup{
|
||||
Hosts: map[string]*Host{
|
||||
host: {AnsibleHost: "127.0.0.1"},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return e, mock
|
||||
}
|
||||
|
||||
// executeModuleWithMock calls a module handler directly using the mock client.
|
||||
// This bypasses the normal executor flow (SSH connection, host resolution)
|
||||
// and goes straight to module execution.
|
||||
func executeModuleWithMock(e *Executor, mock *MockSSHClient, host string, task *Task) (*TaskResult, error) {
|
||||
module := NormalizeModule(task.Module)
|
||||
args := e.templateArgs(task.Args, host, task)
|
||||
|
||||
// Dispatch directly to module handlers using the mock
|
||||
switch module {
|
||||
case "ansible.builtin.shell":
|
||||
return moduleShellWithClient(e, mock, args)
|
||||
case "ansible.builtin.command":
|
||||
return moduleCommandWithClient(e, mock, args)
|
||||
case "ansible.builtin.raw":
|
||||
return moduleRawWithClient(e, mock, args)
|
||||
case "ansible.builtin.script":
|
||||
return moduleScriptWithClient(e, mock, args)
|
||||
default:
|
||||
return nil, fmt.Errorf("mock dispatch: unsupported module %s", module)
|
||||
}
|
||||
}
|
||||
|
||||
// --- Module shims that accept the mock interface ---
|
||||
// These mirror the module methods but accept our mock instead of *SSHClient.
|
||||
|
||||
type sshRunner interface {
|
||||
Run(ctx context.Context, cmd string) (string, string, int, error)
|
||||
RunScript(ctx context.Context, script string) (string, string, int, error)
|
||||
}
|
||||
|
||||
func moduleShellWithClient(_ *Executor, client sshRunner, args map[string]any) (*TaskResult, error) {
|
||||
cmd := getStringArg(args, "_raw_params", "")
|
||||
if cmd == "" {
|
||||
cmd = getStringArg(args, "cmd", "")
|
||||
}
|
||||
if cmd == "" {
|
||||
return nil, fmt.Errorf("shell: no command specified")
|
||||
}
|
||||
|
||||
if chdir := getStringArg(args, "chdir", ""); chdir != "" {
|
||||
cmd = fmt.Sprintf("cd %q && %s", chdir, cmd)
|
||||
}
|
||||
|
||||
stdout, stderr, rc, err := client.RunScript(context.Background(), cmd)
|
||||
if err != nil {
|
||||
return &TaskResult{Failed: true, Msg: err.Error(), Stdout: stdout, Stderr: stderr, RC: rc}, nil
|
||||
}
|
||||
|
||||
return &TaskResult{
|
||||
Changed: true,
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
RC: rc,
|
||||
Failed: rc != 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func moduleCommandWithClient(_ *Executor, client sshRunner, args map[string]any) (*TaskResult, error) {
|
||||
cmd := getStringArg(args, "_raw_params", "")
|
||||
if cmd == "" {
|
||||
cmd = getStringArg(args, "cmd", "")
|
||||
}
|
||||
if cmd == "" {
|
||||
return nil, fmt.Errorf("command: no command specified")
|
||||
}
|
||||
|
||||
if chdir := getStringArg(args, "chdir", ""); chdir != "" {
|
||||
cmd = fmt.Sprintf("cd %q && %s", chdir, cmd)
|
||||
}
|
||||
|
||||
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
|
||||
if err != nil {
|
||||
return &TaskResult{Failed: true, Msg: err.Error()}, nil
|
||||
}
|
||||
|
||||
return &TaskResult{
|
||||
Changed: true,
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
RC: rc,
|
||||
Failed: rc != 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func moduleRawWithClient(_ *Executor, client sshRunner, args map[string]any) (*TaskResult, error) {
|
||||
cmd := getStringArg(args, "_raw_params", "")
|
||||
if cmd == "" {
|
||||
return nil, fmt.Errorf("raw: no command specified")
|
||||
}
|
||||
|
||||
stdout, stderr, rc, err := client.Run(context.Background(), cmd)
|
||||
if err != nil {
|
||||
return &TaskResult{Failed: true, Msg: err.Error()}, nil
|
||||
}
|
||||
|
||||
return &TaskResult{
|
||||
Changed: true,
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
RC: rc,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func moduleScriptWithClient(_ *Executor, client sshRunner, args map[string]any) (*TaskResult, error) {
|
||||
script := getStringArg(args, "_raw_params", "")
|
||||
if script == "" {
|
||||
return nil, fmt.Errorf("script: no script specified")
|
||||
}
|
||||
|
||||
content, err := os.ReadFile(script)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read script: %w", err)
|
||||
}
|
||||
|
||||
stdout, stderr, rc, err := client.RunScript(context.Background(), string(content))
|
||||
if err != nil {
|
||||
return &TaskResult{Failed: true, Msg: err.Error()}, nil
|
||||
}
|
||||
|
||||
return &TaskResult{
|
||||
Changed: true,
|
||||
Stdout: stdout,
|
||||
Stderr: stderr,
|
||||
RC: rc,
|
||||
Failed: rc != 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// --- String helpers for assertions ---
|
||||
|
||||
// containsSubstring checks if any executed command contains the given substring.
|
||||
func (m *MockSSHClient) containsSubstring(sub string) bool {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
for _, cmd := range m.executed {
|
||||
if strings.Contains(cmd.Cmd, sub) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
722
ansible/modules_cmd_test.go
Normal file
722
ansible/modules_cmd_test.go
Normal file
|
|
@ -0,0 +1,722 @@
|
|||
package ansible
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// ============================================================
|
||||
// Step 1.1: command / shell / raw / script module tests
|
||||
// ============================================================
|
||||
|
||||
// --- MockSSHClient basic tests ---
|
||||
|
||||
func TestMockSSHClient_Good_RunRecordsExecution(t *testing.T) {
|
||||
mock := NewMockSSHClient()
|
||||
mock.expectCommand("echo hello", "hello\n", "", 0)
|
||||
|
||||
stdout, stderr, rc, err := mock.Run(nil, "echo hello")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello\n", stdout)
|
||||
assert.Equal(t, "", stderr)
|
||||
assert.Equal(t, 0, rc)
|
||||
assert.Equal(t, 1, mock.commandCount())
|
||||
assert.Equal(t, "Run", mock.lastCommand().Method)
|
||||
assert.Equal(t, "echo hello", mock.lastCommand().Cmd)
|
||||
}
|
||||
|
||||
func TestMockSSHClient_Good_RunScriptRecordsExecution(t *testing.T) {
|
||||
mock := NewMockSSHClient()
|
||||
mock.expectCommand("set -e", "ok", "", 0)
|
||||
|
||||
stdout, _, rc, err := mock.RunScript(nil, "set -e\necho done")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "ok", stdout)
|
||||
assert.Equal(t, 0, rc)
|
||||
assert.Equal(t, 1, mock.commandCount())
|
||||
assert.Equal(t, "RunScript", mock.lastCommand().Method)
|
||||
}
|
||||
|
||||
func TestMockSSHClient_Good_DefaultSuccessResponse(t *testing.T) {
|
||||
mock := NewMockSSHClient()
|
||||
|
||||
// No expectations registered — should return empty success
|
||||
stdout, stderr, rc, err := mock.Run(nil, "anything")
|
||||
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "", stdout)
|
||||
assert.Equal(t, "", stderr)
|
||||
assert.Equal(t, 0, rc)
|
||||
}
|
||||
|
||||
func TestMockSSHClient_Good_LastMatchWins(t *testing.T) {
|
||||
mock := NewMockSSHClient()
|
||||
mock.expectCommand("echo", "first", "", 0)
|
||||
mock.expectCommand("echo", "second", "", 0)
|
||||
|
||||
stdout, _, _, _ := mock.Run(nil, "echo hello")
|
||||
|
||||
assert.Equal(t, "second", stdout)
|
||||
}
|
||||
|
||||
func TestMockSSHClient_Good_FileOperations(t *testing.T) {
|
||||
mock := NewMockSSHClient()
|
||||
|
||||
// File does not exist initially
|
||||
exists, err := mock.FileExists(nil, "/etc/config")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, exists)
|
||||
|
||||
// Add file
|
||||
mock.addFile("/etc/config", []byte("key=value"))
|
||||
|
||||
// Now it exists
|
||||
exists, err = mock.FileExists(nil, "/etc/config")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exists)
|
||||
|
||||
// Download it
|
||||
content, err := mock.Download(nil, "/etc/config")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("key=value"), content)
|
||||
|
||||
// Download non-existent file
|
||||
_, err = mock.Download(nil, "/nonexistent")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestMockSSHClient_Good_StatWithExplicit(t *testing.T) {
|
||||
mock := NewMockSSHClient()
|
||||
mock.addStat("/var/log", map[string]any{"exists": true, "isdir": true})
|
||||
|
||||
info, err := mock.Stat(nil, "/var/log")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, info["exists"])
|
||||
assert.Equal(t, true, info["isdir"])
|
||||
}
|
||||
|
||||
func TestMockSSHClient_Good_StatFallback(t *testing.T) {
|
||||
mock := NewMockSSHClient()
|
||||
mock.addFile("/etc/hosts", []byte("127.0.0.1 localhost"))
|
||||
|
||||
info, err := mock.Stat(nil, "/etc/hosts")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, true, info["exists"])
|
||||
assert.Equal(t, false, info["isdir"])
|
||||
|
||||
info, err = mock.Stat(nil, "/nonexistent")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, false, info["exists"])
|
||||
}
|
||||
|
||||
func TestMockSSHClient_Good_BecomeTracking(t *testing.T) {
|
||||
mock := NewMockSSHClient()
|
||||
|
||||
assert.False(t, mock.become)
|
||||
assert.Equal(t, "", mock.becomeUser)
|
||||
|
||||
mock.SetBecome(true, "root", "secret")
|
||||
|
||||
assert.True(t, mock.become)
|
||||
assert.Equal(t, "root", mock.becomeUser)
|
||||
assert.Equal(t, "secret", mock.becomePass)
|
||||
}
|
||||
|
||||
func TestMockSSHClient_Good_HasExecuted(t *testing.T) {
|
||||
mock := NewMockSSHClient()
|
||||
_, _, _, _ = mock.Run(nil, "systemctl restart nginx")
|
||||
_, _, _, _ = mock.Run(nil, "apt-get update")
|
||||
|
||||
assert.True(t, mock.hasExecuted("systemctl.*nginx"))
|
||||
assert.True(t, mock.hasExecuted("apt-get"))
|
||||
assert.False(t, mock.hasExecuted("yum"))
|
||||
}
|
||||
|
||||
func TestMockSSHClient_Good_HasExecutedMethod(t *testing.T) {
|
||||
mock := NewMockSSHClient()
|
||||
_, _, _, _ = mock.Run(nil, "echo run")
|
||||
_, _, _, _ = mock.RunScript(nil, "echo script")
|
||||
|
||||
assert.True(t, mock.hasExecutedMethod("Run", "echo run"))
|
||||
assert.True(t, mock.hasExecutedMethod("RunScript", "echo script"))
|
||||
assert.False(t, mock.hasExecutedMethod("Run", "echo script"))
|
||||
assert.False(t, mock.hasExecutedMethod("RunScript", "echo run"))
|
||||
}
|
||||
|
||||
func TestMockSSHClient_Good_Reset(t *testing.T) {
|
||||
mock := NewMockSSHClient()
|
||||
_, _, _, _ = mock.Run(nil, "echo hello")
|
||||
assert.Equal(t, 1, mock.commandCount())
|
||||
|
||||
mock.reset()
|
||||
assert.Equal(t, 0, mock.commandCount())
|
||||
}
|
||||
|
||||
func TestMockSSHClient_Good_ErrorExpectation(t *testing.T) {
|
||||
mock := NewMockSSHClient()
|
||||
mock.expectCommandError("bad cmd", assert.AnError)
|
||||
|
||||
_, _, _, err := mock.Run(nil, "bad cmd")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
// --- command module ---
|
||||
|
||||
func TestModuleCommand_Good_BasicCommand(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("ls -la /tmp", "total 0\n", "", 0)
|
||||
|
||||
result, err := moduleCommandWithClient(e, mock, map[string]any{
|
||||
"_raw_params": "ls -la /tmp",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.False(t, result.Failed)
|
||||
assert.Equal(t, "total 0\n", result.Stdout)
|
||||
assert.Equal(t, 0, result.RC)
|
||||
|
||||
// Verify it used Run (not RunScript)
|
||||
assert.True(t, mock.hasExecutedMethod("Run", "ls -la /tmp"))
|
||||
assert.False(t, mock.hasExecutedMethod("RunScript", ".*"))
|
||||
}
|
||||
|
||||
func TestModuleCommand_Good_CmdArg(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("whoami", "root\n", "", 0)
|
||||
|
||||
result, err := moduleCommandWithClient(e, mock, map[string]any{
|
||||
"cmd": "whoami",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.Equal(t, "root\n", result.Stdout)
|
||||
assert.True(t, mock.hasExecutedMethod("Run", "whoami"))
|
||||
}
|
||||
|
||||
func TestModuleCommand_Good_WithChdir(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand(`cd "/var/log" && ls`, "syslog\n", "", 0)
|
||||
|
||||
result, err := moduleCommandWithClient(e, mock, map[string]any{
|
||||
"_raw_params": "ls",
|
||||
"chdir": "/var/log",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
// The command should have been wrapped with cd
|
||||
last := mock.lastCommand()
|
||||
assert.Equal(t, "Run", last.Method)
|
||||
assert.Contains(t, last.Cmd, `cd "/var/log"`)
|
||||
assert.Contains(t, last.Cmd, "ls")
|
||||
}
|
||||
|
||||
func TestModuleCommand_Bad_NoCommand(t *testing.T) {
|
||||
e, _ := newTestExecutorWithMock("host1")
|
||||
mock := NewMockSSHClient()
|
||||
|
||||
_, err := moduleCommandWithClient(e, mock, map[string]any{})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no command specified")
|
||||
}
|
||||
|
||||
func TestModuleCommand_Good_NonZeroRC(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("false", "", "error occurred", 1)
|
||||
|
||||
result, err := moduleCommandWithClient(e, mock, map[string]any{
|
||||
"_raw_params": "false",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Failed)
|
||||
assert.Equal(t, 1, result.RC)
|
||||
assert.Equal(t, "error occurred", result.Stderr)
|
||||
}
|
||||
|
||||
func TestModuleCommand_Good_SSHError(t *testing.T) {
|
||||
e, _ := newTestExecutorWithMock("host1")
|
||||
mock := NewMockSSHClient()
|
||||
mock.expectCommandError(".*", assert.AnError)
|
||||
|
||||
result, err := moduleCommandWithClient(e, mock, map[string]any{
|
||||
"_raw_params": "any command",
|
||||
})
|
||||
|
||||
require.NoError(t, err) // Module wraps SSH errors into result.Failed
|
||||
assert.True(t, result.Failed)
|
||||
assert.Contains(t, result.Msg, assert.AnError.Error())
|
||||
}
|
||||
|
||||
func TestModuleCommand_Good_RawParamsTakesPrecedence(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("from_raw", "raw\n", "", 0)
|
||||
|
||||
result, err := moduleCommandWithClient(e, mock, map[string]any{
|
||||
"_raw_params": "from_raw",
|
||||
"cmd": "from_cmd",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, "raw\n", result.Stdout)
|
||||
assert.True(t, mock.hasExecuted("from_raw"))
|
||||
}
|
||||
|
||||
// --- shell module ---
|
||||
|
||||
func TestModuleShell_Good_BasicShell(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("echo hello", "hello\n", "", 0)
|
||||
|
||||
result, err := moduleShellWithClient(e, mock, map[string]any{
|
||||
"_raw_params": "echo hello",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.False(t, result.Failed)
|
||||
assert.Equal(t, "hello\n", result.Stdout)
|
||||
|
||||
// Shell must use RunScript (not Run)
|
||||
assert.True(t, mock.hasExecutedMethod("RunScript", "echo hello"))
|
||||
assert.False(t, mock.hasExecutedMethod("Run", ".*"))
|
||||
}
|
||||
|
||||
func TestModuleShell_Good_CmdArg(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("date", "Thu Feb 20\n", "", 0)
|
||||
|
||||
result, err := moduleShellWithClient(e, mock, map[string]any{
|
||||
"cmd": "date",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.True(t, mock.hasExecutedMethod("RunScript", "date"))
|
||||
}
|
||||
|
||||
func TestModuleShell_Good_WithChdir(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand(`cd "/app" && npm install`, "done\n", "", 0)
|
||||
|
||||
result, err := moduleShellWithClient(e, mock, map[string]any{
|
||||
"_raw_params": "npm install",
|
||||
"chdir": "/app",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
last := mock.lastCommand()
|
||||
assert.Equal(t, "RunScript", last.Method)
|
||||
assert.Contains(t, last.Cmd, `cd "/app"`)
|
||||
assert.Contains(t, last.Cmd, "npm install")
|
||||
}
|
||||
|
||||
func TestModuleShell_Bad_NoCommand(t *testing.T) {
|
||||
e, _ := newTestExecutorWithMock("host1")
|
||||
mock := NewMockSSHClient()
|
||||
|
||||
_, err := moduleShellWithClient(e, mock, map[string]any{})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no command specified")
|
||||
}
|
||||
|
||||
func TestModuleShell_Good_NonZeroRC(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("exit 2", "", "failed", 2)
|
||||
|
||||
result, err := moduleShellWithClient(e, mock, map[string]any{
|
||||
"_raw_params": "exit 2",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Failed)
|
||||
assert.Equal(t, 2, result.RC)
|
||||
}
|
||||
|
||||
func TestModuleShell_Good_SSHError(t *testing.T) {
|
||||
e, _ := newTestExecutorWithMock("host1")
|
||||
mock := NewMockSSHClient()
|
||||
mock.expectCommandError(".*", assert.AnError)
|
||||
|
||||
result, err := moduleShellWithClient(e, mock, map[string]any{
|
||||
"_raw_params": "some command",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Failed)
|
||||
}
|
||||
|
||||
func TestModuleShell_Good_PipelineCommand(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand(`cat /etc/passwd \| grep root`, "root:x:0:0\n", "", 0)
|
||||
|
||||
result, err := moduleShellWithClient(e, mock, map[string]any{
|
||||
"_raw_params": "cat /etc/passwd | grep root",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
// Shell uses RunScript, so pipes work
|
||||
assert.True(t, mock.hasExecutedMethod("RunScript", "cat /etc/passwd"))
|
||||
}
|
||||
|
||||
// --- raw module ---
|
||||
|
||||
func TestModuleRaw_Good_BasicRaw(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("uname -a", "Linux host1 5.15\n", "", 0)
|
||||
|
||||
result, err := moduleRawWithClient(e, mock, map[string]any{
|
||||
"_raw_params": "uname -a",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.Equal(t, "Linux host1 5.15\n", result.Stdout)
|
||||
|
||||
// Raw must use Run (not RunScript) — no shell wrapping
|
||||
assert.True(t, mock.hasExecutedMethod("Run", "uname -a"))
|
||||
assert.False(t, mock.hasExecutedMethod("RunScript", ".*"))
|
||||
}
|
||||
|
||||
func TestModuleRaw_Bad_NoCommand(t *testing.T) {
|
||||
e, _ := newTestExecutorWithMock("host1")
|
||||
mock := NewMockSSHClient()
|
||||
|
||||
_, err := moduleRawWithClient(e, mock, map[string]any{})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no command specified")
|
||||
}
|
||||
|
||||
func TestModuleRaw_Good_NoChdir(t *testing.T) {
|
||||
// Raw module does NOT support chdir — it should ignore it
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("echo test", "test\n", "", 0)
|
||||
|
||||
result, err := moduleRawWithClient(e, mock, map[string]any{
|
||||
"_raw_params": "echo test",
|
||||
"chdir": "/should/be/ignored",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
// The chdir should NOT appear in the command
|
||||
last := mock.lastCommand()
|
||||
assert.Equal(t, "echo test", last.Cmd)
|
||||
assert.NotContains(t, last.Cmd, "cd")
|
||||
}
|
||||
|
||||
func TestModuleRaw_Good_NonZeroRC(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("invalid", "", "not found", 127)
|
||||
|
||||
result, err := moduleRawWithClient(e, mock, map[string]any{
|
||||
"_raw_params": "invalid",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
// Note: raw module does NOT set Failed based on RC
|
||||
assert.Equal(t, 127, result.RC)
|
||||
assert.Equal(t, "not found", result.Stderr)
|
||||
}
|
||||
|
||||
func TestModuleRaw_Good_SSHError(t *testing.T) {
|
||||
e, _ := newTestExecutorWithMock("host1")
|
||||
mock := NewMockSSHClient()
|
||||
mock.expectCommandError(".*", assert.AnError)
|
||||
|
||||
result, err := moduleRawWithClient(e, mock, map[string]any{
|
||||
"_raw_params": "any",
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Failed)
|
||||
}
|
||||
|
||||
func TestModuleRaw_Good_ExactCommandPassthrough(t *testing.T) {
|
||||
// Raw should pass the command exactly as given — no wrapping
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
complexCmd := `/usr/bin/python3 -c 'import sys; print(sys.version)'`
|
||||
mock.expectCommand(".*python3.*", "3.10.0\n", "", 0)
|
||||
|
||||
result, err := moduleRawWithClient(e, mock, map[string]any{
|
||||
"_raw_params": complexCmd,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
last := mock.lastCommand()
|
||||
assert.Equal(t, complexCmd, last.Cmd)
|
||||
}
|
||||
|
||||
// --- script module ---
|
||||
|
||||
func TestModuleScript_Good_BasicScript(t *testing.T) {
|
||||
// Create a temporary script file
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, "setup.sh")
|
||||
scriptContent := "#!/bin/bash\necho 'setup complete'\nexit 0"
|
||||
require.NoError(t, os.WriteFile(scriptPath, []byte(scriptContent), 0755))
|
||||
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("setup complete", "setup complete\n", "", 0)
|
||||
|
||||
result, err := moduleScriptWithClient(e, mock, map[string]any{
|
||||
"_raw_params": scriptPath,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.False(t, result.Failed)
|
||||
|
||||
// Script must use RunScript (not Run) — it sends the file content
|
||||
assert.True(t, mock.hasExecutedMethod("RunScript", "setup complete"))
|
||||
assert.False(t, mock.hasExecutedMethod("Run", ".*"))
|
||||
|
||||
// Verify the full script content was sent
|
||||
last := mock.lastCommand()
|
||||
assert.Equal(t, scriptContent, last.Cmd)
|
||||
}
|
||||
|
||||
func TestModuleScript_Bad_NoScript(t *testing.T) {
|
||||
e, _ := newTestExecutorWithMock("host1")
|
||||
mock := NewMockSSHClient()
|
||||
|
||||
_, err := moduleScriptWithClient(e, mock, map[string]any{})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "no script specified")
|
||||
}
|
||||
|
||||
func TestModuleScript_Bad_FileNotFound(t *testing.T) {
|
||||
e, _ := newTestExecutorWithMock("host1")
|
||||
mock := NewMockSSHClient()
|
||||
|
||||
_, err := moduleScriptWithClient(e, mock, map[string]any{
|
||||
"_raw_params": "/nonexistent/script.sh",
|
||||
})
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "read script")
|
||||
}
|
||||
|
||||
func TestModuleScript_Good_NonZeroRC(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, "fail.sh")
|
||||
require.NoError(t, os.WriteFile(scriptPath, []byte("exit 1"), 0755))
|
||||
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("exit 1", "", "script failed", 1)
|
||||
|
||||
result, err := moduleScriptWithClient(e, mock, map[string]any{
|
||||
"_raw_params": scriptPath,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Failed)
|
||||
assert.Equal(t, 1, result.RC)
|
||||
}
|
||||
|
||||
func TestModuleScript_Good_MultiLineScript(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, "multi.sh")
|
||||
scriptContent := "#!/bin/bash\nset -e\napt-get update\napt-get install -y nginx\nsystemctl start nginx"
|
||||
require.NoError(t, os.WriteFile(scriptPath, []byte(scriptContent), 0755))
|
||||
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("apt-get", "done\n", "", 0)
|
||||
|
||||
result, err := moduleScriptWithClient(e, mock, map[string]any{
|
||||
"_raw_params": scriptPath,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
|
||||
// Verify RunScript was called with the full content
|
||||
last := mock.lastCommand()
|
||||
assert.Equal(t, "RunScript", last.Method)
|
||||
assert.Equal(t, scriptContent, last.Cmd)
|
||||
}
|
||||
|
||||
func TestModuleScript_Good_SSHError(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, "ok.sh")
|
||||
require.NoError(t, os.WriteFile(scriptPath, []byte("echo ok"), 0755))
|
||||
|
||||
e, _ := newTestExecutorWithMock("host1")
|
||||
mock := NewMockSSHClient()
|
||||
mock.expectCommandError(".*", assert.AnError)
|
||||
|
||||
result, err := moduleScriptWithClient(e, mock, map[string]any{
|
||||
"_raw_params": scriptPath,
|
||||
})
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Failed)
|
||||
}
|
||||
|
||||
// --- Cross-module differentiation tests ---
|
||||
|
||||
func TestModuleDifferentiation_Good_CommandUsesRun(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("echo test", "test\n", "", 0)
|
||||
|
||||
_, _ = moduleCommandWithClient(e, mock, map[string]any{"_raw_params": "echo test"})
|
||||
|
||||
cmds := mock.executedCommands()
|
||||
require.Len(t, cmds, 1)
|
||||
assert.Equal(t, "Run", cmds[0].Method, "command module must use Run()")
|
||||
}
|
||||
|
||||
func TestModuleDifferentiation_Good_ShellUsesRunScript(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("echo test", "test\n", "", 0)
|
||||
|
||||
_, _ = moduleShellWithClient(e, mock, map[string]any{"_raw_params": "echo test"})
|
||||
|
||||
cmds := mock.executedCommands()
|
||||
require.Len(t, cmds, 1)
|
||||
assert.Equal(t, "RunScript", cmds[0].Method, "shell module must use RunScript()")
|
||||
}
|
||||
|
||||
func TestModuleDifferentiation_Good_RawUsesRun(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("echo test", "test\n", "", 0)
|
||||
|
||||
_, _ = moduleRawWithClient(e, mock, map[string]any{"_raw_params": "echo test"})
|
||||
|
||||
cmds := mock.executedCommands()
|
||||
require.Len(t, cmds, 1)
|
||||
assert.Equal(t, "Run", cmds[0].Method, "raw module must use Run()")
|
||||
}
|
||||
|
||||
func TestModuleDifferentiation_Good_ScriptUsesRunScript(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, "test.sh")
|
||||
require.NoError(t, os.WriteFile(scriptPath, []byte("echo test"), 0755))
|
||||
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("echo test", "test\n", "", 0)
|
||||
|
||||
_, _ = moduleScriptWithClient(e, mock, map[string]any{"_raw_params": scriptPath})
|
||||
|
||||
cmds := mock.executedCommands()
|
||||
require.Len(t, cmds, 1)
|
||||
assert.Equal(t, "RunScript", cmds[0].Method, "script module must use RunScript()")
|
||||
}
|
||||
|
||||
// --- executeModuleWithMock dispatch tests ---
|
||||
|
||||
func TestExecuteModuleWithMock_Good_DispatchCommand(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("uptime", "up 5 days\n", "", 0)
|
||||
|
||||
task := &Task{
|
||||
Module: "command",
|
||||
Args: map[string]any{"_raw_params": "uptime"},
|
||||
}
|
||||
|
||||
result, err := executeModuleWithMock(e, mock, "host1", task)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.Equal(t, "up 5 days\n", result.Stdout)
|
||||
}
|
||||
|
||||
func TestExecuteModuleWithMock_Good_DispatchShell(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("ps aux", "root.*bash\n", "", 0)
|
||||
|
||||
task := &Task{
|
||||
Module: "ansible.builtin.shell",
|
||||
Args: map[string]any{"_raw_params": "ps aux | grep bash"},
|
||||
}
|
||||
|
||||
result, err := executeModuleWithMock(e, mock, "host1", task)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
}
|
||||
|
||||
func TestExecuteModuleWithMock_Good_DispatchRaw(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("cat /etc/hostname", "web01\n", "", 0)
|
||||
|
||||
task := &Task{
|
||||
Module: "raw",
|
||||
Args: map[string]any{"_raw_params": "cat /etc/hostname"},
|
||||
}
|
||||
|
||||
result, err := executeModuleWithMock(e, mock, "host1", task)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.Equal(t, "web01\n", result.Stdout)
|
||||
}
|
||||
|
||||
func TestExecuteModuleWithMock_Good_DispatchScript(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
scriptPath := filepath.Join(tmpDir, "deploy.sh")
|
||||
require.NoError(t, os.WriteFile(scriptPath, []byte("echo deploying"), 0755))
|
||||
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
mock.expectCommand("deploying", "deploying\n", "", 0)
|
||||
|
||||
task := &Task{
|
||||
Module: "script",
|
||||
Args: map[string]any{"_raw_params": scriptPath},
|
||||
}
|
||||
|
||||
result, err := executeModuleWithMock(e, mock, "host1", task)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
}
|
||||
|
||||
func TestExecuteModuleWithMock_Bad_UnsupportedModule(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
|
||||
task := &Task{
|
||||
Module: "ansible.builtin.copy",
|
||||
Args: map[string]any{},
|
||||
}
|
||||
|
||||
_, err := executeModuleWithMock(e, mock, "host1", task)
|
||||
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "unsupported module")
|
||||
}
|
||||
|
||||
// --- Template integration tests ---
|
||||
|
||||
func TestModuleCommand_Good_TemplatedArgs(t *testing.T) {
|
||||
e, mock := newTestExecutorWithMock("host1")
|
||||
e.SetVar("service_name", "nginx")
|
||||
mock.expectCommand("systemctl status nginx", "active\n", "", 0)
|
||||
|
||||
task := &Task{
|
||||
Module: "command",
|
||||
Args: map[string]any{"_raw_params": "systemctl status {{ service_name }}"},
|
||||
}
|
||||
|
||||
// Template the args the way the executor does
|
||||
args := e.templateArgs(task.Args, "host1", task)
|
||||
result, err := moduleCommandWithClient(e, mock, args)
|
||||
|
||||
require.NoError(t, err)
|
||||
assert.True(t, result.Changed)
|
||||
assert.True(t, mock.hasExecuted("systemctl status nginx"))
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue