// Copyright (C) 2021 Simon Ruderich // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with this program. If not, see . package sync import ( "fmt" "io/fs" "os" "os/exec" "testing" "ruderich.org/simon/safcm" "ruderich.org/simon/safcm/testutil" ) func TestSyncCommands(t *testing.T) { exe, err := os.Executable() if err != nil { t.Fatal(err) } env := append(os.Environ(), "SAFCM_HELPER="+exe, "SAFCM_GROUPS=all group1 group2 host.example.org", "SAFCM_GROUP_all=all", "SAFCM_GROUP_group1=group1", "SAFCM_GROUP_group2=group2", "SAFCM_GROUP_host.example.org=host.example.org", ) tests := []struct { name string req safcm.MsgSyncReq triggers []string stdout [][]byte stderr [][]byte errors []error expCmds []*exec.Cmd expDbg []string expResp safcm.MsgSyncResp expErr error }{ // NOTE: Also update MsgSyncResp in safcm test cases when // changing anything here! { "successful command", safcm.MsgSyncReq{ Groups: []string{ "all", "group1", "group2", "host.example.org", }, Commands: []*safcm.Command{ { OrigGroup: "group", Cmd: "echo; env | grep SAFCM_", }, }, }, nil, [][]byte{[]byte("fake stdout/stderr")}, [][]byte{nil}, []error{nil}, []*exec.Cmd{{ Path: "/bin/sh", Args: []string{ "/bin/sh", "-c", "echo; env | grep SAFCM_", }, Env: env, }}, []string{ `3: sync remote: commands: running "/bin/sh" "-c" "echo; env | grep SAFCM_" (group)`, "5: sync remote: commands: command output:\nfake stdout/stderr", }, safcm.MsgSyncResp{ CommandChanges: []safcm.CommandChange{ { Command: "echo; env | grep SAFCM_", Output: "fake stdout/stderr", }, }, }, nil, }, { "successful command (dry-run)", safcm.MsgSyncReq{ DryRun: true, Groups: []string{ "all", "group1", "group2", "host.example.org", }, Commands: []*safcm.Command{ { OrigGroup: "group", Cmd: "echo; env | grep SAFCM_", }, }, }, nil, nil, nil, nil, nil, nil, safcm.MsgSyncResp{ CommandChanges: []safcm.CommandChange{ { Command: "echo; env | grep SAFCM_", }, }, }, nil, }, { "failed command", safcm.MsgSyncReq{ Groups: []string{ "all", "group1", "group2", "host.example.org", }, Commands: []*safcm.Command{ { OrigGroup: "group", Cmd: "echo hi; false", }, }, }, nil, [][]byte{[]byte("fake stdout/stderr")}, [][]byte{nil}, []error{fmt.Errorf("fake error")}, []*exec.Cmd{{ Path: "/bin/sh", Args: []string{ "/bin/sh", "-c", "echo hi; false", }, Env: env, }}, []string{ `3: sync remote: commands: running "/bin/sh" "-c" "echo hi; false" (group)`, "5: sync remote: commands: command output:\nfake stdout/stderr", }, safcm.MsgSyncResp{ CommandChanges: []safcm.CommandChange{ { Command: "echo hi; false", Output: "fake stdout/stderr", Error: "fake error", }, }, }, fmt.Errorf("\"echo hi; false\" failed: fake error"), }, { "failed command (dry-run)", safcm.MsgSyncReq{ DryRun: true, Groups: []string{ "all", "group1", "group2", "host.example.org", }, Commands: []*safcm.Command{ { OrigGroup: "group", Cmd: "echo hi; false", }, }, }, nil, nil, nil, nil, nil, nil, safcm.MsgSyncResp{ CommandChanges: []safcm.CommandChange{ { Command: "echo hi; false", }, }, }, nil, }, { "multiple commands, abort on first failed", safcm.MsgSyncReq{ Groups: []string{ "all", "group1", "group2", "host.example.org", }, Commands: []*safcm.Command{ { OrigGroup: "group1", Cmd: "echo first", }, { OrigGroup: "group2", Cmd: "echo second", }, { OrigGroup: "group3", Cmd: "false", }, { OrigGroup: "group4", Cmd: "echo third", }, }, }, nil, [][]byte{ []byte("fake stdout/stderr first"), []byte("fake stdout/stderr second"), nil, }, [][]byte{ nil, nil, nil, }, []error{ nil, nil, fmt.Errorf("fake error"), }, []*exec.Cmd{{ Path: "/bin/sh", Args: []string{ "/bin/sh", "-c", "echo first", }, Env: env, }, { Path: "/bin/sh", Args: []string{ "/bin/sh", "-c", "echo second", }, Env: env, }, { Path: "/bin/sh", Args: []string{ "/bin/sh", "-c", "false", }, Env: env, }}, []string{ `3: sync remote: commands: running "/bin/sh" "-c" "echo first" (group1)`, "5: sync remote: commands: command output:\nfake stdout/stderr first", `3: sync remote: commands: running "/bin/sh" "-c" "echo second" (group2)`, "5: sync remote: commands: command output:\nfake stdout/stderr second", `3: sync remote: commands: running "/bin/sh" "-c" "false" (group3)`, }, safcm.MsgSyncResp{ CommandChanges: []safcm.CommandChange{ { Command: "echo first", Output: "fake stdout/stderr first", }, { Command: "echo second", Output: "fake stdout/stderr second", }, { Command: "false", Output: "", Error: "fake error", }, }, }, fmt.Errorf("\"false\" failed: fake error"), }, { "triggers", safcm.MsgSyncReq{ Groups: []string{ "all", "group1", "group2", "host.example.org", }, Files: map[string]*safcm.File{ ".": { Path: ".", Mode: fs.ModeDir | 0700, Uid: -1, Gid: -1, OrigGroup: "group", TriggerCommands: []string{ "echo trigger .", }, }, "dir": { Path: "dir", Mode: fs.ModeDir | 0755, Uid: -1, Gid: -1, OrigGroup: "group", TriggerCommands: []string{ "echo trigger dir", }, }, "dir/file": { Path: "dir/file", Mode: 0644, Uid: -1, Gid: -1, Data: []byte("content\n"), OrigGroup: "group", TriggerCommands: []string{ "echo trigger dir/file", }, }, }, Commands: []*safcm.Command{ { OrigGroup: "group", Cmd: "echo; env | grep SAFCM_", }, }, }, []string{ ".", "dir", }, [][]byte{ []byte("fake stdout/stderr ."), []byte("fake stdout/stderr dir"), []byte("fake stdout/stderr"), }, [][]byte{ nil, nil, nil, }, []error{ nil, nil, nil, }, []*exec.Cmd{{ Path: "/bin/sh", Args: []string{ "/bin/sh", "-c", "echo trigger .", }, Env: env, }, { Path: "/bin/sh", Args: []string{ "/bin/sh", "-c", "echo trigger dir", }, Env: env, }, { Path: "/bin/sh", Args: []string{ "/bin/sh", "-c", "echo; env | grep SAFCM_", }, Env: env, }}, []string{ `3: sync remote: commands: running "/bin/sh" "-c" "echo trigger ." (".")`, "5: sync remote: commands: command output:\nfake stdout/stderr .", `3: sync remote: commands: running "/bin/sh" "-c" "echo trigger dir" ("dir")`, "5: sync remote: commands: command output:\nfake stdout/stderr dir", `3: sync remote: commands: running "/bin/sh" "-c" "echo; env | grep SAFCM_" (group)`, "5: sync remote: commands: command output:\nfake stdout/stderr", }, safcm.MsgSyncResp{ CommandChanges: []safcm.CommandChange{ { Command: "echo trigger .", Trigger: ".", Output: "fake stdout/stderr .", }, { Command: "echo trigger dir", Trigger: "dir", Output: "fake stdout/stderr dir", }, { Command: "echo; env | grep SAFCM_", Output: "fake stdout/stderr", }, }, }, nil, }, { "failed trigger", safcm.MsgSyncReq{ Groups: []string{ "all", "group1", "group2", "host.example.org", }, Files: map[string]*safcm.File{ ".": { Path: ".", Mode: fs.ModeDir | 0700, Uid: -1, Gid: -1, OrigGroup: "group", TriggerCommands: []string{ "echo trigger .", }, }, "dir": { Path: "dir", Mode: fs.ModeDir | 0755, Uid: -1, Gid: -1, OrigGroup: "group", TriggerCommands: []string{ "false", }, }, "dir/file": { Path: "dir/file", Mode: 0644, Uid: -1, Gid: -1, Data: []byte("content\n"), OrigGroup: "group", TriggerCommands: []string{ "echo trigger dir/file", }, }, }, Commands: []*safcm.Command{ { OrigGroup: "group", Cmd: "echo; env | grep SAFCM_", }, }, }, []string{ ".", "dir", }, [][]byte{ []byte("fake stdout/stderr ."), []byte("fake stdout/stderr dir"), }, [][]byte{ nil, nil, }, []error{ nil, fmt.Errorf("fake error"), }, []*exec.Cmd{{ Path: "/bin/sh", Args: []string{ "/bin/sh", "-c", "echo trigger .", }, Env: env, }, { Path: "/bin/sh", Args: []string{ "/bin/sh", "-c", "false", }, Env: env, }}, []string{ `3: sync remote: commands: running "/bin/sh" "-c" "echo trigger ." (".")`, "5: sync remote: commands: command output:\nfake stdout/stderr .", `3: sync remote: commands: running "/bin/sh" "-c" "false" ("dir")`, "5: sync remote: commands: command output:\nfake stdout/stderr dir", }, safcm.MsgSyncResp{ CommandChanges: []safcm.CommandChange{ { Command: "echo trigger .", Trigger: ".", Output: "fake stdout/stderr .", }, { Command: "false", Trigger: "dir", Output: "fake stdout/stderr dir", Error: "fake error", }, }, }, fmt.Errorf("\"false\" failed: fake error"), }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { s, res := prepareSync(tc.req, &testRunner{ t: t, expCmds: tc.expCmds, resStdout: tc.stdout, resStderr: tc.stderr, resError: tc.errors, }) s.triggers = tc.triggers err := s.syncCommands() testutil.AssertErrorEqual(t, "err", err, tc.expErr) dbg := res.Wait() testutil.AssertEqual(t, "resp", s.resp, tc.expResp) testutil.AssertEqual(t, "dbg", dbg, tc.expDbg) }) } }