Files
summercms/modules/cabana/fileupload_smoke_test.go
Jakub Zych 044e0450ef feat(12.2-02): add the fileupload field with deferred uploads committed on save
- type: fileupload compiles the D-08 keys and binds to the model's attach.Relation at boot
- X-Session-Key (cabana.SessionKeyHeader) carries the form session key; RecordInput.SessionKey
- GET and POST .../{id}/files/{field}: list with pending uploads, multipart upload into attach.Store
- the create and update save attaches the session's pending files in its transaction
- swagger2openapi folds formData parameters into a multipart requestBody
- admin OpenAPI, TS types, conformance cases, README and forms docs
2026-10-02 18:04:34 +02:00

119 lines
4.5 KiB
Go

package cabana_test
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
"testing"
"git.golem15.com/golem15/summercms/modules/cabana"
"git.golem15.com/golem15/summercms/modules/lagoon"
)
func fileList(t *testing.T, rec *httptest.ResponseRecorder) []cabana.FileItem {
t.Helper()
if rec.Code != http.StatusOK {
t.Fatalf("file list status=%d body=%s", rec.Code, rec.Body.String())
}
var body cabana.Envelope[[]cabana.FileItem]
if err := json.Unmarshal(rec.Body.Bytes(), &body); err != nil {
t.Fatalf("file list: %v\n%s", err, rec.Body.String())
}
return body.Data
}
func (e *conformEnv) listFiles(t *testing.T, id uint, field, key string) []cabana.FileItem {
t.Helper()
headers := map[string]string{}
if key != "" {
headers[cabana.SessionKeyHeader] = key
}
return fileList(t, e.sendWith(t, http.MethodGet, fmt.Sprintf("/acme/conform/gadgets/%d/files/%s", id, field), nil, "", headers))
}
func (e *conformEnv) loginAs(t *testing.T, login string) {
t.Helper()
e.login = login
if rec := e.send(t, http.MethodPost, "/auth/login", map[string]string{"login": login, "password": adminTestPassword}, false); rec.Code != http.StatusOK {
t.Fatalf("login %s status=%d body=%s", login, rec.Code, rec.Body.String())
}
}
func (e *conformEnv) bindingCount(t *testing.T, key string) int64 {
t.Helper()
var n int64
if err := e.db.Model(&lagoon.DeferredBinding{}).Where("session_key = ?", key).Count(&n).Error; err != nil {
t.Fatal(err)
}
return n
}
// TestFileuploadSmokeCreateCommit uploads an image to the create form (id 0)
// and saves the record with the same session key: the file is attached to
// the new record and the binding is gone.
func TestFileuploadSmokeCreateCommit(t *testing.T) {
env := newConformEnv(t)
env.loginAs(t, env.login)
key := newSessionKey(t)
up := env.upload(t, 0, "photos", "photo.png", conformPNG(t), key)
if up.Code != http.StatusCreated {
t.Fatalf("upload status=%d body=%s", up.Code, up.Body.String())
}
if got := env.listFiles(t, 0, "photos", key); len(got) != 1 || !got[0].Pending {
t.Fatalf("pending list = %#v", got)
}
payload, _ := json.Marshal(map[string]any{"name": "smoke-" + env.stamp})
created := env.sendWith(t, http.MethodPost, "/acme/conform/gadgets", payload, "application/json", map[string]string{cabana.SessionKeyHeader: key})
if created.Code != http.StatusCreated {
t.Fatalf("create status=%d body=%s", created.Code, created.Body.String())
}
id := dataID(t, created.Body.Bytes())
got := env.listFiles(t, id, "photos", "")
if len(got) != 1 || got[0].Pending || got[0].FileName != "photo.png" || got[0].URL == "" {
t.Fatalf("attached list = %#v", got)
}
if n := env.bindingCount(t, key); n != 0 {
t.Fatalf("deferred_bindings rows for the key after save = %d", n)
}
// Without the key, the unsaved form shows nothing: id 0 is 404.
if rec := env.sendWith(t, http.MethodGet, "/acme/conform/gadgets/0/files/photos", nil, "", nil); rec.Code != http.StatusNotFound {
t.Fatalf("id 0 without key status=%d", rec.Code)
}
// A malformed key is a 422 on session_key.
if rec := env.sendWith(t, http.MethodGet, "/acme/conform/gadgets/0/files/photos", nil, "", map[string]string{cabana.SessionKeyHeader: "short"}); rec.Code != http.StatusUnprocessableEntity {
t.Fatalf("malformed key status=%d", rec.Code)
}
}
// TestFileuploadSmokeForeignAdmin replays one admin's session key as another
// admin: the pending upload is neither listed nor committed.
func TestFileuploadSmokeForeignAdmin(t *testing.T) {
env := newConformEnv(t)
env.loginAs(t, env.login)
key := newSessionKey(t)
if up := env.upload(t, 0, "photos", "photo.png", conformPNG(t), key); up.Code != http.StatusCreated {
t.Fatalf("upload status=%d body=%s", up.Code, up.Body.String())
}
other := *env
login := "other-" + env.stamp
insertAdmin(t, env.db, login, login+"@example.test", adminTestPassword, true, false)
other.loginAs(t, login)
if got := other.listFiles(t, 0, "photos", key); len(got) != 0 {
t.Fatalf("foreign admin sees %#v", got)
}
payload, _ := json.Marshal(map[string]any{"name": "foreign-" + env.stamp})
created := other.sendWith(t, http.MethodPost, "/acme/conform/gadgets", payload, "application/json", map[string]string{cabana.SessionKeyHeader: key})
if created.Code != http.StatusCreated {
t.Fatalf("create status=%d body=%s", created.Code, created.Body.String())
}
if got := other.listFiles(t, dataID(t, created.Body.Bytes()), "photos", ""); len(got) != 0 {
t.Fatalf("foreign save attached %#v", got)
}
if n := env.bindingCount(t, key); n != 1 {
t.Fatalf("owner's binding rows = %d, want 1", n)
}
}