- 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
119 lines
4.5 KiB
Go
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)
|
|
}
|
|
}
|