Files
felhom-controller/controller/internal/web/r1144_decommission_anyway_test.go
T

102 lines
4.9 KiB
Go

package web
import (
"context"
"net/http"
"reflect"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
"gitea.dooplex.hu/admin/felhom-controller/internal/util"
)
// fakeDecomStacks is the stack-manager seam of „Végleges leszerelés — mégis": it records stops and starts and never
// reaches docker.
type fakeDecomStacks struct {
list []stacks.Stack
hdd map[string]string
stops, starts []string
}
func (f *fakeDecomStacks) GetStacks() []stacks.Stack { return f.list }
func (f *fakeDecomStacks) LoadAppConfigByName(n string) *stacks.AppConfig {
return &stacks.AppConfig{Deployed: true, Env: map[string]string{"HDD_PATH": f.hdd[n]}}
}
func (f *fakeDecomStacks) StopStack(n string) error { f.stops = append(f.stops, n); return nil }
func (f *fakeDecomStacks) StartStack(n string) error { f.starts = append(f.starts, n); return nil }
func r1144Setup(t *testing.T) (*Server, *fakeDecomStacks) {
t.Helper()
s := testServer(t)
_ = s.settings.AddStoragePath(settings.StoragePath{Path: "/mnt/old", Schedulable: true, IsDefault: true})
_ = s.settings.AddStoragePath(settings.StoragePath{Path: "/mnt/new", Schedulable: true})
st := func(n string, state stacks.ContainerState) stacks.Stack {
return stacks.Stack{Name: n, Deployed: true, State: state, Meta: stacks.Metadata{DisplayName: strings.ToUpper(n)}}
}
f := &fakeDecomStacks{
list: []stacks.Stack{st("immich", stacks.StateRunning), st("jellyfin", stacks.StateUnhealthy), st("komga", stacks.StateStopped), st("other", stacks.StateRunning)},
hdd: map[string]string{"immich": "/mnt/old", "jellyfin": "/mnt/old", "komga": "/mnt/old", "other": "/mnt/new"},
}
return s, f
}
// R-1144: the agent refuses AFTER the apps were stopped (403 here; its disk list gave no verdict) → exactly the apps
// that were running before the press run again (immich, jellyfin — not komga, which was stopped, and not other, on
// another drive), the drive is not marked, and the household reads both sentences.
// RED-PROOF: without the restart leg in decommissionAnyway, starts = [] and this test fails.
func TestR1144_RefusalAfterStopStartsTheRunningAppsAgain(t *testing.T) {
for _, code := range []int{http.StatusForbidden, http.StatusConflict} {
s, f := r1144Setup(t)
agent := &mockAgent{decommissionErr: &agentapi.DecommissionRefusedError{Status: code, Reason: "no"}}
stopped, all, err := s.decommissionAnyway(context.Background(), agent, f, "/mnt/old")
if err == nil || !driveUntouched(err) {
t.Fatalf("HTTP %d: err = %v, want a policy refusal", code, err)
}
if !reflect.DeepEqual(f.stops, []string{"immich", "jellyfin", "komga"}) {
t.Fatalf("stops = %v", f.stops)
}
if !reflect.DeepEqual(f.starts, []string{"immich", "jellyfin"}) || !all {
t.Fatalf("HTTP %d: starts = %v (all=%v), want exactly the apps that were running", code, f.starts, all)
}
if len(stopped) != 3 || s.settings.IsDecommissioned("/mnt/old") {
t.Fatalf("stopped=%v marked=%v", stopped, s.settings.IsDecommissioned("/mnt/old"))
}
for lang, frag := range map[string]string{"hu": "újra futnak", "en": "running again"} {
if !strings.Contains(util.Text(lang, "disk.decommission.apps_restarted"), frag) {
t.Errorf("[%s] the restart sentence lacks %q", lang, frag)
}
}
}
}
// When the agent's own disk list already says no (a protected role, or the whole-guest backup target), NOTHING is
// stopped: the refusal comes before the first StopStack.
func TestR1144_AgentVerdictBeforeAnythingIsStopped(t *testing.T) {
for name, d := range map[string]agentapi.DiskInfo{
"protected role": {MountPath: "/mnt/old", Role: "backup"},
"backup target": {MountPath: "/mnt/old", Role: "user-data", BackupTarget: true},
} {
s, f := r1144Setup(t)
agent := &mockAgent{disks: agentapi.DisksResponse{Disks: []agentapi.DiskInfo{d}}}
_, _, err := s.decommissionAnyway(context.Background(), agent, f, "/mnt/old")
if !isDecommissionPolicyRefusal(err) || len(f.stops) != 0 || len(agent.decommissionCalls) != 0 {
t.Fatalf("%s: err=%v stops=%v agent calls=%v — want a refusal before anything is stopped", name, err, f.stops, agent.decommissionCalls)
}
}
}
// The agent says yes (200) → retired as before: marked, apps stay stopped, nothing restarted.
func TestR1144_AcceptedRetirementKeepsTheAppsStopped(t *testing.T) {
s, f := r1144Setup(t)
agent := &mockAgent{disks: agentapi.DisksResponse{Disks: []agentapi.DiskInfo{{MountPath: "/mnt/old", Role: "user-data"}}}}
if _, all, err := s.decommissionAnyway(context.Background(), agent, f, "/mnt/old"); err != nil || !all {
t.Fatalf("err=%v all=%v", err, all)
}
if len(f.starts) != 0 || !s.settings.IsDecommissioned("/mnt/old") {
t.Fatalf("starts=%v marked=%v — a retired drive's apps must stay stopped", f.starts, s.settings.IsDecommissioned("/mnt/old"))
}
}