Files
RenewCMS/test/container_update_guidance_http_test.go
T

199 lines
6.9 KiB
Go

package test
import (
"RenewCMS/api"
validator "RenewCMS/api/pkg/validator"
"RenewCMS/internal/domain/update"
"RenewCMS/internal/infrastructure/config"
infraupdate "RenewCMS/internal/infrastructure/update"
"RenewCMS/internal/infrastructure/useCases"
"net/http"
"testing"
"time"
"github.com/go-chi/jwtauth/v5"
)
type containerHarness struct {
*updateHarness
fetcher *fakeAssetFetcher
requests *fakeRestartRequestStore
jobs *fakeJobStore
}
func newContainerHarness(t *testing.T, container bool, helperInstalled bool) *containerHarness {
t.Helper()
validator.InitValidator()
rig := newDownloadRig(t)
rig.helper.installed = helperInstalled
helper := infraupdate.NewContainerHelperDetector(rig.helper, container)
now := func() time.Time { return updateTestCheckedAt }
token := jwtauth.New("HS256", []byte("test-secret"), nil)
feed := &fakeReleaseFeed{release: update.Release{
Tag: "v2.3.0-beta",
URL: "https://git.example/fsylvain/RenewCMS/releases/tag/v2.3.0-beta",
PublishedAt: time.Date(2026, 9, 24, 8, 0, 0, 0, time.UTC),
Notes: update.ReleaseNotes{Version: "v2.3.0-beta", Items: []string{"Media library"}},
}}
rig.store.found = true
start := useCases.NewStartUpdateDownloadUseCase(useCases.StartUpdateDownloadDependencies{
Store: rig.store,
Jobs: rig.jobs,
Helper: helper,
Results: rig.results,
Requests: rig.requests,
Fetcher: rig.fetcher,
Verifier: nil,
Runner: rig.runner,
Backup: rig.backup,
Staging: rig.staging,
Logger: rig.logger,
CurrentVersion: updateRunning,
Platform: rig.platform,
FeedURL: downloadFeedURL,
Now: now,
})
routes := &api.UseCases{
Token: token,
GetUserUseCase: useCases.NewGetUserUseCase(newUpdateUserRepo()),
GetUpdateStatusUseCase: useCases.NewGetUpdateStatusUseCase(rig.store, rig.jobs, helper, rig.results, rig.requests, updateRunning, container, now),
CheckForUpdateUseCase: useCases.NewCheckForUpdateUseCase(feed, rig.store, updateRunning, now),
StartUpdateDownloadUseCase: start,
RequestRestartUseCase: useCases.NewRequestRestartUseCase(rig.store, rig.jobs, helper, rig.results, rig.requests, updateRunning, now),
}
return &containerHarness{
updateHarness: &updateHarness{router: api.NewSystemRouter(routes), token: token, feed: feed, store: rig.store},
fetcher: rig.fetcher,
requests: rig.requests,
jobs: rig.jobs,
}
}
func TestContainerUpdateGuidanceHTTP(t *testing.T) {
t.Run("The status carries the container flag", func(t *testing.T) {
cases := []struct {
value string
flag bool
}{{"true", true}, {"TRUE", true}, {"false", false}, {"", false}, {"yes", false}}
for _, tc := range cases {
t.Run("value "+tc.value, func(t *testing.T) {
t.Setenv("SMTP_PORT", "1")
t.Setenv("AUTH_RATE_LIMIT_MAX_ATTEMPTS", "1")
t.Setenv("AUTH_RATE_LIMIT_WINDOW_SECONDS", "1")
t.Setenv("RENEWCMS_CONTAINER", tc.value)
settings, err := config.NewSettingsFromEnv()
if err != nil {
t.Fatal(err)
}
if settings.Container != tc.flag {
t.Fatalf("Container = %v, want %v", settings.Container, tc.flag)
}
h := newContainerHarness(t, settings.Container, false)
body := decodeUpdateBody(t, h.sendAs(t, http.MethodGet, "/update", updateOwnerId))
assertUpdateKeys(t, body)
if body["container"] != tc.flag {
t.Errorf("container = %v, want %v", body["container"], tc.flag)
}
})
}
})
t.Run("Container mode disables apply even with a helper", func(t *testing.T) {
h := newContainerHarness(t, true, true)
h.store.result = newStoredUpdateResult()
for name, userId := range map[string]uint32{"owner": updateOwnerId, "admin": updateAdminId} {
body := decodeUpdateBody(t, h.sendAs(t, http.MethodGet, "/update", userId))
if body["can_apply"] != false || body["container"] != true {
t.Errorf("%s: body = %v", name, body)
}
if body["available"] != true || body["latest"] != "v2.3.0-beta" || body["notes"] == nil {
t.Errorf("%s: body = %v", name, body)
}
}
})
t.Run("Host installs are unchanged", func(t *testing.T) {
h := newContainerHarness(t, false, true)
h.store.result = newStoredUpdateResult()
body := decodeUpdateBody(t, h.sendAs(t, http.MethodGet, "/update", updateOwnerId))
if body["container"] != false || body["can_apply"] != true {
t.Errorf("body = %v", body)
}
})
t.Run("The check still works in a container", func(t *testing.T) {
h := newContainerHarness(t, true, true)
h.store.found = false
rec := h.sendAs(t, http.MethodPost, "/update/check", updateOwnerId)
body := decodeUpdateBody(t, rec)
if rec.Code != http.StatusOK || body["available"] != true || body["container"] != true {
t.Fatalf("status = %d body = %v", rec.Code, body)
}
if h.store.saves != 1 || h.store.result.Latest != "v2.3.0-beta" {
t.Errorf("saves = %d result = %+v", h.store.saves, h.store.result)
}
})
t.Run("Download and restart are refused in a container", func(t *testing.T) {
for _, path := range []string{"/update/download", "/update/restart"} {
t.Run(path, func(t *testing.T) {
h := newContainerHarness(t, true, true)
h.store.result = newStoredUpdateResult()
rec := h.sendAs(t, http.MethodPost, path, updateOwnerId)
if rec.Code != http.StatusConflict {
t.Fatalf("status = %d: %s", rec.Code, rec.Body.String())
}
if _, found := h.jobs.Load(); found {
t.Error("no job may start")
}
if len(h.fetcher.calls) != 0 {
t.Errorf("fetcher calls = %v", h.fetcher.calls)
}
if h.requests.writes != 0 {
t.Errorf("restart request writes = %d", h.requests.writes)
}
})
}
})
}
func TestContainerHelperDetectorTruthTable(t *testing.T) {
for _, tc := range []struct {
container, inner, want bool
}{{false, false, false}, {false, true, true}, {true, false, false}, {true, true, false}} {
got := infraupdate.NewContainerHelperDetector(&fakeHelperDetector{installed: tc.inner}, tc.container).Installed()
if got != tc.want {
t.Errorf("container=%v inner=%v: Installed() = %v, want %v", tc.container, tc.inner, got, tc.want)
}
}
}
func TestContainerStatusUseCaseCanApplyTruthTable(t *testing.T) {
for _, tc := range []struct {
isOwner, container, helper, want bool
}{
{true, false, true, true},
{false, false, true, false},
{true, true, true, false},
{true, false, false, false},
{false, true, true, false},
} {
rig := newDownloadRig(t)
rig.helper.installed = tc.helper
status := useCases.NewGetUpdateStatusUseCase(rig.store, rig.jobs, infraupdate.NewContainerHelperDetector(rig.helper, tc.container), rig.results, rig.requests, updateRunning, tc.container, func() time.Time { return updateTestCheckedAt }).GetStatus(tc.isOwner)
if status.CanApply != tc.want || status.Container != tc.container {
t.Errorf("%+v: CanApply=%v Container=%v", tc, status.CanApply, status.Container)
}
}
}