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) } } }