Files
RenewCMS/test/update_restart_http_test.go
T

632 lines
23 KiB
Go

package test
import (
"RenewCMS/api"
"RenewCMS/api/pkg/apperr"
validator "RenewCMS/api/pkg/validator"
"RenewCMS/internal/domain/update"
"RenewCMS/internal/infrastructure/config"
"RenewCMS/internal/infrastructure/storage"
infraUpdate "RenewCMS/internal/infrastructure/update"
"RenewCMS/internal/infrastructure/useCases"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
"github.com/go-chi/jwtauth/v5"
)
const restartPath = "/update/restart"
type restartHTTP struct {
router http.Handler
token *jwtauth.JWTAuth
dataDir string
markerPath string
resultPath string
store *fakeUpdateStore
jobs *fakeJobStore
helper *fakeHelperDetector
clock *fakeClock
markers update.RestartRequestStore
resultFiles update.ResultStore
}
func newRestartHTTP(t *testing.T) *restartHTTP {
t.Helper()
validator.InitValidator()
dataDir := t.TempDir()
h := &restartHTTP{
token: jwtauth.New("HS256", []byte("test-secret"), nil),
dataDir: dataDir,
markerPath: filepath.Join(dataDir, update.RestartRequestFileName),
resultPath: filepath.Join(dataDir, update.ResultFileName),
store: &fakeUpdateStore{result: newStoredUpdateResult(), found: true},
jobs: &fakeJobStore{},
helper: &fakeHelperDetector{installed: true},
clock: &fakeClock{now: restartBaseTime},
}
h.markers = infraUpdate.NewJSONRestartRequestStore(h.markerPath)
h.resultFiles = infraUpdate.NewJSONResultStore(h.resultPath)
container := &api.UseCases{
Token: h.token,
GetUserUseCase: useCases.NewGetUserUseCase(newUpdateUserRepo()),
GetMyAuthorUseCase: useCases.NewGetMyAuthorUseCase(plainAuthors()),
GetUpdateStatusUseCase: useCases.NewGetUpdateStatusUseCase(
h.store, h.jobs, h.helper, h.resultFiles, h.markers, updateRunning, false, h.clock.Now),
RequestRestartUseCase: useCases.NewRequestRestartUseCase(
h.store, h.jobs, h.helper, h.resultFiles, h.markers, updateRunning, h.clock.Now, discardRecorder),
}
h.router = api.NewSystemRouter(container, nil)
return h
}
func (h *restartHTTP) readyJob() {
h.jobs.put(update.Job{Tag: downloadTag, State: update.StateReady, StartedAt: restartBaseTime.Add(-time.Hour)})
}
func (h *restartHTTP) post(t *testing.T, userId uint32, tokenVersion uint32, body string) *httptest.ResponseRecorder {
t.Helper()
return h.request(t, http.MethodPost, restartPath, userId, tokenVersion, body)
}
func (h *restartHTTP) postAsOwner(t *testing.T, body string) *httptest.ResponseRecorder {
t.Helper()
return h.post(t, updateOwnerId, 2, body)
}
func (h *restartHTTP) request(t *testing.T, method string, target string, userId uint32, tokenVersion uint32, body string) *httptest.ResponseRecorder {
t.Helper()
req := httptest.NewRequest(method, target, strings.NewReader(body))
if userId != 0 {
_, tokenString, err := h.token.Encode(map[string]any{"user_id": userId, "token_version": tokenVersion})
if err != nil {
t.Fatal(err)
}
req.AddCookie(&http.Cookie{Name: "jwt", Value: tokenString})
}
rec := httptest.NewRecorder()
h.router.ServeHTTP(rec, req)
return rec
}
func (h *restartHTTP) getAs(t *testing.T, userId uint32) *httptest.ResponseRecorder {
t.Helper()
tokenVersion := uint32(0)
if userId == updateOwnerId {
tokenVersion = 2
}
return h.request(t, http.MethodGet, "/update", userId, tokenVersion, "")
}
func (h *restartHTTP) markerBytes() ([]byte, bool) {
content, err := os.ReadFile(h.markerPath)
return content, err == nil
}
func (h *restartHTTP) seedResult(t *testing.T, result update.Result) {
t.Helper()
if err := h.resultFiles.Write(result); err != nil {
t.Fatal(err)
}
}
func (h *restartHTTP) assertNoMarkerAndResultKept(t *testing.T, resultKept bool) {
t.Helper()
if _, found := h.markerBytes(); found {
t.Error("no marker may be written")
}
if _, found := h.resultFiles.Read(); found != resultKept {
t.Errorf("result present = %v, want %v", found, resultKept)
}
}
func TestRestartHTTP(t *testing.T) {
t.Run("Request an update restart, happy path", func(t *testing.T) {
for name, body := range map[string]string{"no body": "", "kind update": `{"kind":"update"}`} {
t.Run(name, func(t *testing.T) {
h := newRestartHTTP(t)
h.readyJob()
h.seedResult(t, update.Result{Outcome: update.OutcomeRolledBack, Kind: update.RestartKindUpdate, Tag: downloadTag, StartedAt: restartBaseTime.Add(-time.Hour), FinishedAt: finishedAt(restartBaseTime.Add(-time.Hour))})
rec := h.postAsOwner(t, body)
if rec.Code != http.StatusAccepted {
t.Fatalf("status = %d: %s", rec.Code, rec.Body.String())
}
status := decodeUpdateBody(t, rec)
assertUpdateKeys(t, status)
if status["state"] != "restarting" || status["started_at"] != "2026-09-30T09:50:00Z" || status["error"] != nil || status["finished_at"] != nil {
t.Errorf("status = %v", status)
}
marker, err := h.markers.Read()
if err != nil || marker.Kind != "update" || marker.Tag != downloadTag || marker.RequestedBy != updateOwnerId || !marker.RequestedAt.Equal(restartBaseTime) {
t.Errorf("marker = %+v, err = %v", marker, err)
}
if _, found := h.resultFiles.Read(); found {
t.Error("update-result.json must be deleted first")
}
entries, _ := os.ReadDir(h.dataDir)
if len(entries) != 1 {
t.Errorf("only the marker may exist in the data directory, got %d entries", len(entries))
}
asAdmin := decodeUpdateBody(t, h.getAs(t, updateAdminId))
if asAdmin["state"] != "restarting" || asAdmin["can_apply"] != false {
t.Errorf("admin view = %v", asAdmin)
}
})
}
})
t.Run("Request a plain restart", func(t *testing.T) {
for name, body := range map[string]string{"no body": "", "kind restart": `{"kind":"restart"}`, "empty object": `{}`} {
t.Run(name, func(t *testing.T) {
h := newRestartHTTP(t)
rec := h.postAsOwner(t, body)
marker, err := h.markers.Read()
if rec.Code != http.StatusAccepted || err != nil || marker.Kind != "restart" || marker.Tag != "" {
t.Fatalf("status = %d, marker = %+v, err = %v", rec.Code, marker, err)
}
if decodeUpdateBody(t, h.getAs(t, updateOwnerId))["state"] != "restarting" {
t.Error("GET must report restarting")
}
})
}
})
t.Run("The kind must match the state", func(t *testing.T) {
cases := []struct {
name string
prepare func(*restartHTTP)
kind string
}{
{"no job ready (idle) with update", func(*restartHTTP) {}, "update"},
{"a job failed with update", func(h *restartHTTP) {
h.jobs.put(update.Job{Tag: downloadTag, State: update.StateFailed, Error: "download"})
}, "update"},
{"a job ready with restart", func(h *restartHTTP) { h.readyJob() }, "restart"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
h := newRestartHTTP(t)
h.seedResult(t, update.Result{Outcome: update.OutcomeFailed, StartedAt: restartBaseTime, FinishedAt: finishedAt(restartBaseTime)})
tc.prepare(h)
jobBefore, _ := h.jobs.Load()
rec := h.postAsOwner(t, `{"kind":"`+tc.kind+`"}`)
jobAfter, _ := h.jobs.Load()
if rec.Code != http.StatusConflict || jobAfter != jobBefore {
t.Errorf("status = %d", rec.Code)
}
h.assertNoMarkerAndResultKept(t, true)
})
}
})
t.Run("Restart site never installs an update another owner just made ready", func(t *testing.T) {
h := newRestartHTTP(t)
h.readyJob()
rec := h.postAsOwner(t, `{"kind":"restart"}`)
if rec.Code != http.StatusConflict {
t.Fatalf("status = %d", rec.Code)
}
h.assertNoMarkerAndResultKept(t, false)
if job, found := h.jobs.Load(); !found || job.State != update.StateReady {
t.Error("the ready job must stay ready")
}
if decodeUpdateBody(t, h.getAs(t, updateOwnerId))["state"] != "ready" {
t.Error("the card must read Ready to install after the reload")
}
})
t.Run("A bad body is a 400", func(t *testing.T) {
for _, body := range []string{`{"kind":"upgrade"}`, `{"kind":""}`, `{"kind":7}`, `{"kind":"update"`, `not json`} {
t.Run(body, func(t *testing.T) {
h := newRestartHTTP(t)
h.readyJob()
h.seedResult(t, update.Result{Outcome: update.OutcomeFailed})
rec := h.postAsOwner(t, body)
if rec.Code != http.StatusBadRequest || rec.Body.String() != apperr.BodyErrorMessage+"\n" {
t.Errorf("status = %d body = %q", rec.Code, rec.Body.String())
}
h.assertNoMarkerAndResultKept(t, true)
})
}
})
t.Run("POST is guarded like the other system routes", func(t *testing.T) {
cases := []struct {
name string
userId uint32
tokenVersion uint32
status int
body string
}{
{"no jwt cookie", 0, 0, http.StatusUnauthorized, ""},
{"a stale token version", updateOwnerId, 1, http.StatusUnauthorized, apperr.UnauthorizedErrorMessage + "\n"},
{"an unverified owner", updateUnverifiedOwner, 0, http.StatusForbidden, apperr.VerificationRequiredErrorMessage + "\n"},
{"an admin", updateAdminId, 0, http.StatusForbidden, apperr.OwnerRequiredErrorMessage + "\n"},
{"an admin with a bad body", updateAdminId, 0, http.StatusForbidden, apperr.OwnerRequiredErrorMessage + "\n"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
h := newRestartHTTP(t)
h.seedResult(t, update.Result{Outcome: update.OutcomeFailed})
body := ""
if strings.Contains(tc.name, "bad body") {
body = "not json"
}
rec := h.post(t, tc.userId, tc.tokenVersion, body)
if rec.Code != tc.status || (tc.body != "" && rec.Body.String() != tc.body) {
t.Errorf("status = %d body = %q", rec.Code, rec.Body.String())
}
h.assertNoMarkerAndResultKept(t, true)
})
}
})
t.Run("The checks run in the documented order", func(t *testing.T) {
h := newRestartHTTP(t)
h.helper.installed = false
if rec := h.postAsOwner(t, "not json"); rec.Code != http.StatusBadRequest {
t.Errorf("a bad body with can_apply off = %d, want 400", rec.Code)
}
if rec := h.postAsOwner(t, `{"kind":"update"}`); rec.Code != http.StatusConflict {
t.Errorf("can_apply off with a mismatching kind = %d, want 409", rec.Code)
}
})
t.Run("Restart is refused with 409", func(t *testing.T) {
cases := []struct {
name string
prepare func(*restartHTTP, *testing.T)
}{
{"can_apply is false (no update-helper.json)", func(h *restartHTTP, _ *testing.T) { h.helper.installed = false }},
{"the job state is downloading", func(h *restartHTTP, _ *testing.T) {
h.jobs.put(update.Job{Tag: downloadTag, State: update.StateDownloading})
}},
{"the job state is verifying", func(h *restartHTTP, _ *testing.T) {
h.jobs.put(update.Job{Tag: downloadTag, State: update.StateVerifying})
}},
{"a marker is already pending", func(h *restartHTTP, t *testing.T) {
if err := h.markers.Write(update.RestartRequest{Kind: "restart", RequestedAt: restartBaseTime, RequestedBy: 9}); err != nil {
t.Fatal(err)
}
}},
{"the result file reads running (helper at work)", func(h *restartHTTP, t *testing.T) {
h.seedResult(t, update.Result{Outcome: update.OutcomeRunning, Kind: "update", StartedAt: restartBaseTime.Add(-30 * time.Second)})
}},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
h := newRestartHTTP(t)
tc.prepare(h, t)
markerBefore, markerExisted := h.markerBytes()
resultBefore, resultExisted := os.ReadFile(h.resultPath)
rec := h.postAsOwner(t, "")
markerAfter, markerExistsNow := h.markerBytes()
resultAfter, resultExistsNow := os.ReadFile(h.resultPath)
if rec.Code != http.StatusConflict {
t.Fatalf("status = %d", rec.Code)
}
if markerExisted != markerExistsNow || string(markerBefore) != string(markerAfter) || (resultExisted == nil) != (resultExistsNow == nil) || string(resultBefore) != string(resultAfter) {
t.Error("the existing marker and result must be untouched and no new marker written")
}
})
}
})
t.Run("Two restarts at once write one marker", func(t *testing.T) {
h := newRestartHTTP(t)
var wait sync.WaitGroup
codes := make([]int, 2)
for i, userId := range []uint32{updateOwnerId, updateSecondOwner} {
wait.Add(1)
go func() {
defer wait.Done()
tokenVersion := uint32(0)
if userId == updateOwnerId {
tokenVersion = 2
}
codes[i] = h.post(t, userId, tokenVersion, "").Code
}()
}
wait.Wait()
accepted, refused := 0, 0
for _, code := range codes {
switch code {
case http.StatusAccepted:
accepted++
case http.StatusConflict:
refused++
}
}
entries, _ := os.ReadDir(h.dataDir)
if accepted != 1 || refused != 1 || len(entries) != 1 {
t.Errorf("codes = %v, entries = %d", codes, len(entries))
}
})
t.Run("The marker cannot be written", func(t *testing.T) {
assertFailure := func(t *testing.T, h *restartHTTP) {
t.Helper()
jobBefore, _ := h.jobs.Load()
rec := h.postAsOwner(t, "")
jobAfter, _ := h.jobs.Load()
if rec.Code != http.StatusInternalServerError || rec.Body.String() != "Internal server error\n" {
t.Errorf("status = %d body = %q", rec.Code, rec.Body.String())
}
if strings.Contains(rec.Body.String(), h.dataDir) || jobAfter != jobBefore {
t.Error("no path may leak and the job record must not change")
}
}
t.Run("a directory sits where the marker goes", func(t *testing.T) {
h := newRestartHTTP(t)
h.readyJob()
if err := os.Mkdir(h.markerPath, 0o755); err != nil {
t.Fatal(err)
}
assertFailure(t, h)
if info, err := os.Stat(h.markerPath); err != nil || !info.IsDir() {
t.Error("the directory must stay")
}
})
t.Run("the data directory is not writable", func(t *testing.T) {
if runtime.GOOS == "windows" || os.Geteuid() == 0 {
t.Skip("directory permissions are not enforced here")
}
h := newRestartHTTP(t)
if err := os.Chmod(h.dataDir, 0o500); err != nil {
t.Fatal(err)
}
defer os.Chmod(h.dataDir, 0o700)
assertFailure(t, h)
if _, found := h.markerBytes(); found {
t.Error("no marker may exist")
}
})
})
t.Run("GET derives the state from the files and never changes them", func(t *testing.T) {
finished := restartBaseTime.Add(-10 * time.Second)
cases := []struct {
name string
files func(*restartHTTP, *testing.T)
state string
errKind any
finished any
}{
{"a marker is present", func(h *restartHTTP, t *testing.T) {
_ = h.markers.Write(update.RestartRequest{Kind: "restart", RequestedAt: restartBaseTime.Add(-time.Second)})
}, "restarting", nil, nil},
{"result running written 30 s ago", func(h *restartHTTP, t *testing.T) {
h.seedResult(t, update.Result{Outcome: "running", Kind: "update", Tag: downloadTag, StartedAt: restartBaseTime.Add(-30 * time.Second)})
}, "restarting", nil, nil},
{"result running written 6 minutes ago, no marker", func(h *restartHTTP, t *testing.T) {
h.seedResult(t, update.Result{Outcome: "running", Kind: "update", Tag: downloadTag, StartedAt: restartBaseTime.Add(-6 * time.Minute), FinishedAt: &finished})
}, "failed", "install", "2026-09-30T09:49:50Z"},
{"result succeeded, kind update, tag equal to the running version", func(h *restartHTTP, t *testing.T) {
h.seedResult(t, update.Result{Outcome: "succeeded", Kind: "update", Tag: updateRunning, StartedAt: restartBaseTime.Add(-time.Minute), FinishedAt: &finished})
}, "succeeded", nil, "2026-09-30T09:49:50Z"},
{"result succeeded, kind update, tag older than the running one", func(h *restartHTTP, t *testing.T) {
h.seedResult(t, update.Result{Outcome: "succeeded", Kind: "update", Tag: "v2.1.0-beta", StartedAt: restartBaseTime.Add(-time.Minute), FinishedAt: &finished})
}, "idle", nil, "2026-09-30T09:49:50Z"},
{"result succeeded, kind restart", func(h *restartHTTP, t *testing.T) {
h.seedResult(t, update.Result{Outcome: "succeeded", Kind: "restart", StartedAt: restartBaseTime.Add(-time.Minute), FinishedAt: &finished})
}, "idle", nil, "2026-09-30T09:49:50Z"},
{"result rolled_back", func(h *restartHTTP, t *testing.T) {
h.seedResult(t, update.Result{Outcome: "rolled_back", Kind: "update", Tag: downloadTag, StartedAt: restartBaseTime.Add(-time.Minute), FinishedAt: &finished})
}, "failed", "rolled_back", "2026-09-30T09:49:50Z"},
{"result failed", func(h *restartHTTP, t *testing.T) {
h.seedResult(t, update.Result{Outcome: "failed", Kind: "update", Tag: downloadTag, StartedAt: restartBaseTime.Add(-time.Minute), FinishedAt: &finished})
}, "failed", "install", "2026-09-30T09:49:50Z"},
{"no marker, no result", func(*restartHTTP, *testing.T) {}, "idle", nil, nil},
{"a corrupt result file", func(h *restartHTTP, t *testing.T) {
if err := os.WriteFile(h.resultPath, []byte("{not json"), 0o644); err != nil {
t.Fatal(err)
}
}, "idle", nil, nil},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
h := newRestartHTTP(t)
tc.files(h, t)
before := snapshotDirectory(t, h.dataDir)
first := h.getAs(t, updateAdminId)
second := h.getAs(t, updateAdminId)
body := decodeUpdateBody(t, first)
assertUpdateKeys(t, body)
if first.Body.String() != second.Body.String() {
t.Error("two GETs must answer identically")
}
if body["state"] != tc.state || body["error"] != tc.errKind || body["finished_at"] != tc.finished {
t.Errorf("state = %v, error = %v, finished_at = %v", body["state"], body["error"], body["finished_at"])
}
if after := snapshotDirectory(t, h.dataDir); !equalSnapshots(before, after) {
t.Errorf("a GET changed the data directory: %v -> %v", before, after)
}
})
}
})
t.Run("After a successful update the status is consistent", func(t *testing.T) {
h := newRestartHTTP(t)
h.seedResult(t, update.Result{Outcome: "succeeded", Kind: "update", Tag: updateRunning, StartedAt: restartBaseTime.Add(-time.Minute), FinishedAt: finishedAt(restartBaseTime)})
h.store.result.Latest = updateRunning
body := decodeUpdateBody(t, h.getAs(t, updateOwnerId))
if body["state"] != "succeeded" || body["available"] != false || body["current"] != updateRunning || body["finished_at"] == nil {
t.Errorf("body = %v", body)
}
})
}
type directorySnapshot map[string]string
func snapshotDirectory(t *testing.T, directory string) directorySnapshot {
t.Helper()
snapshot := directorySnapshot{}
entries, err := os.ReadDir(directory)
if err != nil {
t.Fatal(err)
}
for _, entry := range entries {
info, _ := entry.Info()
content, _ := os.ReadFile(filepath.Join(directory, entry.Name()))
snapshot[entry.Name()] = string(content) + "|" + info.ModTime().String()
}
return snapshot
}
func equalSnapshots(a, b directorySnapshot) bool {
if len(a) != len(b) {
return false
}
for name, value := range a {
if b[name] != value {
return false
}
}
return true
}
func TestRestartResultLivesUntilTheNextJob(t *testing.T) {
t.Run("The result lives until the next job", func(t *testing.T) {
rig := newDownloadRig(t)
h := newDownloadHTTP(t, rig)
rig.results.put(update.Result{Outcome: update.OutcomeRolledBack, Kind: update.RestartKindUpdate, Tag: downloadTag, StartedAt: restartBaseTime, FinishedAt: finishedAt(restartBaseTime)})
before := decodeUpdateBody(t, h.sendAs(t, http.MethodGet, "/update", updateOwnerId))
rec := h.sendAs(t, http.MethodPost, downloadPath, updateOwnerId)
ready := waitForHTTPState(t, h, "ready", "failed")
if before["state"] != "failed" || before["error"] != "rolled_back" || before["finished_at"] == nil {
t.Errorf("before = %v", before)
}
if rec.Code != http.StatusAccepted || rig.results.deletes != 1 || rig.results.found {
t.Errorf("status = %d, deletes = %d", rec.Code, rig.results.deletes)
}
assertUpdateKeys(t, ready)
if ready["state"] != "ready" || ready["error"] != nil || ready["finished_at"] != nil {
t.Errorf("ready = %v", ready)
}
})
t.Run("A download is refused while a restart is pending", func(t *testing.T) {
rig := newDownloadRig(t)
h := newDownloadHTTP(t, rig)
rig.requests.present = true
rig.results.put(update.Result{Outcome: update.OutcomeRunning, StartedAt: restartBaseTime})
rec := h.sendAs(t, http.MethodPost, downloadPath, updateOwnerId)
if rec.Code != http.StatusConflict || rig.results.deletes != 0 || rig.fetcher.totalCalls() != 0 {
t.Errorf("status = %d", rec.Code)
}
})
}
func TestHealthzIsUnchanged(t *testing.T) {
t.Run("Healthz is unchanged", func(t *testing.T) {
container := &api.UseCases{
Token: jwtauth.New("HS256", []byte("test-secret"), nil),
OpenMediaUseCase: useCases.NewOpenMediaUseCase(storage.NewLocalStorage(t.TempDir())),
}
router := api.InitRoutes(container, config.Settings{Environment: "production"})
rec := httptest.NewRecorder()
router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/healthz", nil))
if rec.Code != http.StatusOK || rec.Body.String() != "ok" {
t.Errorf("status = %d body = %q", rec.Code, rec.Body.String())
}
for name := range rec.Header() {
if strings.Contains(strings.ToLower(name), "version") {
t.Errorf("unexpected header %s", name)
}
}
})
}
func TestRunningInfoFileAtStartup(t *testing.T) {
t.Run("The server writes update-running.json at startup", func(t *testing.T) {
directory := t.TempDir()
path := filepath.Join(directory, update.RunningInfoFileName)
store := infraUpdate.NewJSONRunningInfoStore(path)
first := &fakeClock{now: restartBaseTime}
if err := useCases.NewRecordRunningInfoUseCase(store, "v2.3.0-beta", 111, first.Now).Record(); err != nil {
t.Fatal(err)
}
content, _ := os.ReadFile(path)
var written map[string]any
_ = json.Unmarshal(content, &written)
if written["version"] != "v2.3.0-beta" || written["pid"] != float64(111) || written["started_at"] != "2026-09-30T09:50:00Z" || len(written) != 3 {
t.Errorf("file = %s", content)
}
later := &fakeClock{now: restartBaseTime.Add(time.Minute)}
if err := useCases.NewRecordRunningInfoUseCase(store, "v2.3.0-beta", 222, later.Now).Record(); err != nil {
t.Fatal(err)
}
info, found := store.Read()
if !found || info.Pid != 222 || !info.StartedAt.Equal(later.now) {
t.Errorf("a second start must replace the file: %+v", info)
}
if entries, _ := os.ReadDir(directory); len(entries) != 1 {
t.Errorf("the temporary file must be renamed away, got %d entries", len(entries))
}
})
t.Run("a write error is returned and nothing panics", func(t *testing.T) {
store := infraUpdate.NewJSONRunningInfoStore(filepath.Join(t.TempDir(), "missing", update.RunningInfoFileName))
err := useCases.NewRecordRunningInfoUseCase(store, "v2.3.0-beta", 1, time.Now).Record()
if err == nil {
t.Error("the error must reach the startup code")
}
})
t.Run("the file is not served by any route", func(t *testing.T) {
container := &api.UseCases{
Token: jwtauth.New("HS256", []byte("test-secret"), nil),
OpenMediaUseCase: useCases.NewOpenMediaUseCase(storage.NewLocalStorage(t.TempDir())),
}
router := api.InitRoutes(container, config.Settings{Environment: "production"})
for _, target := range []string{"/update-running.json", "/v1/update-running.json", "/static/uploadedImages/update-running.json"} {
rec := httptest.NewRecorder()
router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, target, nil))
if rec.Code == http.StatusOK && strings.Contains(rec.Body.String(), "pid") {
t.Errorf("%s served the file", target)
}
}
})
}