From 5b0686dc06756b284acdab35976d0084b68876ee Mon Sep 17 00:00:00 2001 From: Lemon-miaow Date: Sun, 27 Sep 2026 12:49:46 +0800 Subject: [PATCH] =?UTF-8?q?fix(api):=20=E5=81=9C=E6=9C=8D=E6=97=B6=20CR=20?= =?UTF-8?q?=E5=B7=B2=E4=B8=8D=E5=AD=98=E5=9C=A8=E8=BF=94=E5=9B=9E=20404=20?= =?UTF-8?q?not=5Ffound=EF=BC=8C=E4=B8=8E=E5=94=A4=E9=86=92=E4=B8=80?= =?UTF-8?q?=E8=87=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/api/api_test.go | 3 ++ internal/api/handlers_stop_test.go | 66 ++++++++++++++++++++++++++++++ internal/api/handlers_user.go | 2 +- 3 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 internal/api/handlers_stop_test.go diff --git a/internal/api/api_test.go b/internal/api/api_test.go index da23b5d..e19b74f 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -1853,6 +1853,9 @@ func (c *fakeCluster) WorldVolumeExists(_ context.Context, n string) (bool, erro } func (c *fakeCluster) SetDesiredState(_ context.Context, n string, s v1alpha1.DesiredState) error { + if _, ok := c.byName[n]; !ok { + return ErrNotFound // the MinecraftServer is gone, as K8sCluster.getServer reports it + } if err := c.wakeErr[n]; err != nil && s == v1alpha1.DesiredRunning { return err } diff --git a/internal/api/handlers_stop_test.go b/internal/api/handlers_stop_test.go new file mode 100644 index 0000000..13f34a3 --- /dev/null +++ b/internal/api/handlers_stop_test.go @@ -0,0 +1,66 @@ +package api + +import ( + "net/http" + "testing" + + "felis.lolicon.best/internal/apis/felis/v1alpha1" +) + +// A stop flips desiredState to Stopped for the server's owner or an admin. A +// server whose database row outlived its MinecraftServer answers 404 not_found, +// the same as a wake of it, where it used to be an opaque 500. +func TestStop(t *testing.T) { + mk := func(p *Principal, inCluster bool) (*API, *fakeCluster, *fakeRepo) { + repo := newFakeRepo() + repo.byName["survival"] = &ServerRecord{Name: "survival", OwnerID: "u1"} + cl := newFakeCluster() + if inCluster { + cl.byName["survival"] = &ServerInfo{Name: "survival", DesiredState: string(v1alpha1.DesiredRunning)} + } + api := newTestAPI(repo, cl) + api.External = staticExternal{p: p} + return api, cl, repo + } + owner := &Principal{UserID: "u1", Role: "user"} + stop := func(api *API) (int, string) { + w := do(api.ExternalHandler(), "POST", "/api/v1/servers/survival/stop", "", nil) + if w.Code == http.StatusAccepted { + return w.Code, "" + } + return w.Code, decodeErr(t, w) + } + + t.Run("the owner stops it", func(t *testing.T) { + api, cl, repo := mk(owner, true) + if code, e := stop(api); code != http.StatusAccepted { + t.Fatalf("stop = %d %s, want 202", code, e) + } + if cl.desired["survival"] != v1alpha1.DesiredStopped { + t.Fatalf("desired = %q, want Stopped", cl.desired["survival"]) + } + if len(repo.audits) != 1 || repo.audits[0].Action != "stop" || repo.audits[0].ServerName != "survival" { + t.Fatalf("audits = %+v, want one stop of survival", repo.audits) + } + }) + + t.Run("someone else is refused", func(t *testing.T) { + api, cl, repo := mk(&Principal{UserID: "u2", Role: "user"}, true) + if code, e := stop(api); code != http.StatusForbidden || e != "forbidden" { + t.Fatalf("stop = %d %s, want 403 forbidden", code, e) + } + if _, set := cl.desired["survival"]; set || len(repo.audits) != 0 { + t.Fatalf("a refused stop wrote desired=%v audits=%+v", cl.desired, repo.audits) + } + }) + + t.Run("its MinecraftServer is gone", func(t *testing.T) { + api, _, repo := mk(owner, false) + if code, e := stop(api); code != http.StatusNotFound || e != "not_found" { + t.Fatalf("stop = %d %s, want 404 not_found", code, e) + } + if len(repo.audits) != 0 { + t.Fatalf("audits = %+v, want none", repo.audits) + } + }) +} diff --git a/internal/api/handlers_user.go b/internal/api/handlers_user.go index 9928bd6..20ab405 100644 --- a/internal/api/handlers_user.go +++ b/internal/api/handlers_user.go @@ -112,7 +112,7 @@ func (a *API) handleStop(w http.ResponseWriter, r *http.Request) { } if err := a.Cluster.SetDesiredState(r.Context(), name, v1alpha1.DesiredStopped); err != nil { - writeError(w, r, err) + a.writeLookupError(w, r, err) // a row whose MinecraftServer is gone is 404, as for a wake return } a.audit(r, "stop", name)