--- /dev/null
+// 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 <http://www.gnu.org/licenses/>.
+
+package sync
+
+import (
+ "bytes"
+ "fmt"
+ "os/exec"
+ "reflect"
+ "testing"
+
+ "github.com/google/go-cmp/cmp"
+
+ "ruderich.org/simon/safcm"
+)
+
+func TestSyncServicesSystemd(t *testing.T) {
+ tests := []struct {
+ name string
+ req safcm.MsgSyncReq
+ 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!
+
+ {
+ "no service change necessary",
+ safcm.MsgSyncReq{
+ Services: []string{
+ "service-one",
+ "service-two",
+ },
+ },
+ [][]byte{
+ []byte(`ActiveState=active
+UnitFileState=enabled
+LoadError=
+
+ActiveState=active
+UnitFileState=enabled
+LoadError=
+`),
+ },
+ [][]byte{nil},
+ []error{nil},
+ []*exec.Cmd{&exec.Cmd{
+ Path: "/bin/systemctl",
+ Args: []string{
+ "/bin/systemctl",
+ "show",
+ "--property=ActiveState,UnitFileState,LoadError",
+ "--",
+ "service-one",
+ "service-two",
+ },
+ Stdout: &bytes.Buffer{},
+ Stderr: &bytes.Buffer{},
+ }},
+ []string{
+ "4: sync remote: services: detected systemd",
+ "4: sync remote: services: checking service-one service-two",
+ `4: sync remote: services: running "/bin/systemctl" "show" "--property=ActiveState,UnitFileState,LoadError" "--" "service-one" "service-two"`,
+ `5: sync remote: services: command stdout:
+ActiveState=active
+UnitFileState=enabled
+LoadError=
+
+ActiveState=active
+UnitFileState=enabled
+LoadError=
+`,
+ },
+ safcm.MsgSyncResp{},
+ nil,
+ },
+
+ {
+ "no service change necessary (older systemd)",
+ safcm.MsgSyncReq{
+ Services: []string{
+ "service-one",
+ "service-two",
+ },
+ },
+ [][]byte{
+ []byte(`ActiveState=active
+UnitFileState=enabled
+LoadError= ""
+
+ActiveState=active
+UnitFileState=enabled
+LoadError= ""
+`),
+ },
+ [][]byte{nil},
+ []error{nil},
+ []*exec.Cmd{&exec.Cmd{
+ Path: "/bin/systemctl",
+ Args: []string{
+ "/bin/systemctl",
+ "show",
+ "--property=ActiveState,UnitFileState,LoadError",
+ "--",
+ "service-one",
+ "service-two",
+ },
+ Stdout: &bytes.Buffer{},
+ Stderr: &bytes.Buffer{},
+ }},
+ []string{
+ "4: sync remote: services: detected systemd",
+ "4: sync remote: services: checking service-one service-two",
+ `4: sync remote: services: running "/bin/systemctl" "show" "--property=ActiveState,UnitFileState,LoadError" "--" "service-one" "service-two"`,
+ `5: sync remote: services: command stdout:
+ActiveState=active
+UnitFileState=enabled
+LoadError= ""
+
+ActiveState=active
+UnitFileState=enabled
+LoadError= ""
+`,
+ },
+ safcm.MsgSyncResp{},
+ nil,
+ },
+
+ {
+ "invalid service",
+ safcm.MsgSyncReq{
+ Services: []string{
+ "service-does-not-exist",
+ "service-two",
+ },
+ },
+ [][]byte{
+ []byte(`ActiveState=inactive
+UnitFileState=
+LoadError=org.freedesktop.systemd1.NoSuchUnit "Unit service-does-not-exist.service not found."
+
+ActiveState=active
+UnitFileState=enabled
+LoadError=
+`),
+ },
+ [][]byte{nil},
+ []error{nil},
+ []*exec.Cmd{&exec.Cmd{
+ Path: "/bin/systemctl",
+ Args: []string{
+ "/bin/systemctl",
+ "show",
+ "--property=ActiveState,UnitFileState,LoadError",
+ "--",
+ "service-does-not-exist",
+ "service-two",
+ },
+ Stdout: &bytes.Buffer{},
+ Stderr: &bytes.Buffer{},
+ }},
+ []string{
+ "4: sync remote: services: detected systemd",
+ "4: sync remote: services: checking service-does-not-exist service-two",
+ `4: sync remote: services: running "/bin/systemctl" "show" "--property=ActiveState,UnitFileState,LoadError" "--" "service-does-not-exist" "service-two"`,
+ `5: sync remote: services: command stdout:
+ActiveState=inactive
+UnitFileState=
+LoadError=org.freedesktop.systemd1.NoSuchUnit "Unit service-does-not-exist.service not found."
+
+ActiveState=active
+UnitFileState=enabled
+LoadError=
+`,
+ },
+ safcm.MsgSyncResp{},
+ fmt.Errorf("systemd unit \"service-does-not-exist\" not found"),
+ },
+
+ {
+ "start/enable service",
+ safcm.MsgSyncReq{
+ Services: []string{
+ "service-one",
+ "service-two",
+ "service-three",
+ },
+ },
+ [][]byte{
+ []byte(`ActiveState=inactive
+UnitFileState=enabled
+LoadError=
+
+ActiveState=active
+UnitFileState=disabled
+LoadError=
+
+ActiveState=failed
+UnitFileState=disabled
+LoadError=
+`),
+ nil,
+ nil,
+ nil,
+ },
+ [][]byte{
+ nil,
+ nil,
+ nil,
+ []byte(`fake stderr`),
+ },
+ []error{nil, nil, nil, nil},
+ []*exec.Cmd{&exec.Cmd{
+ Path: "/bin/systemctl",
+ Args: []string{
+ "/bin/systemctl",
+ "show",
+ "--property=ActiveState,UnitFileState,LoadError",
+ "--",
+ "service-one",
+ "service-two",
+ "service-three",
+ },
+ Stdout: &bytes.Buffer{},
+ Stderr: &bytes.Buffer{},
+ }, &exec.Cmd{
+ Path: "/bin/systemctl",
+ Args: []string{
+ "/bin/systemctl",
+ "daemon-reload",
+ },
+ Stdout: &bytes.Buffer{},
+ Stderr: &bytes.Buffer{},
+ }, &exec.Cmd{
+ Path: "/bin/systemctl",
+ Args: []string{
+ "/bin/systemctl",
+ "start",
+ "--",
+ "service-one",
+ "service-three",
+ },
+ Stdout: &bytes.Buffer{},
+ Stderr: &bytes.Buffer{},
+ }, &exec.Cmd{
+ Path: "/bin/systemctl",
+ Args: []string{
+ "/bin/systemctl",
+ "enable",
+ "--",
+ "service-two",
+ "service-three",
+ },
+ Stdout: &bytes.Buffer{},
+ Stderr: &bytes.Buffer{},
+ }},
+ []string{
+ "4: sync remote: services: detected systemd",
+ "4: sync remote: services: checking service-one service-two service-three",
+ `4: sync remote: services: running "/bin/systemctl" "show" "--property=ActiveState,UnitFileState,LoadError" "--" "service-one" "service-two" "service-three"`,
+ `5: sync remote: services: command stdout:
+ActiveState=inactive
+UnitFileState=enabled
+LoadError=
+
+ActiveState=active
+UnitFileState=disabled
+LoadError=
+
+ActiveState=failed
+UnitFileState=disabled
+LoadError=
+`,
+ `4: sync remote: services: running "/bin/systemctl" "daemon-reload"`,
+ "3: sync remote: services: starting service-one service-three",
+ `4: sync remote: services: running "/bin/systemctl" "start" "--" "service-one" "service-three"`,
+ "3: sync remote: services: enabling service-two service-three",
+ `4: sync remote: services: running "/bin/systemctl" "enable" "--" "service-two" "service-three"`,
+ "5: sync remote: services: command stderr:\nfake stderr",
+ },
+ safcm.MsgSyncResp{
+ ServiceChanges: []safcm.ServiceChange{
+ {
+ Name: "service-one",
+ Started: true,
+ },
+ {
+ Name: "service-two",
+ Enabled: true,
+ },
+ {
+ Name: "service-three",
+ Started: true,
+ Enabled: true,
+ },
+ },
+ },
+ nil,
+ },
+
+ {
+ "start/enable service (dry-run)",
+ safcm.MsgSyncReq{
+ DryRun: true,
+ Services: []string{
+ "service-one",
+ "service-two",
+ "service-three",
+ },
+ },
+ [][]byte{
+ []byte(`ActiveState=inactive
+UnitFileState=enabled
+LoadError=
+
+ActiveState=active
+UnitFileState=disabled
+LoadError=
+
+ActiveState=failed
+UnitFileState=disabled
+LoadError=
+`),
+ },
+ [][]byte{nil},
+ []error{nil},
+ []*exec.Cmd{&exec.Cmd{
+ Path: "/bin/systemctl",
+ Args: []string{
+ "/bin/systemctl",
+ "show",
+ "--property=ActiveState,UnitFileState,LoadError",
+ "--",
+ "service-one",
+ "service-two",
+ "service-three",
+ },
+ Stdout: &bytes.Buffer{},
+ Stderr: &bytes.Buffer{},
+ }},
+ []string{
+ "4: sync remote: services: detected systemd",
+ "4: sync remote: services: checking service-one service-two service-three",
+ `4: sync remote: services: running "/bin/systemctl" "show" "--property=ActiveState,UnitFileState,LoadError" "--" "service-one" "service-two" "service-three"`,
+ `5: sync remote: services: command stdout:
+ActiveState=inactive
+UnitFileState=enabled
+LoadError=
+
+ActiveState=active
+UnitFileState=disabled
+LoadError=
+
+ActiveState=failed
+UnitFileState=disabled
+LoadError=
+`,
+ },
+ safcm.MsgSyncResp{
+ ServiceChanges: []safcm.ServiceChange{
+ {
+ Name: "service-one",
+ Started: true,
+ },
+ {
+ Name: "service-two",
+ Enabled: true,
+ },
+ {
+ Name: "service-three",
+ Started: true,
+ Enabled: true,
+ },
+ },
+ },
+ nil,
+ },
+
+ {
+ "start/enable service (error)",
+ safcm.MsgSyncReq{
+ Services: []string{
+ "service-one",
+ "service-two",
+ "service-three",
+ },
+ },
+ [][]byte{
+ []byte(`ActiveState=inactive
+UnitFileState=enabled
+LoadError=
+
+ActiveState=active
+UnitFileState=disabled
+LoadError=
+
+ActiveState=failed
+UnitFileState=disabled
+LoadError=
+`),
+ nil,
+ nil,
+ },
+ [][]byte{
+ nil,
+ nil,
+ []byte(`fake stderr`),
+ },
+ []error{
+ nil,
+ nil,
+ fmt.Errorf("fake error"),
+ },
+ []*exec.Cmd{&exec.Cmd{
+ Path: "/bin/systemctl",
+ Args: []string{
+ "/bin/systemctl",
+ "show",
+ "--property=ActiveState,UnitFileState,LoadError",
+ "--",
+ "service-one",
+ "service-two",
+ "service-three",
+ },
+ Stdout: &bytes.Buffer{},
+ Stderr: &bytes.Buffer{},
+ }, &exec.Cmd{
+ Path: "/bin/systemctl",
+ Args: []string{
+ "/bin/systemctl",
+ "daemon-reload",
+ },
+ Stdout: &bytes.Buffer{},
+ Stderr: &bytes.Buffer{},
+ }, &exec.Cmd{
+ Path: "/bin/systemctl",
+ Args: []string{
+ "/bin/systemctl",
+ "start",
+ "--",
+ "service-one",
+ "service-three",
+ },
+ Stdout: &bytes.Buffer{},
+ Stderr: &bytes.Buffer{},
+ }},
+ []string{
+ "4: sync remote: services: detected systemd",
+ "4: sync remote: services: checking service-one service-two service-three",
+ `4: sync remote: services: running "/bin/systemctl" "show" "--property=ActiveState,UnitFileState,LoadError" "--" "service-one" "service-two" "service-three"`,
+ `5: sync remote: services: command stdout:
+ActiveState=inactive
+UnitFileState=enabled
+LoadError=
+
+ActiveState=active
+UnitFileState=disabled
+LoadError=
+
+ActiveState=failed
+UnitFileState=disabled
+LoadError=
+`,
+ `4: sync remote: services: running "/bin/systemctl" "daemon-reload"`,
+ "3: sync remote: services: starting service-one service-three",
+ `4: sync remote: services: running "/bin/systemctl" "start" "--" "service-one" "service-three"`,
+ "5: sync remote: services: command stderr:\nfake stderr",
+ },
+ safcm.MsgSyncResp{
+ ServiceChanges: []safcm.ServiceChange{
+ {
+ Name: "service-one",
+ Started: true,
+ },
+ {
+ Name: "service-two",
+ Enabled: true,
+ },
+ {
+ Name: "service-three",
+ Started: true,
+ Enabled: true,
+ },
+ },
+ },
+ fmt.Errorf(`"/bin/systemctl" "start" "--" "service-one" "service-three" failed: fake error; stdout: "", stderr: "fake stderr"`),
+ },
+ }
+
+ for _, tc := range tests {
+ s, res := prepareSync(tc.req, &testRunner{
+ t: t,
+ name: tc.name,
+ expCmds: tc.expCmds,
+ resStdout: tc.stdout,
+ resStderr: tc.stderr,
+ resError: tc.errors,
+ })
+
+ err := s.syncServicesSystemd()
+ // Ugly but the simplest way to compare errors (including nil)
+ if fmt.Sprintf("%s", err) != fmt.Sprintf("%s", tc.expErr) {
+ t.Errorf("%s: err = %#v, want %#v",
+ tc.name, err, tc.expErr)
+ }
+ dbg := res.Wait()
+
+ if !reflect.DeepEqual(tc.expResp, s.resp) {
+ t.Errorf("%s: resp: %s", tc.name,
+ cmp.Diff(tc.expResp, s.resp))
+ }
+ if !reflect.DeepEqual(tc.expDbg, dbg) {
+ t.Errorf("%s: dbg: %s", tc.name,
+ cmp.Diff(tc.expDbg, dbg))
+ }
+ }
+}