Files
pad/internal/server/task3585_create_limit_test.go
T
xarmian 6dbe15e795 fix(items): a keyed create claims its key before the plan-limit check (TASK-3585) (#2088)
Retro CONVE-735 finding on #2065. handleCreateItem enforced the workspace
item limit before claiming the idempotency key, so a create that
succeeded AT the limit and lost its answer could not replay: its retry met
the limit its own success had reached. The claim now comes first; the
request is not modified in between, so the key's hash is unchanged, and
every refusal after it (the limit included) still releases the key.

Claude-Session: https://claude.ai/code/session_01Q9WssCjFuHjSfLABoW4tmS
2026-10-11 20:34:31 -04:00

34 lines
1.4 KiB
Go

package server
import (
"encoding/json"
"net/http"
"testing"
)
// Retro CONVE-735 finding on #2065: create checked the workspace item limit
// before claiming its idempotency key, so a create that succeeded AT the
// limit and lost its answer could not replay: the retry met the limit its
// own success had reached. The claim now comes first. The control: a NEW
// create at the cap is still refused.
func TestTASK3585_CreateAtTheLimitReplays(t *testing.T) {
e := newRestoreLimitEnv(t)
e.setCap(t, e.liveItems(t)+1)
path := "/api/v1/workspaces/" + e.home.Slug + "/collections/tasks/items"
body, _ := json.Marshal(map[string]any{"title": "The last one", "fields": `{"status":"open"}`, "op_key": "at-cap"})
first := e.do("POST", path, "application/json", body, "")
if first.Code != http.StatusCreated {
t.Fatalf("the create that reaches the cap: %d %s", first.Code, first.Body.String())
}
again := e.do("POST", path, "application/json", body, "")
if again.Code != http.StatusCreated || again.Header().Get("Idempotent-Replayed") != "true" {
t.Fatalf("its retry must replay, not meet the cap: %d %s", again.Code, again.Body.String())
}
other, _ := json.Marshal(map[string]any{"title": "One too many", "fields": `{"status":"open"}`, "op_key": "over-cap"})
if rr := e.do("POST", path, "application/json", other, ""); rr.Code == http.StatusCreated {
t.Fatalf("a new create at the cap must still be refused: %d", rr.Code)
}
}