package test import ( "RenewCMS/internal/domain/update" infraUpdate "RenewCMS/internal/infrastructure/update" "errors" "net/http" "net/http/httptest" "os" "path" "runtime" "strings" "testing" "time" ) func TestApplyRefusesStagedEntriesThatAreNotRegularFiles(t *testing.T) { for _, name := range []string{helperAsset, update.ChecksumsAssetName, update.SignatureAssetName} { t.Run("a directory in place of "+name, func(t *testing.T) { rig := newHelperRig(t) rig.stage() rig.markUpdate(downloadTag) target := path.Join(helperStagedDir, name) rig.forget(target) rig.fs.putDir(target) err := rig.apply() if err != nil || rig.result().Outcome != update.OutcomeFailed { t.Errorf("err = %v, result = %+v", err, rig.result()) } if !strings.Contains(rig.logger.text(), target+" is not a regular file") { t.Errorf("the refusal must name the entry: %s", rig.logger.text()) } if rig.fs.operationCount("tempdir") != 0 { t.Error("nothing may be copied once a staged entry is refused") } rig.assertUntouched() }) } } func TestApplyKeepsThePreviousBinaryOrFails(t *testing.T) { t.Run("a failed rename of the previous-binary backup stops the install", func(t *testing.T) { rig := newHelperRig(t) rig.stage() rig.markUpdate(downloadTag) rig.fs.failNth("rename", 2, errors.New("cross-device link")) err := rig.apply() if err != nil || rig.result().Outcome != update.OutcomeFailed || rig.service.restartCount() != 0 { t.Errorf("err = %v, result = %+v", err, rig.result()) } rig.assertBinary("old") if rig.fs.exists(helperBinary+".previous") || rig.fs.exists(helperBinary+".previous.tmp") { t.Error("no backup file may be left behind") } if !strings.Contains(rig.logger.text(), "keeping the previous binary: ") { t.Errorf("log = %s", rig.logger.text()) } }) t.Run("a failed rename of the rollback restore is reported loudly", func(t *testing.T) { rig := newHelperRig(t) rig.stage() rig.markUpdate(downloadTag) rig.service.failOn[1] = errors.New("restart failed") rig.fs.failNth("rename", 4, errors.New("input/output error")) err := rig.apply() if err == nil || rig.result().Outcome != update.OutcomeFailed || rig.service.restartCount() != 1 { t.Errorf("err = %v, result = %+v, restarts = %d", err, rig.result(), rig.service.restartCount()) } if !strings.Contains(rig.logger.text(), "the site may be down") || rig.fs.exists(helperBinary+".restore") { t.Errorf("log = %s", rig.logger.text()) } }) } func TestApplyRestartWindowBoundary(t *testing.T) { cases := []struct { name string comesBack int wantOK bool }{ {"the site back one second before the window ends counts", 59, true}, {"the site back exactly when the window ends is too late", 60, false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { rig := newHelperRig(t) rig.markRestart() rig.service.onRestart = func(int) {} rig.clock.onSleep = func(sleeps int) { if sleeps == tc.comesBack { rig.running.set(update.RunningInfo{Version: helperOld, Pid: 300, StartedAt: rig.clock.Now().Add(time.Second)}) } } err := rig.apply() if tc.wantOK && (err != nil || rig.result().Outcome != update.OutcomeSucceeded) { t.Errorf("err = %v, result = %+v", err, rig.result()) } if !tc.wantOK && (err == nil || rig.result().Outcome != update.OutcomeFailed || rig.service.restartCount() != 1) { t.Errorf("err = %v, result = %+v", err, rig.result()) } }) } } func TestApplyLogsOnlyRealFailures(t *testing.T) { t.Run("a clean update logs neither a write nor a removal failure", func(t *testing.T) { rig := newHelperRig(t) rig.stage() rig.markUpdate(downloadTag) if err := rig.apply(); err != nil { t.Fatal(err) } for _, unwanted := range []string{"could not be written", "could not remove", "staged files left in place"} { if strings.Contains(rig.logger.text(), unwanted) { t.Errorf("unexpected log %q in %s", unwanted, rig.logger.text()) } } }) t.Run("a result that cannot be written is logged and does not stop the restart", func(t *testing.T) { rig := newHelperRig(t) rig.markRestart() rig.fs.failNth("create", 1, errors.New("disk full")) err := rig.apply() if err != nil || rig.service.restartCount() != 1 { t.Errorf("err = %v, restarts = %d", err, rig.service.restartCount()) } if !strings.Contains(rig.logger.text(), "update-result.json could not be written: disk full") { t.Errorf("log = %s", rig.logger.text()) } }) t.Run("a temp directory that cannot be removed is logged and the update still succeeds", func(t *testing.T) { rig := newHelperRig(t) rig.stage() rig.markUpdate(downloadTag) rig.fs.failNth("removeall", 1, errors.New("device busy")) err := rig.apply() if err != nil || rig.result().Outcome != update.OutcomeSucceeded { t.Errorf("err = %v, result = %+v", err, rig.result()) } if !strings.Contains(rig.logger.text(), "could not remove /tmp/renewcms-update-1: device busy") { t.Errorf("log = %s", rig.logger.text()) } }) t.Run("staged files that are already gone at cleanup are not an error", func(t *testing.T) { rig := newHelperRig(t) rig.stage() rig.markUpdate(downloadTag) rig.fs.after("removeall", 1, func(f *fakeHelperFS) { for name := range f.nodes { if name == helperStagedDir || strings.HasPrefix(name, helperStagedDir+"/") { delete(f.nodes, name) } } }) err := rig.apply() if err != nil || rig.result().Outcome != update.OutcomeSucceeded { t.Errorf("err = %v, result = %+v", err, rig.result()) } if strings.Contains(rig.logger.text(), "could not remove") || strings.Contains(rig.logger.text(), "staged files left in place") { t.Errorf("log = %s", rig.logger.text()) } if rig.fs.operationCount("removeall") != 1 { t.Errorf("removeall calls = %d, a missing staged directory needs none", rig.fs.operationCount("removeall")) } }) } func TestEnableAndDisableBrowserUpdatesEdges(t *testing.T) { t.Run("a binary given by flag still needs a data directory or a known service", func(t *testing.T) { rig := newInstallRig() rig.locator.found = false rig.locator.description = update.ServiceDescription{} _, err := rig.enable.Enable(update.InstallOptions{Binary: "/opt/cms/renewcms"}) if !errors.Is(err, update.ErrServiceNotFound) || len(rig.events) != 0 { t.Errorf("err = %v, events = %v", err, rig.events) } }) t.Run("every flag given makes an unknown service installable", func(t *testing.T) { rig := newInstallRig() rig.locator.found = false rig.locator.description = update.ServiceDescription{} _, err := rig.enable.Enable(update.InstallOptions{Binary: "/opt/cms/renewcms", DataDir: "/srv/cms", Port: "8081"}) if err != nil || len(rig.units.units) != 2 { t.Errorf("err = %v, units = %v", err, rig.units.units) } if _, found := rig.status.status["/srv/cms"]; !found { t.Error("the status file goes to the data directory given by flag") } }) t.Run("the port is accepted from 1 to 65535 and refused outside it", func(t *testing.T) { for port, valid := range map[string]bool{"1": true, "65535": true, "0": false, "65536": false, "-1": false, "80a": false} { rig := newInstallRig() _, err := rig.enable.Enable(update.InstallOptions{Port: port}) if valid && err != nil { t.Errorf("port %q: %v", port, err) } if !valid && !errors.Is(err, update.ErrInvalidPort) { t.Errorf("port %q: err = %v", port, err) } } }) t.Run("disable with the units present but no status file still disables", func(t *testing.T) { rig := newInstallRig() if _, err := rig.enable.Enable(update.InstallOptions{}); err != nil { t.Fatal(err) } delete(rig.status.status, "/var/lib/renewcms") rig.events = nil message, err := rig.disable.Disable(update.InstallOptions{}) if err != nil || strings.Contains(message, "not enabled") || len(rig.units.units) != 0 { t.Errorf("message = %q, err = %v, units = %v", message, err, rig.units.units) } assertInOrder(t, rig.events, "disable "+update.PathUnitName, "daemon-reload") }) t.Run("disable with a status file but no units still disables", func(t *testing.T) { rig := newInstallRig() if _, err := rig.enable.Enable(update.InstallOptions{}); err != nil { t.Fatal(err) } rig.units.units = map[string]string{} rig.events = nil message, err := rig.disable.Disable(update.InstallOptions{}) if err != nil || strings.Contains(message, "not enabled") || len(rig.status.status) != 0 { t.Errorf("message = %q, err = %v", message, err) } }) } func TestFSStoresReportRealFailures(t *testing.T) { stores := map[string]func(files *fakeHelperFS) (write func() error, remove func() error){ "the result store": func(files *fakeHelperFS) (func() error, func() error) { store := infraUpdate.NewFSResultStore(files, helperResult) return func() error { return store.Write(update.Result{Outcome: update.OutcomeFailed, StartedAt: restartBaseTime}) }, store.Delete }, "the restart request store": func(files *fakeHelperFS) (func() error, func() error) { store := infraUpdate.NewFSRestartRequestStore(files, helperMarker) return func() error { return store.Write(update.RestartRequest{Kind: update.RestartKindRestart, RequestedAt: restartBaseTime}) }, store.Delete }, } for name, build := range stores { t.Run(name+": deleting a file that is not there is fine", func(t *testing.T) { files := newFakeHelperFS() files.putDir(helperData) _, remove := build(files) if err := remove(); err != nil { t.Errorf("err = %v", err) } }) t.Run(name+": a delete that fails for another reason is returned", func(t *testing.T) { files := newFakeHelperFS() files.putDir(helperData) files.failNth("remove", 1, errors.New("read-only file system")) _, remove := build(files) if err := remove(); err == nil || err.Error() != "read-only file system" { t.Errorf("err = %v", err) } }) t.Run(name+": a failed rename is returned and the temp file is removed", func(t *testing.T) { files := newFakeHelperFS() files.putDir(helperData) files.failNth("rename", 1, errors.New("cross-device link")) write, _ := build(files) err := write() if err == nil || err.Error() != "cross-device link" { t.Errorf("err = %v", err) } for node := range files.nodes { if strings.HasSuffix(node, ".tmp") { t.Errorf("the temp file %s must be removed", node) } } }) t.Run(name+": a stale temp file that cannot be removed fails the write", func(t *testing.T) { files := newFakeHelperFS() files.putDir(helperData) files.putFile(helperMarker+".tmp", "stale") files.putFile(helperResult+".tmp", "stale") files.failNth("remove", 1, errors.New("operation not permitted")) write, _ := build(files) err := write() if err == nil || err.Error() != "operation not permitted" || files.operationCount("create") != 0 { t.Errorf("err = %v, creates = %d", err, files.operationCount("create")) } }) } } func TestHTTPHealthChecker(t *testing.T) { serve := func(status int) (string, func()) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/healthz" { w.WriteHeader(http.StatusNotFound) return } w.WriteHeader(status) })) return strings.TrimPrefix(server.URL, "http://127.0.0.1:"), server.Close } t.Run("a 200 answer on /healthz is healthy", func(t *testing.T) { port, stop := serve(http.StatusOK) defer stop() if !infraUpdate.NewHTTPHealthChecker(port).Healthy() { t.Error("a 200 must be healthy") } }) t.Run("any other status is not healthy", func(t *testing.T) { for _, status := range []int{http.StatusNoContent, http.StatusInternalServerError, http.StatusServiceUnavailable} { port, stop := serve(status) healthy := infraUpdate.NewHTTPHealthChecker(port).Healthy() stop() if healthy { t.Errorf("status %d must not be healthy", status) } } }) t.Run("nothing listening is not healthy", func(t *testing.T) { port, stop := serve(http.StatusOK) stop() if infraUpdate.NewHTTPHealthChecker(port).Healthy() { t.Error("a closed port must not be healthy") } }) } func TestSystemMachineProbe(t *testing.T) { probe := infraUpdate.NewSystemMachineProbe() t.Run("it reports the real OS and effective user", func(t *testing.T) { if probe.OS() != runtime.GOOS { t.Errorf("OS = %q, want %q", probe.OS(), runtime.GOOS) } if probe.IsRoot() != (os.Geteuid() == 0) { t.Errorf("IsRoot = %v, euid = %d", probe.IsRoot(), os.Geteuid()) } }) t.Run("it reports systemd only where its runtime directory exists", func(t *testing.T) { info, err := os.Stat("/run/systemd/system") want := err == nil && info.IsDir() if probe.SystemdRunning() != want { t.Errorf("SystemdRunning = %v, want %v", probe.SystemdRunning(), want) } }) }