@@ 225,6 225,15 @@ func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
// Delete handles DELETE /saved-searches/{name}. Returns 204 No Content on
// success, 404 when the row does not exist.
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
+ // Defensive: reject any explicit ?owner= param to prevent confusion (IV2).
+ if r.URL.Query().Get("owner") != "" {
+ apierror.Render(w, r, culpa.WithCode(
+ culpa.WithPublic(culpa.New("?owner= not accepted on write paths"), "?owner= is not accepted on saved-search write paths"),
+ "INVALID",
+ ))
+ return
+ }
+
owner := h.ownerOf(r)
name := chi.URLParam(r, "name")
@@ 330,3 330,42 @@ func TestHandler_Delete_Sequence(t *testing.T) {
t.Fatalf("expected NOT_FOUND; got %q", p.Code)
}
}
+
+// TestHandler_WritePaths_OwnerParamRejected verifies IV2 — every write path
+// (POST/PUT/DELETE) returns 400 INVALID when ?owner= is set, regardless of
+// the value. Owner derivation is exclusively from the auth identity.
+func TestHandler_WritePaths_OwnerParamRejected(t *testing.T) {
+ h := newHandler(t)
+ router := mountWithIdentity(h, auth.Identity{User: "alice"})
+
+ // Seed a row so the PUT/DELETE paths have a target name.
+ doPOST(t, router, "/api/v1/saved-searches", map[string]string{"name": "x", "query": "q"})
+
+ cases := []struct {
+ name string
+ send func() *httptest.ResponseRecorder
+ }{
+ {"POST", func() *httptest.ResponseRecorder {
+ return doPOST(t, router, "/api/v1/saved-searches?owner=alice", map[string]string{"name": "y", "query": "q"})
+ }},
+ {"PUT", func() *httptest.ResponseRecorder {
+ return doPUT(t, router, "/api/v1/saved-searches/x?owner=bob", map[string]*string{"query": ptrStr("q2")})
+ }},
+ {"DELETE", func() *httptest.ResponseRecorder {
+ return doDELETE(t, router, "/api/v1/saved-searches/x?owner=*")
+ }},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ rec := tc.send()
+ if rec.Code != http.StatusBadRequest {
+ t.Fatalf("status=%d; want 400; body=%s", rec.Code, rec.Body.String())
+ }
+ var p problemBody
+ _ = json.Unmarshal(rec.Body.Bytes(), &p)
+ if p.Code != "INVALID" {
+ t.Fatalf("expected INVALID; got %q (body=%s)", p.Code, rec.Body.String())
+ }
+ })
+ }
+}