go-devops/ansible/modules_adv_test.go
Snider 427929f0e9 test(ansible): Phase 1 Step 1.4 — user/group & advanced module tests
69 new tests for user (7), group (7), cron (5), authorized_key (7),
git (8), unarchive (8), uri (6), ufw (8), docker_compose (7), and
dispatch (6) modules. 9 module shims added to mock infrastructure.
Total ansible tests: 334, all passing.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-02-20 02:52:42 +00:00

1127 lines
32 KiB
Go

package ansible
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// ============================================================
// Step 1.4: user / group / cron / authorized_key / git /
// unarchive / uri / ufw / docker_compose / blockinfile
// advanced module tests
// ============================================================
// --- user module ---
func TestModuleUser_Good_CreateNewUser(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`id deploy >/dev/null 2>&1`, "", "no such user", 1)
mock.expectCommand(`useradd`, "", "", 0)
result, err := moduleUserWithClient(e, mock, map[string]any{
"name": "deploy",
"uid": "1500",
"group": "www-data",
"groups": "docker,sudo",
"home": "/opt/deploy",
"shell": "/bin/bash",
"create_home": true,
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.containsSubstring("useradd"))
assert.True(t, mock.containsSubstring("-u 1500"))
assert.True(t, mock.containsSubstring("-g www-data"))
assert.True(t, mock.containsSubstring("-G docker,sudo"))
assert.True(t, mock.containsSubstring("-d /opt/deploy"))
assert.True(t, mock.containsSubstring("-s /bin/bash"))
assert.True(t, mock.containsSubstring("-m"))
}
func TestModuleUser_Good_ModifyExistingUser(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
// id returns success meaning user exists, so usermod branch is taken
mock.expectCommand(`id deploy >/dev/null 2>&1 && usermod`, "", "", 0)
result, err := moduleUserWithClient(e, mock, map[string]any{
"name": "deploy",
"shell": "/bin/zsh",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.containsSubstring("usermod"))
assert.True(t, mock.containsSubstring("-s /bin/zsh"))
}
func TestModuleUser_Good_RemoveUser(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`userdel -r deploy`, "", "", 0)
result, err := moduleUserWithClient(e, mock, map[string]any{
"name": "deploy",
"state": "absent",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.True(t, mock.hasExecuted(`userdel -r deploy`))
}
func TestModuleUser_Good_SystemUser(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`id|useradd`, "", "", 0)
result, err := moduleUserWithClient(e, mock, map[string]any{
"name": "prometheus",
"system": true,
"create_home": false,
"shell": "/usr/sbin/nologin",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
// system flag adds -r
assert.True(t, mock.containsSubstring("-r"))
assert.True(t, mock.containsSubstring("-s /usr/sbin/nologin"))
// create_home=false means -m should NOT be present
// Actually, looking at the production code: getBoolArg(args, "create_home", true) — default is true
// We set it to false explicitly, so -m should NOT appear
cmd := mock.lastCommand()
assert.NotContains(t, cmd.Cmd, " -m ")
}
func TestModuleUser_Good_NoOptsUsesSimpleForm(t *testing.T) {
// When no options are provided, uses the simple "id || useradd" form
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`id testuser >/dev/null 2>&1 || useradd testuser`, "", "", 0)
result, err := moduleUserWithClient(e, mock, map[string]any{
"name": "testuser",
"create_home": false,
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
}
func TestModuleUser_Bad_MissingName(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
_, err := moduleUserWithClient(e, mock, map[string]any{
"state": "present",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "name required")
}
func TestModuleUser_Good_CommandFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`id|useradd|usermod`, "", "useradd: Permission denied", 1)
result, err := moduleUserWithClient(e, mock, map[string]any{
"name": "deploy",
"shell": "/bin/bash",
})
require.NoError(t, err)
assert.True(t, result.Failed)
assert.Contains(t, result.Msg, "Permission denied")
}
// --- group module ---
func TestModuleGroup_Good_CreateNewGroup(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
// getent fails → groupadd runs
mock.expectCommand(`getent group appgroup`, "", "", 1)
mock.expectCommand(`groupadd`, "", "", 0)
result, err := moduleGroupWithClient(e, mock, map[string]any{
"name": "appgroup",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.containsSubstring("groupadd"))
assert.True(t, mock.containsSubstring("appgroup"))
}
func TestModuleGroup_Good_GroupAlreadyExists(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
// getent succeeds → groupadd skipped (|| short-circuits)
mock.expectCommand(`getent group docker >/dev/null 2>&1 || groupadd`, "", "", 0)
result, err := moduleGroupWithClient(e, mock, map[string]any{
"name": "docker",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
}
func TestModuleGroup_Good_RemoveGroup(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`groupdel oldgroup`, "", "", 0)
result, err := moduleGroupWithClient(e, mock, map[string]any{
"name": "oldgroup",
"state": "absent",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.True(t, mock.hasExecuted(`groupdel oldgroup`))
}
func TestModuleGroup_Good_SystemGroup(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`getent group|groupadd`, "", "", 0)
result, err := moduleGroupWithClient(e, mock, map[string]any{
"name": "prometheus",
"system": true,
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.containsSubstring("-r"))
}
func TestModuleGroup_Good_CustomGID(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`getent group|groupadd`, "", "", 0)
result, err := moduleGroupWithClient(e, mock, map[string]any{
"name": "custom",
"gid": "5000",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.containsSubstring("-g 5000"))
}
func TestModuleGroup_Bad_MissingName(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
_, err := moduleGroupWithClient(e, mock, map[string]any{
"state": "present",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "name required")
}
func TestModuleGroup_Good_CommandFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`getent group|groupadd`, "", "groupadd: Permission denied", 1)
result, err := moduleGroupWithClient(e, mock, map[string]any{
"name": "failgroup",
})
require.NoError(t, err)
assert.True(t, result.Failed)
}
// --- cron module ---
func TestModuleCron_Good_AddCronJob(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`crontab -u root`, "", "", 0)
result, err := moduleCronWithClient(e, mock, map[string]any{
"name": "backup",
"job": "/usr/local/bin/backup.sh",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
// Default schedule is * * * * *
assert.True(t, mock.containsSubstring("* * * * *"))
assert.True(t, mock.containsSubstring("/usr/local/bin/backup.sh"))
assert.True(t, mock.containsSubstring("# backup"))
}
func TestModuleCron_Good_RemoveCronJob(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`crontab -u root -l`, "* * * * * /bin/backup # backup\n", "", 0)
result, err := moduleCronWithClient(e, mock, map[string]any{
"name": "backup",
"job": "/bin/backup",
"state": "absent",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.True(t, mock.containsSubstring("grep -v"))
}
func TestModuleCron_Good_CustomSchedule(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`crontab -u root`, "", "", 0)
result, err := moduleCronWithClient(e, mock, map[string]any{
"name": "nightly-backup",
"job": "/opt/scripts/backup.sh",
"minute": "30",
"hour": "2",
"day": "1",
"month": "6",
"weekday": "0",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.containsSubstring("30 2 1 6 0"))
assert.True(t, mock.containsSubstring("/opt/scripts/backup.sh"))
}
func TestModuleCron_Good_CustomUser(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`crontab -u www-data`, "", "", 0)
result, err := moduleCronWithClient(e, mock, map[string]any{
"name": "cache-clear",
"job": "php artisan cache:clear",
"user": "www-data",
"minute": "0",
"hour": "*/4",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.containsSubstring("crontab -u www-data"))
assert.True(t, mock.containsSubstring("0 */4 * * *"))
}
func TestModuleCron_Good_AbsentWithNoName(t *testing.T) {
// Absent with no name — changed but no grep command
e, mock := newTestExecutorWithMock("host1")
result, err := moduleCronWithClient(e, mock, map[string]any{
"state": "absent",
})
require.NoError(t, err)
assert.True(t, result.Changed)
// No commands should have run since name is empty
assert.Equal(t, 0, mock.commandCount())
}
// --- authorized_key module ---
func TestModuleAuthorizedKey_Good_AddKey(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... user@host"
mock.expectCommand(`getent passwd deploy`, "/home/deploy", "", 0)
mock.expectCommand(`mkdir -p`, "", "", 0)
mock.expectCommand(`grep -qF`, "", "", 1) // key not found, will be appended
mock.expectCommand(`echo`, "", "", 0)
mock.expectCommand(`chmod 600`, "", "", 0)
result, err := moduleAuthorizedKeyWithClient(e, mock, map[string]any{
"user": "deploy",
"key": testKey,
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.containsSubstring("mkdir -p"))
assert.True(t, mock.containsSubstring("chmod 700"))
assert.True(t, mock.containsSubstring("authorized_keys"))
}
func TestModuleAuthorizedKey_Good_RemoveKey(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... user@host"
mock.expectCommand(`getent passwd deploy`, "/home/deploy", "", 0)
mock.expectCommand(`sed -i`, "", "", 0)
result, err := moduleAuthorizedKeyWithClient(e, mock, map[string]any{
"user": "deploy",
"key": testKey,
"state": "absent",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.True(t, mock.hasExecuted(`sed -i`))
assert.True(t, mock.containsSubstring("authorized_keys"))
}
func TestModuleAuthorizedKey_Good_KeyAlreadyExists(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... user@host"
mock.expectCommand(`getent passwd deploy`, "/home/deploy", "", 0)
mock.expectCommand(`mkdir -p`, "", "", 0)
// grep succeeds: key already present, || short-circuits, echo not needed
mock.expectCommand(`grep -qF.*echo`, "", "", 0)
mock.expectCommand(`chmod 600`, "", "", 0)
result, err := moduleAuthorizedKeyWithClient(e, mock, map[string]any{
"user": "deploy",
"key": testKey,
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
}
func TestModuleAuthorizedKey_Good_RootUserFallback(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
testKey := "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT... admin@host"
// getent returns empty — falls back to /root for root user
mock.expectCommand(`getent passwd root`, "", "", 0)
mock.expectCommand(`mkdir -p`, "", "", 0)
mock.expectCommand(`grep -qF.*echo`, "", "", 0)
mock.expectCommand(`chmod 600`, "", "", 0)
result, err := moduleAuthorizedKeyWithClient(e, mock, map[string]any{
"user": "root",
"key": testKey,
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
// Should use /root/.ssh/authorized_keys
assert.True(t, mock.containsSubstring("/root/.ssh"))
}
func TestModuleAuthorizedKey_Bad_MissingUserAndKey(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
_, err := moduleAuthorizedKeyWithClient(e, mock, map[string]any{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "user and key required")
}
func TestModuleAuthorizedKey_Bad_MissingKey(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
_, err := moduleAuthorizedKeyWithClient(e, mock, map[string]any{
"user": "deploy",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "user and key required")
}
func TestModuleAuthorizedKey_Bad_MissingUser(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
_, err := moduleAuthorizedKeyWithClient(e, mock, map[string]any{
"key": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDcT...",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "user and key required")
}
// --- git module ---
func TestModuleGit_Good_FreshClone(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
// .git does not exist → fresh clone
mock.expectCommand(`git clone`, "", "", 0)
result, err := moduleGitWithClient(e, mock, map[string]any{
"repo": "https://github.com/example/app.git",
"dest": "/opt/app",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.hasExecuted(`git clone`))
assert.True(t, mock.containsSubstring("https://github.com/example/app.git"))
assert.True(t, mock.containsSubstring("/opt/app"))
// Default version is HEAD
assert.True(t, mock.containsSubstring("git checkout"))
}
func TestModuleGit_Good_UpdateExisting(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
// .git exists → fetch + checkout
mock.addFile("/opt/app/.git", []byte("gitdir"))
mock.expectCommand(`git fetch --all && git checkout`, "", "", 0)
result, err := moduleGitWithClient(e, mock, map[string]any{
"repo": "https://github.com/example/app.git",
"dest": "/opt/app",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.hasExecuted(`git fetch --all`))
assert.True(t, mock.containsSubstring("git checkout --force"))
// Should NOT contain git clone
assert.False(t, mock.containsSubstring("git clone"))
}
func TestModuleGit_Good_CustomVersion(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`git clone`, "", "", 0)
result, err := moduleGitWithClient(e, mock, map[string]any{
"repo": "https://github.com/example/app.git",
"dest": "/opt/app",
"version": "v2.1.0",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.containsSubstring("v2.1.0"))
}
func TestModuleGit_Good_UpdateWithBranch(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.addFile("/srv/myapp/.git", []byte("gitdir"))
mock.expectCommand(`git fetch --all && git checkout`, "", "", 0)
result, err := moduleGitWithClient(e, mock, map[string]any{
"repo": "git@github.com:org/repo.git",
"dest": "/srv/myapp",
"version": "develop",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.True(t, mock.containsSubstring("develop"))
}
func TestModuleGit_Bad_MissingRepoAndDest(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
_, err := moduleGitWithClient(e, mock, map[string]any{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "repo and dest required")
}
func TestModuleGit_Bad_MissingRepo(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
_, err := moduleGitWithClient(e, mock, map[string]any{
"dest": "/opt/app",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "repo and dest required")
}
func TestModuleGit_Bad_MissingDest(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
_, err := moduleGitWithClient(e, mock, map[string]any{
"repo": "https://github.com/example/app.git",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "repo and dest required")
}
func TestModuleGit_Good_CloneFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`git clone`, "", "fatal: repository not found", 128)
result, err := moduleGitWithClient(e, mock, map[string]any{
"repo": "https://github.com/example/nonexistent.git",
"dest": "/opt/app",
})
require.NoError(t, err)
assert.True(t, result.Failed)
assert.Contains(t, result.Msg, "repository not found")
}
// --- unarchive module ---
func TestModuleUnarchive_Good_ExtractTarGzLocal(t *testing.T) {
// Create a temporary "archive" file
tmpDir := t.TempDir()
archivePath := filepath.Join(tmpDir, "package.tar.gz")
require.NoError(t, os.WriteFile(archivePath, []byte("fake-archive-content"), 0644))
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`mkdir -p`, "", "", 0)
mock.expectCommand(`tar -xzf`, "", "", 0)
mock.expectCommand(`rm -f`, "", "", 0)
result, err := moduleUnarchiveWithClient(e, mock, map[string]any{
"src": archivePath,
"dest": "/opt/app",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
// Should have uploaded the file
assert.Equal(t, 1, mock.uploadCount())
assert.True(t, mock.containsSubstring("tar -xzf"))
assert.True(t, mock.containsSubstring("/opt/app"))
}
func TestModuleUnarchive_Good_ExtractZipLocal(t *testing.T) {
tmpDir := t.TempDir()
archivePath := filepath.Join(tmpDir, "release.zip")
require.NoError(t, os.WriteFile(archivePath, []byte("fake-zip-content"), 0644))
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`mkdir -p`, "", "", 0)
mock.expectCommand(`unzip -o`, "", "", 0)
mock.expectCommand(`rm -f`, "", "", 0)
result, err := moduleUnarchiveWithClient(e, mock, map[string]any{
"src": archivePath,
"dest": "/opt/releases",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.Equal(t, 1, mock.uploadCount())
assert.True(t, mock.containsSubstring("unzip -o"))
}
func TestModuleUnarchive_Good_RemoteSource(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`mkdir -p`, "", "", 0)
mock.expectCommand(`tar -xzf`, "", "", 0)
result, err := moduleUnarchiveWithClient(e, mock, map[string]any{
"src": "/tmp/remote-archive.tar.gz",
"dest": "/opt/app",
"remote_src": true,
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
// No upload should happen for remote sources
assert.Equal(t, 0, mock.uploadCount())
assert.True(t, mock.containsSubstring("tar -xzf"))
}
func TestModuleUnarchive_Good_TarXz(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`mkdir -p`, "", "", 0)
mock.expectCommand(`tar -xJf`, "", "", 0)
result, err := moduleUnarchiveWithClient(e, mock, map[string]any{
"src": "/tmp/archive.tar.xz",
"dest": "/opt/extract",
"remote_src": true,
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.True(t, mock.containsSubstring("tar -xJf"))
}
func TestModuleUnarchive_Good_TarBz2(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`mkdir -p`, "", "", 0)
mock.expectCommand(`tar -xjf`, "", "", 0)
result, err := moduleUnarchiveWithClient(e, mock, map[string]any{
"src": "/tmp/archive.tar.bz2",
"dest": "/opt/extract",
"remote_src": true,
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.True(t, mock.containsSubstring("tar -xjf"))
}
func TestModuleUnarchive_Bad_MissingSrcAndDest(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
_, err := moduleUnarchiveWithClient(e, mock, map[string]any{})
assert.Error(t, err)
assert.Contains(t, err.Error(), "src and dest required")
}
func TestModuleUnarchive_Bad_MissingSrc(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
_, err := moduleUnarchiveWithClient(e, mock, map[string]any{
"dest": "/opt/app",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "src and dest required")
}
func TestModuleUnarchive_Bad_LocalFileNotFound(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
mock.expectCommand(`mkdir -p`, "", "", 0)
_, err := moduleUnarchiveWithClient(e, mock, map[string]any{
"src": "/nonexistent/archive.tar.gz",
"dest": "/opt/app",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "read src")
}
// --- uri module ---
func TestModuleURI_Good_GetRequestDefault(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl.*https://example.com/api/health`, "OK\n200", "", 0)
result, err := moduleURIWithClient(e, mock, map[string]any{
"url": "https://example.com/api/health",
})
require.NoError(t, err)
assert.False(t, result.Failed)
assert.False(t, result.Changed) // URI module does not set changed
assert.Equal(t, 200, result.RC)
assert.Equal(t, 200, result.Data["status"])
}
func TestModuleURI_Good_PostWithBodyAndHeaders(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
// Use a broad pattern since header order in map iteration is non-deterministic
mock.expectCommand(`curl.*api\.example\.com`, "{\"id\":1}\n201", "", 0)
result, err := moduleURIWithClient(e, mock, map[string]any{
"url": "https://api.example.com/users",
"method": "POST",
"body": `{"name":"test"}`,
"status_code": 201,
"headers": map[string]any{
"Content-Type": "application/json",
"Authorization": "Bearer token123",
},
})
require.NoError(t, err)
assert.False(t, result.Failed)
assert.Equal(t, 201, result.RC)
assert.True(t, mock.containsSubstring("-X POST"))
assert.True(t, mock.containsSubstring("-d"))
assert.True(t, mock.containsSubstring("Content-Type"))
assert.True(t, mock.containsSubstring("Authorization"))
}
func TestModuleURI_Good_WrongStatusCode(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl`, "Not Found\n404", "", 0)
result, err := moduleURIWithClient(e, mock, map[string]any{
"url": "https://example.com/missing",
})
require.NoError(t, err)
assert.True(t, result.Failed) // Expected 200, got 404
assert.Equal(t, 404, result.RC)
}
func TestModuleURI_Good_CurlCommandFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommandError(`curl`, assert.AnError)
result, err := moduleURIWithClient(e, mock, map[string]any{
"url": "https://unreachable.example.com",
})
require.NoError(t, err)
assert.True(t, result.Failed)
assert.Contains(t, result.Msg, assert.AnError.Error())
}
func TestModuleURI_Good_CustomExpectedStatus(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl`, "\n204", "", 0)
result, err := moduleURIWithClient(e, mock, map[string]any{
"url": "https://api.example.com/resource/1",
"method": "DELETE",
"status_code": 204,
})
require.NoError(t, err)
assert.False(t, result.Failed)
assert.Equal(t, 204, result.RC)
}
func TestModuleURI_Bad_MissingURL(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
_, err := moduleURIWithClient(e, mock, map[string]any{
"method": "GET",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "url required")
}
// --- ufw module ---
func TestModuleUFW_Good_AllowRuleWithPort(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw allow 443/tcp`, "Rule added", "", 0)
result, err := moduleUFWWithClient(e, mock, map[string]any{
"rule": "allow",
"port": "443",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.hasExecuted(`ufw allow 443/tcp`))
}
func TestModuleUFW_Good_EnableFirewall(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw --force enable`, "Firewall is active", "", 0)
result, err := moduleUFWWithClient(e, mock, map[string]any{
"state": "enabled",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.hasExecuted(`ufw --force enable`))
}
func TestModuleUFW_Good_DenyRuleWithProto(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw deny 53/udp`, "Rule added", "", 0)
result, err := moduleUFWWithClient(e, mock, map[string]any{
"rule": "deny",
"port": "53",
"proto": "udp",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.hasExecuted(`ufw deny 53/udp`))
}
func TestModuleUFW_Good_ResetFirewall(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw --force reset`, "Resetting", "", 0)
result, err := moduleUFWWithClient(e, mock, map[string]any{
"state": "reset",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.hasExecuted(`ufw --force reset`))
}
func TestModuleUFW_Good_DisableFirewall(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw disable`, "Firewall stopped", "", 0)
result, err := moduleUFWWithClient(e, mock, map[string]any{
"state": "disabled",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.hasExecuted(`ufw disable`))
}
func TestModuleUFW_Good_ReloadFirewall(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw reload`, "Firewall reloaded", "", 0)
result, err := moduleUFWWithClient(e, mock, map[string]any{
"state": "reloaded",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.hasExecuted(`ufw reload`))
}
func TestModuleUFW_Good_LimitRule(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw limit 22/tcp`, "Rule added", "", 0)
result, err := moduleUFWWithClient(e, mock, map[string]any{
"rule": "limit",
"port": "22",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.hasExecuted(`ufw limit 22/tcp`))
}
func TestModuleUFW_Good_StateCommandFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`ufw --force enable`, "", "ERROR: problem running ufw", 1)
result, err := moduleUFWWithClient(e, mock, map[string]any{
"state": "enabled",
})
require.NoError(t, err)
assert.True(t, result.Failed)
}
// --- docker_compose module ---
func TestModuleDockerCompose_Good_StatePresent(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose up -d`, "Creating container_1\nCreating container_2\n", "", 0)
result, err := moduleDockerComposeWithClient(e, mock, map[string]any{
"project_src": "/opt/myapp",
"state": "present",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.hasExecuted(`docker compose up -d`))
assert.True(t, mock.containsSubstring("/opt/myapp"))
}
func TestModuleDockerCompose_Good_StateAbsent(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose down`, "Removing container_1\n", "", 0)
result, err := moduleDockerComposeWithClient(e, mock, map[string]any{
"project_src": "/opt/myapp",
"state": "absent",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.hasExecuted(`docker compose down`))
}
func TestModuleDockerCompose_Good_AlreadyUpToDate(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose up -d`, "Container myapp-web-1 Up to date\n", "", 0)
result, err := moduleDockerComposeWithClient(e, mock, map[string]any{
"project_src": "/opt/myapp",
"state": "present",
})
require.NoError(t, err)
assert.False(t, result.Changed) // "Up to date" in stdout → changed=false
assert.False(t, result.Failed)
}
func TestModuleDockerCompose_Good_StateRestarted(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose restart`, "Restarting container_1\n", "", 0)
result, err := moduleDockerComposeWithClient(e, mock, map[string]any{
"project_src": "/opt/stack",
"state": "restarted",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.hasExecuted(`docker compose restart`))
}
func TestModuleDockerCompose_Bad_MissingProjectSrc(t *testing.T) {
e, _ := newTestExecutorWithMock("host1")
mock := NewMockSSHClient()
_, err := moduleDockerComposeWithClient(e, mock, map[string]any{
"state": "present",
})
assert.Error(t, err)
assert.Contains(t, err.Error(), "project_src required")
}
func TestModuleDockerCompose_Good_CommandFailure(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose up -d`, "", "Error response from daemon", 1)
result, err := moduleDockerComposeWithClient(e, mock, map[string]any{
"project_src": "/opt/broken",
"state": "present",
})
require.NoError(t, err)
assert.True(t, result.Failed)
assert.Contains(t, result.Msg, "Error response from daemon")
}
func TestModuleDockerCompose_Good_DefaultStateIsPresent(t *testing.T) {
// When no state is specified, default is "present"
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose up -d`, "Starting\n", "", 0)
result, err := moduleDockerComposeWithClient(e, mock, map[string]any{
"project_src": "/opt/app",
})
require.NoError(t, err)
assert.True(t, result.Changed)
assert.False(t, result.Failed)
assert.True(t, mock.hasExecuted(`docker compose up -d`))
}
// --- Cross-module dispatch tests for advanced modules ---
func TestExecuteModuleWithMock_Good_DispatchUser(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`id|useradd|usermod`, "", "", 0)
task := &Task{
Module: "user",
Args: map[string]any{
"name": "appuser",
"shell": "/bin/bash",
},
}
result, err := executeModuleWithMock(e, mock, "host1", task)
require.NoError(t, err)
assert.True(t, result.Changed)
}
func TestExecuteModuleWithMock_Good_DispatchGroup(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`getent group|groupadd`, "", "", 0)
task := &Task{
Module: "group",
Args: map[string]any{
"name": "docker",
},
}
result, err := executeModuleWithMock(e, mock, "host1", task)
require.NoError(t, err)
assert.True(t, result.Changed)
}
func TestExecuteModuleWithMock_Good_DispatchCron(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`crontab`, "", "", 0)
task := &Task{
Module: "cron",
Args: map[string]any{
"name": "logrotate",
"job": "/usr/sbin/logrotate",
},
}
result, err := executeModuleWithMock(e, mock, "host1", task)
require.NoError(t, err)
assert.True(t, result.Changed)
}
func TestExecuteModuleWithMock_Good_DispatchGit(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`git clone`, "", "", 0)
task := &Task{
Module: "git",
Args: map[string]any{
"repo": "https://github.com/org/repo.git",
"dest": "/opt/repo",
},
}
result, err := executeModuleWithMock(e, mock, "host1", task)
require.NoError(t, err)
assert.True(t, result.Changed)
}
func TestExecuteModuleWithMock_Good_DispatchURI(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`curl`, "OK\n200", "", 0)
task := &Task{
Module: "uri",
Args: map[string]any{
"url": "https://example.com",
},
}
result, err := executeModuleWithMock(e, mock, "host1", task)
require.NoError(t, err)
assert.False(t, result.Failed)
}
func TestExecuteModuleWithMock_Good_DispatchDockerCompose(t *testing.T) {
e, mock := newTestExecutorWithMock("host1")
mock.expectCommand(`docker compose up -d`, "Creating\n", "", 0)
task := &Task{
Module: "ansible.builtin.docker_compose",
Args: map[string]any{
"project_src": "/opt/stack",
"state": "present",
},
}
result, err := executeModuleWithMock(e, mock, "host1", task)
require.NoError(t, err)
assert.True(t, result.Changed)
}