fix(12.2): close code-review blockers on uploads, JSON caps, and pivot fill

Keep form save behind in-flight uploads, make retries idempotent via X-Upload-Id, cap remaining JSON bodies, and surface pending pivot type errors instead of zeroing them.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-10-02 23:10:48 +02:00
parent 6bfc0faa8a
commit 516f9c9025
22 changed files with 533 additions and 96 deletions

View File

@@ -65,9 +65,19 @@ func TestFileuploadCompile(t *testing.T) {
}
err = activateDeferred(t, dfPlugin{fsys: dfOverlay(t, map[string]string{"models/gadget/fields.yaml": dfGadgetFields(photo(" maxFilesize: 2\n"))})},
map[string]any{"http.body_limits.upload_bytes": 1048576})
if err == nil || !strings.Contains(err.Error(), "field photos: maxFilesize exceeds http.body_limits.upload_bytes") {
if err == nil || !strings.Contains(err.Error(), "field photos: maxFilesize plus multipart overhead exceeds http.body_limits.upload_bytes") {
t.Fatalf("maxFilesize over upload_bytes: %v", err)
}
// Equality leaves no room for multipart framing.
err = activateDeferred(t, dfPlugin{fsys: dfOverlay(t, map[string]string{"models/gadget/fields.yaml": dfGadgetFields(photo(" maxFilesize: 1\n"))})},
map[string]any{"http.body_limits.upload_bytes": 1048576})
if err == nil || !strings.Contains(err.Error(), "field photos: maxFilesize plus multipart overhead exceeds http.body_limits.upload_bytes") {
t.Fatalf("maxFilesize equal to upload_bytes: %v", err)
}
if err := activateDeferred(t, dfPlugin{fsys: dfOverlay(t, map[string]string{"models/gadget/fields.yaml": dfGadgetFields(photo(" maxFilesize: 1\n"))})},
map[string]any{"http.body_limits.upload_bytes": 1048576 + 64<<10}); err != nil {
t.Fatalf("maxFilesize with 64 KiB headroom: %v", err)
}
if err := bootGadgetFields(t, dfGadgetFields(photo(" mode: image\n fileTypes: jpg|png\n mimeTypes: image/png, png\n maxFiles: 2\n imageWidth: 120\n thumbOptions:\n mode: fit\n useCaption: true\n prompt: Drop\n"))); err != nil {
t.Fatalf("every key: %v", err)
}
@@ -332,3 +342,41 @@ func TestFileuploadMIME(t *testing.T) {
}
want(t, "text", env.a.upload(t, dfPath(g, "/files/manual"), "a.txt", []byte("hello"), sk(key)), http.StatusCreated)
}
// TestFileuploadUploadID: a repeated X-Upload-Id returns the stored file
// instead of creating a second binding (CR-02).
func TestFileuploadUploadID(t *testing.T) {
env := newDeferredEnv(t)
key := newSessionKey(t)
h := map[string]string{cabana.SessionKeyHeader: key, "X-Upload-Id": "retry-one"}
first := env.a.upload(t, photosPath(0, ""), "a.png", conformPNG(t), h)
want(t, "first upload", first, http.StatusCreated)
id := dataID(t, first.Body.Bytes())
again := env.a.upload(t, photosPath(0, ""), "b.png", conformPNG(t), h)
want(t, "same upload id", again, http.StatusCreated)
if got := dataID(t, again.Body.Bytes()); got != id {
t.Fatalf("retry created %d, want %d", got, id)
}
if got := fileItems(t, env.a, 0, "photos", key); len(got) != 1 || got[0].ID != id {
t.Fatalf("list = %+v", got)
}
}
// TestJSONBodyCaps: create, update, link, unlink and settings refuse a
// JSON body past http.body_limits.default_bytes (CR-03).
func TestJSONBodyCaps(t *testing.T) {
env := newDeferredEnv(t)
huge := strings.Repeat("x", 1100<<10)
want(t, "create", env.a.do(t, http.MethodPost, "/acme/deferred/gadgets", map[string]any{"name": huge}, nil), http.StatusRequestEntityTooLarge)
g := env.gadget(t, "g-"+env.stamp, false)
want(t, "update", env.a.do(t, http.MethodPut, dfPath(g, ""), map[string]any{"name": huge}, nil), http.StatusRequestEntityTooLarge)
want(t, "link", env.a.do(t, http.MethodPost, dfPath(g, "/relations/members/link"), map[string]any{"ids": []uint{1}, "pad": huge}, nil), http.StatusRequestEntityTooLarge)
want(t, "unlink", env.a.do(t, http.MethodPost, dfPath(g, "/relations/members/unlink"), map[string]any{"ids": []uint{1}, "pad": huge}, nil), http.StatusRequestEntityTooLarge)
settings := newConformEnv(t)
settings.send(t, http.MethodPost, "/auth/login", map[string]string{"login": settings.login, "password": adminTestPassword}, false)
rec := settings.send(t, http.MethodPut, "/settings/conform", map[string]any{"enabled": true, "pad": huge}, true)
if rec.Code != http.StatusRequestEntityTooLarge {
t.Fatalf("settings: status=%d want %d body=%s", rec.Code, http.StatusRequestEntityTooLarge, rec.Body.String())
}
}