package test import ( "RenewCMS/internal/domain/update" infraUpdate "RenewCMS/internal/infrastructure/update" "errors" "os" "path/filepath" "testing" "time" ) func TestRestartRequestJSONStore(t *testing.T) { t.Run("it round trips and replaces atomically", func(t *testing.T) { directory := t.TempDir() path := filepath.Join(directory, update.RestartRequestFileName) store := infraUpdate.NewJSONRestartRequestStore(path) if _, err := store.Read(); !errors.Is(err, update.ErrNoRestartRequest) { t.Fatalf("an absent marker = %v", err) } first := update.RestartRequest{Kind: "update", Tag: downloadTag, RequestedAt: restartBaseTime, RequestedBy: 7} second := update.RestartRequest{Kind: "restart", RequestedAt: restartBaseTime.Add(time.Minute), RequestedBy: 8} if err := store.Write(first); err != nil { t.Fatal(err) } if err := store.Write(second); err != nil { t.Fatal(err) } got, err := store.Read() if err != nil || got != second { t.Errorf("got = %+v, err = %v", got, err) } if entries, _ := os.ReadDir(directory); len(entries) != 1 { t.Errorf("no temporary file may remain, got %d entries", len(entries)) } if err := store.Delete(); err != nil { t.Fatal(err) } if err := store.Delete(); err != nil { t.Errorf("deleting twice = %v", err) } if _, err := store.Read(); !errors.Is(err, update.ErrNoRestartRequest) { t.Errorf("after delete = %v", err) } }) t.Run("a corrupt marker still counts as present", func(t *testing.T) { path := filepath.Join(t.TempDir(), update.RestartRequestFileName) _ = os.WriteFile(path, []byte("{nope"), 0o600) _, err := infraUpdate.NewJSONRestartRequestStore(path).Read() if err == nil || errors.Is(err, update.ErrNoRestartRequest) { t.Errorf("err = %v", err) } }) } func TestResultJSONStore(t *testing.T) { t.Run("it round trips, keeps null finished_at and ignores corrupt or unknown files", func(t *testing.T) { path := filepath.Join(t.TempDir(), update.ResultFileName) store := infraUpdate.NewJSONResultStore(path) if _, found := store.Read(); found { t.Fatal("absent file") } running := update.Result{Outcome: update.OutcomeRunning, Kind: "update", Tag: downloadTag, FromVersion: helperOld, StartedAt: restartBaseTime} if err := store.Write(running); err != nil { t.Fatal(err) } got, found := store.Read() if !found || got.Outcome != "running" || got.FinishedAt != nil || !got.StartedAt.Equal(restartBaseTime) { t.Errorf("got = %+v", got) } for name, content := range map[string]string{"corrupt": "{nope", "unknown outcome": `{"outcome":"weird"}`, "empty": ""} { _ = os.WriteFile(path, []byte(content), 0o644) if _, found := store.Read(); found { t.Errorf("%s must count as no result", name) } } _ = store.Write(running) if err := store.Delete(); err != nil { t.Fatal(err) } if _, found := store.Read(); found { t.Error("deleted") } if err := store.Delete(); err != nil { t.Errorf("deleting twice = %v", err) } }) } func TestRunningInfoJSONStore(t *testing.T) { t.Run("it round trips and rejects missing, corrupt and incomplete files", func(t *testing.T) { path := filepath.Join(t.TempDir(), update.RunningInfoFileName) store := infraUpdate.NewJSONRunningInfoStore(path) if _, found := store.Read(); found { t.Fatal("absent file") } info := update.RunningInfo{Version: downloadTag, Pid: 12, StartedAt: restartBaseTime} if err := store.Write(info); err != nil { t.Fatal(err) } if got, found := store.Read(); !found || got != info { t.Errorf("got = %+v", got) } for name, content := range map[string]string{"corrupt": "{nope", "no pid": `{"version":"v1.0.0"}`, "no version": `{"pid":3}`} { _ = os.WriteFile(path, []byte(content), 0o644) if _, found := store.Read(); found { t.Errorf("%s must not be read as running info", name) } } }) t.Run("a symlink is never read", func(t *testing.T) { directory := t.TempDir() target := filepath.Join(directory, "target.json") _ = os.WriteFile(target, []byte(`{"version":"v9.9.9","pid":1,"started_at":"2026-09-30T09:50:00Z"}`), 0o644) link := filepath.Join(directory, update.RunningInfoFileName) if err := os.Symlink(target, link); err != nil { t.Skipf("symlinks are not available: %v", err) } if _, found := infraUpdate.NewJSONRunningInfoStore(link).Read(); found { t.Error("a symlink must not count") } }) } func TestHelperStatusFile(t *testing.T) { t.Run("it is written world readable so the app can detect it", func(t *testing.T) { directory := t.TempDir() writer := infraUpdate.NewJSONHelperStatusWriter() err := writer.Write(directory, update.HelperStatus{Version: 1, Service: "renewcms", Binary: "/opt/renewcms/renewcms", DataDir: directory, InstalledAt: restartBaseTime}) path := filepath.Join(directory, update.HelperStatusFileName) if err != nil || !infraUpdate.NewFileHelperDetector(path).Installed() { t.Fatalf("err = %v", err) } if info, _ := os.Stat(path); info.Mode().Perm()&0o044 != 0o044 && filepath.Separator == '/' { t.Errorf("mode = %v", info.Mode()) } removed, err := writer.Remove(directory) if err != nil || !removed { t.Errorf("removed = %v, err = %v", removed, err) } removed, err = writer.Remove(directory) if err != nil || removed { t.Errorf("a second removal = %v, %v", removed, err) } }) } func TestServiceUnitParsing(t *testing.T) { t.Run("it reads the binary, the data directory and the environment file", func(t *testing.T) { unit := "[Service]\nUser=renewcms\nEnvironmentFile=/etc/renewcms/renewcms.env\nExecStart=/opt/renewcms/renewcms\nProtectSystem=strict\nReadWritePaths=/var/lib/renewcms\n" got := infraUpdate.ParseServiceUnit(unit) if got.BinaryPath != "/opt/renewcms/renewcms" || got.DataDir != "/var/lib/renewcms" || got.EnvironmentFile != "/etc/renewcms/renewcms.env" { t.Errorf("got = %+v", got) } }) t.Run("systemd prefixes are stripped", func(t *testing.T) { unit := "ExecStart=-@/opt/renewcms/renewcms --flag\nEnvironmentFile=-/etc/x.env\nReadWritePaths=-/var/lib/renewcms /other\n" got := infraUpdate.ParseServiceUnit(unit) if got.BinaryPath != "/opt/renewcms/renewcms" || got.EnvironmentFile != "/etc/x.env" || got.DataDir != "/var/lib/renewcms" { t.Errorf("got = %+v", got) } }) } func TestNoFollowFilesystemExists(t *testing.T) { var files update.HelperFilesystem = infraUpdate.NewNoFollowFilesystem() if files == nil { t.Fatal("the adapter must compile on every platform") } }