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