feat: complete dockerization (optional)

This commit is contained in:
fsylvain committed 2026-10-05 04:52:49 +02:00
1 parent 3e01c29657
commit d275ba3c14
38 files changed
+1244 -20

No files matched your search

+198
View File
@@ -0,0 +1,198 @@
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)
}
}
}
+1 -1
View File
@@ -386,7 +386,7 @@ func (r *downloadRig) rebuild() {
FeedURL: downloadFeedURL,
Now: func() time.Time { return updateTestCheckedAt },
})
r.status = useCases.NewGetUpdateStatusUseCase(r.store, r.jobs, r.helper, r.results, r.requests, updateRunning, time.Now)
r.status = useCases.NewGetUpdateStatusUseCase(r.store, r.jobs, r.helper, r.results, r.requests, updateRunning, false, time.Now)
}
func (r *downloadRig) checksumsFor(name string, content []byte) string {
+1 -1
View File
@@ -28,7 +28,7 @@ const (
updateRunning = "v2.2.0-beta"
)
var updateStatusKeys = []string{"available", "can_apply", "checked_at", "current", "error", "finished_at", "latest", "notes", "progress", "release_url", "started_at", "state"}
var updateStatusKeys = []string{"available", "can_apply", "checked_at", "container", "current", "error", "finished_at", "latest", "notes", "progress", "release_url", "started_at", "state"}
type updateHarness struct {
router http.Handler
+1 -1
View File
@@ -13,7 +13,7 @@ import (
)
func newUpdateStatusUseCase(store update.StatusStore, jobs update.JobStore, helper update.HelperDetector, running string) *useCases.GetUpdateStatusUseCase {
return useCases.NewGetUpdateStatusUseCase(store, jobs, helper, &fakeResultStore{}, &fakeRestartRequestStore{}, running, time.Now)
return useCases.NewGetUpdateStatusUseCase(store, jobs, helper, &fakeResultStore{}, &fakeRestartRequestStore{}, running, false, time.Now)
}
type fakeRestartRequestStore struct {
+1 -1
View File
@@ -58,7 +58,7 @@ func newRestartHTTP(t *testing.T) *restartHTTP {
Token: h.token,
GetUserUseCase: useCases.NewGetUserUseCase(newUpdateUserRepo()),
GetUpdateStatusUseCase: useCases.NewGetUpdateStatusUseCase(
h.store, h.jobs, h.helper, h.resultFiles, h.markers, updateRunning, h.clock.Now),
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),
}
+1 -1
View File
@@ -32,7 +32,7 @@ func newRestartRig(running string) *restartRig {
clock: &fakeClock{now: restartBaseTime},
}
rig.request = useCases.NewRequestRestartUseCase(rig.store, rig.jobs, rig.helper, rig.results, rig.requests, running, rig.clock.Now)
rig.status = useCases.NewGetUpdateStatusUseCase(rig.store, rig.jobs, rig.helper, rig.results, rig.requests, running, rig.clock.Now)
rig.status = useCases.NewGetUpdateStatusUseCase(rig.store, rig.jobs, rig.helper, rig.results, rig.requests, running, false, rig.clock.Now)
return rig
}