v0.329.0: each app reaches only its own appdata folder; empty allow-list; drive retirement asks the agent first; plain no-durable-id refusal; :latest only for a release
gates / gates (push) Successful in 45s

R-1142: the HDD_PATH root is appdata/<own folder>, with a compiled-in folder map (paperless-ngx ->
paperless). R-981: the allow-list is empty (homepage's socket served no Felhom feature). R-1063:
finalizeDecommissionWith asks the agent before it marks the drive or moves the default; the 403/409
come back typed and reach the household as a plain 409. R-1069: the agent's no-durable-id format
refusal is ErrNoDurableID and a plain sentence. R-1143: build.sh carries :latest only for x.y.z.
Each pinned by a red-proofed test. MinAgent 0.158.1.
This commit is contained in:
2026-10-17 23:11:56 +02:00
parent 5d67041d6d
commit 2fe634141f
20 changed files with 504 additions and 78 deletions
@@ -2,8 +2,15 @@ package web
import (
"context"
"errors"
"net/http"
"strconv"
"strings"
"testing"
"gitea.dooplex.hu/admin/felhom-controller/internal/agentapi"
"gitea.dooplex.hu/admin/felhom-controller/internal/util"
"gitea.dooplex.hu/admin/felhom-controller/internal/settings"
"gitea.dooplex.hu/admin/felhom-controller/internal/stacks"
)
@@ -115,3 +122,57 @@ func TestMissingStorageLabel(t *testing.T) {
t.Errorf("unregistered path: missing=%v label=%q, want true/non-empty", missing, label)
}
}
// R-1063: the agent is asked FIRST. When it refuses — 403 (a protected role) or 409 (the whole-guest backup target) —
// nothing changes on the dashboard: the drive is not marked decommissioned, the default does not move, and the
// household reads a plain reason in its language (a 409 to the browser, like the M1 refusal).
// RED-PROOF: with SetDecommissioned + the default move back BEFORE agent.Decommission (the v0.328.0 order), the
// drive reads decommissioned and the default moved, and this test fails.
func TestFinalizeDecommission_AgentRefusalChangesNothing(t *testing.T) {
for _, c := range []struct {
status int
want error
huFrag, enFrag string
}{
{http.StatusForbidden, errDecommissionProtected, "rendszer v", "system protects this drive"},
{http.StatusConflict, errDecommissionBackupTarget, "rendszermentés helyéül", "place for the system backup"},
} {
t.Run(strconv.Itoa(c.status), func(t *testing.T) {
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})
agent := &mockAgent{decommissionErr: &agentapi.DecommissionRefusedError{Status: c.status, Reason: "refused"}}
err := s.finalizeDecommissionWith(context.Background(), agent, "/mnt/old", "")
if !errors.Is(err, c.want) {
t.Fatalf("err = %v, want the plain refusal %v", err, c.want)
}
if len(agent.decommissionCalls) != 1 {
t.Fatalf("agent asked %d times, want 1", len(agent.decommissionCalls))
}
if s.settings.IsDecommissioned("/mnt/old") {
t.Error("the drive reads decommissioned although the agent refused")
}
if got := s.settings.GetDefaultStoragePath(); got != "/mnt/old" {
t.Errorf("default moved to %q although the agent refused", got)
}
if !isDecommissionPolicyRefusal(err) {
t.Error("the refusal is not mapped to 409 (the CF edge would swallow a 502's body)")
}
if hu := util.ErrText("hu", err); !strings.Contains(hu, c.huFrag) || !strings.Contains(hu, "Semmi nem v") {
t.Errorf("HU text = %q", hu)
}
if en := util.ErrText("en", err); !strings.Contains(en, c.enFrag) || !strings.Contains(en, "Nothing was changed") {
t.Errorf("EN text = %q", en)
}
})
}
// Any other agent failure is still the old „failed" message, and changes nothing either.
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})
err := s.finalizeDecommissionWith(context.Background(), &mockAgent{decommissionErr: errors.New("agentapi: dial: refused")}, "/mnt/old", "")
if err == nil || isDecommissionPolicyRefusal(err) || s.settings.IsDecommissioned("/mnt/old") {
t.Fatalf("a transport failure: err=%v policy=%v marked=%v", err, isDecommissionPolicyRefusal(err), s.settings.IsDecommissioned("/mnt/old"))
}
}