// 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 ( "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 { 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, }) 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("err = %#v, want %#v", err, tc.expErr) } dbg := res.Wait() if !reflect.DeepEqual(tc.expResp, s.resp) { t.Errorf("resp: %s", cmp.Diff(tc.expResp, s.resp)) } if !reflect.DeepEqual(tc.expDbg, dbg) { t.Errorf("dbg: %s", cmp.Diff(tc.expDbg, dbg)) } }) } }