Files
felhom-controller/controller/internal/agentapi/crashguard_test.go
T
admin 2fe634141f
gates / gates (push) Successful in 45s
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
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.
2026-10-17 23:11:56 +02:00

88 lines
3.5 KiB
Go

package agentapi
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
)
// R-856: GET /host/crash-guard decodes the crash guard's fields, and an agent that predates the route
// surfaces as a typed 404 (the caller reads it as unknown → the normal boot grace).
func TestR856_CrashGuardDecodesAndAnOlderAgentIs404(t *testing.T) {
mux := http.NewServeMux()
mux.HandleFunc("GET /host/crash-guard", func(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte(`{"ok":true,"data":{"present":true,"last_boot_at":"2026-10-04T12:00:00Z","last_boot_unclean":true,"tripped":false}}`))
})
s := httptest.NewTLSServer(mux)
defer s.Close()
c := clientFor(t, s, strings.TrimPrefix(s.URL, "https://"))
st, err := c.CrashGuard(context.Background())
if err != nil {
t.Fatal(err)
}
if !st.Present || !st.LastBootUnclean || st.LastBootAt != "2026-10-04T12:00:00Z" || st.Tripped {
t.Fatalf("state = %+v", st)
}
old := httptest.NewTLSServer(http.NewServeMux())
defer old.Close()
_, err = clientFor(t, old, strings.TrimPrefix(old.URL, "https://")).CrashGuard(context.Background())
var se *StatusError
if !errors.As(err, &se) || se.Code != http.StatusNotFound {
t.Fatalf("an older agent must answer a typed 404, got %v", err)
}
}
// R-1063: the agent's two decommission refusals come back TYPED (403 a protected role, 409 the whole-guest backup
// target), with the same text as before; any other failure is not that type.
func TestDecommission_PolicyRefusalsAreTyped(t *testing.T) {
for _, c := range []struct {
code int
typed bool
}{{http.StatusForbidden, true}, {http.StatusConflict, true}, {http.StatusBadGateway, false}} {
mux := http.NewServeMux()
mux.HandleFunc("POST /disks/decommission", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(c.code)
_, _ = w.Write([]byte(`{"ok":false,"error":"refused for the test"}`))
})
s := httptest.NewTLSServer(mux)
_, err := clientFor(t, s, strings.TrimPrefix(s.URL, "https://")).Decommission(context.Background(), "/mnt/x")
s.Close()
var dr *DecommissionRefusedError
if got := errors.As(err, &dr); got != c.typed {
t.Fatalf("HTTP %d: typed=%v, want %v (err %v)", c.code, got, c.typed, err)
}
if c.typed && (dr.Status != c.code || !strings.Contains(err.Error(), "HTTP "+strconv.Itoa(c.code)+": refused for the test")) {
t.Fatalf("HTTP %d: %+v / %q", c.code, dr, err)
}
}
}
// R-1069: the agent's anti-retarget refusal of a blank format (its exact text, a 409) comes back as ErrNoDurableID;
// another 409 does not.
func TestFormatDisk_NoDurableIDIsTyped(t *testing.T) {
for _, c := range []struct {
body string
typed bool
}{
{`{"ok":false,"error":"format refused: device has no durable id to bind the format to (path-only formats are not permitted)","data":{"formatted":false,"data_bearing":false}}`, true},
{`{"ok":false,"error":"format refused (device may have changed since inspection): gone","data":{"formatted":false}}`, false},
} {
mux := http.NewServeMux()
mux.HandleFunc("POST /disks/format", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusConflict)
_, _ = w.Write([]byte(c.body))
})
s := httptest.NewTLSServer(mux)
_, err := clientFor(t, s, strings.TrimPrefix(s.URL, "https://")).FormatDisk(context.Background(), "/dev/vdb", "ext4", false, "")
s.Close()
if got := errors.Is(err, ErrNoDurableID); got != c.typed || err == nil {
t.Fatalf("body %s: ErrNoDurableID=%v want %v (err %v)", c.body[:60], got, c.typed, err)
}
}
}