package api import ( "errors" "fmt" "net/http" "os" "strings" "testing" "gitea.dooplex.hu/admin/felhom-controller/internal/stacks" "gitea.dooplex.hu/admin/felhom-controller/internal/util" ) // R-569 — the stop/start/restart/update, remove and delete handlers pick their status by the // refusal's KIND. Every row passes a REWORDED message: the words the handlers used to match // ("protected", "not found", "not deployed", "still running", "not orphaned") are gone, only the // kind remains. RED-PROOF (REPORT): restore any strings.Contains chain and its family's reworded rows // answer 500. func TestR569_StackOpStatus_SurvivesRewording(t *testing.T) { cases := []struct { family, name string err error want int }{ // action family (start/stop/restart/update) {"action", "unknown app, reworded", util.KindErrorf(stacks.ErrStackNotFound, "no app called %q", "x"), http.StatusNotFound}, {"action", "unknown app, wrapped by the caller", fmt.Errorf("restarting: %w", util.KindErrorf(stacks.ErrStackNotFound, "no app called %q", "x")), http.StatusNotFound}, {"action", "protected, reworded", util.KindErrorf(stacks.ErrProtectedStack, "%q is infrastructure", "traefik"), http.StatusForbidden}, {"action", "update refusal", &stacks.UpdateRefusal{Reason: "busy", Message: "foglalt"}, http.StatusConflict}, {"action", "update refusal: unknown app", &stacks.UpdateRefusal{Reason: "not_found", Message: "nincs ilyen"}, http.StatusNotFound}, // remove family {"remove", "not deployed, reworded", util.KindErrorf(stacks.ErrNotDeployed, "%q has nothing installed", "x"), http.StatusConflict}, {"remove", "still running, reworded", util.KindErrorf(stacks.ErrStillRunning, "%q is up — stop it first", "x"), http.StatusConflict}, {"remove", "protected, reworded", util.KindErrorf(stacks.ErrProtectedStack, "cannot take %q away", "traefik"), http.StatusForbidden}, // delete family {"delete", "not orphaned, reworded", util.KindErrorf(stacks.ErrNotOrphaned, "%q still belongs to the catalog", "x"), http.StatusConflict}, {"delete", "still running, reworded", util.KindErrorf(stacks.ErrStillRunning, "%q is up", "x"), http.StatusConflict}, {"delete", "unknown, reworded", util.KindErrorf(stacks.ErrStackNotFound, "nothing named %q", "x"), http.StatusNotFound}, // the negative half: the old WORDS without a kind are not a refusal {"any", "text that merely contains the old words", errors.New("docker: image not found; protected; still running"), http.StatusInternalServerError}, {"any", "anything else", errors.New("compose exploded"), http.StatusInternalServerError}, } for _, c := range cases { if got := stackOpStatusFor(c.err); got != c.want { t.Errorf("%s / %s: stackOpStatusFor(%q) = %d, want %d", c.family, c.name, c.err.Error(), got, c.want) } } } // The seam must be WIRED in all three handlers, and none may decide by reading the error text again. func TestR569_HandlersUseTheKind(t *testing.T) { src, err := os.ReadFile("router.go") if err != nil { t.Fatal(err) } body := string(src) for _, fn := range []string{"func (r *Router) actionStack(", "func (r *Router) removeStack(", "func (r *Router) deleteStack("} { i := strings.Index(body, fn) if i < 0 { t.Fatalf("%s not found — this test no longer reads what it thinks it reads", fn) } h := body[i:] if j := strings.Index(h[10:], "\nfunc "); j > 0 { h = h[:j+10] } if !strings.Contains(h, "stackOpStatusFor(err)") { t.Errorf("%s no longer asks stackOpStatusFor for the status code", fn) } for _, line := range strings.Split(h, "\n") { if strings.HasPrefix(strings.TrimSpace(line), "//") { continue } if strings.Contains(line, "strings.Contains(err.Error()") { t.Errorf("%s decides by reading the error text again (R-569): %s", fn, strings.TrimSpace(line)) } } } }