mirror of
https://github.com/pocketbase/pocketbase.git
synced 2024-12-01 11:01:04 +02:00
1213 lines
30 KiB
Go
1213 lines
30 KiB
Go
package forms_test
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
validation "github.com/go-ozzo/ozzo-validation/v4"
|
|
"github.com/labstack/echo/v5"
|
|
"github.com/pocketbase/pocketbase/core"
|
|
"github.com/pocketbase/pocketbase/daos"
|
|
"github.com/pocketbase/pocketbase/forms"
|
|
"github.com/pocketbase/pocketbase/models"
|
|
"github.com/pocketbase/pocketbase/models/schema"
|
|
"github.com/pocketbase/pocketbase/tests"
|
|
"github.com/pocketbase/pocketbase/tools/filesystem"
|
|
"github.com/pocketbase/pocketbase/tools/list"
|
|
"github.com/pocketbase/pocketbase/tools/types"
|
|
)
|
|
|
|
func hasRecordFile(app core.App, record *models.Record, filename string) bool {
|
|
fs, _ := app.NewFilesystem()
|
|
defer fs.Close()
|
|
|
|
fileKey := filepath.Join(
|
|
record.Collection().Id,
|
|
record.Id,
|
|
filename,
|
|
)
|
|
|
|
exists, _ := fs.Exists(fileKey)
|
|
|
|
return exists
|
|
}
|
|
|
|
func TestNewRecordUpsert(t *testing.T) {
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
collection, _ := app.Dao().FindCollectionByNameOrId("demo2")
|
|
record := models.NewRecord(collection)
|
|
record.Set("title", "test_value")
|
|
|
|
form := forms.NewRecordUpsert(app, record)
|
|
|
|
val := form.Data()["title"]
|
|
if val != "test_value" {
|
|
t.Errorf("Expected record data to be loaded, got %v", form.Data())
|
|
}
|
|
}
|
|
|
|
func TestRecordUpsertLoadRequestUnsupported(t *testing.T) {
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
record, err := app.Dao().FindRecordById("demo2", "0yxhwia2amd8gec")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
testData := "title=test123"
|
|
|
|
form := forms.NewRecordUpsert(app, record)
|
|
req := httptest.NewRequest(http.MethodGet, "/", strings.NewReader(testData))
|
|
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationForm)
|
|
|
|
if err := form.LoadRequest(req, ""); err == nil {
|
|
t.Fatal("Expected LoadRequest to fail, got nil")
|
|
}
|
|
}
|
|
|
|
func TestRecordUpsertLoadRequestJson(t *testing.T) {
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
record, err := app.Dao().FindRecordById("demo1", "84nmscqy84lsi1t")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
testData := map[string]any{
|
|
"a": map[string]any{
|
|
"b": map[string]any{
|
|
"id": "test_id",
|
|
"text": "test123",
|
|
"unknown": "test456",
|
|
// file fields unset/delete
|
|
"file_one": nil,
|
|
"file_many.0": "", // delete by index
|
|
"file_many-": []string{"test_MaWC6mWyrP.txt", "test_tC1Yc87DfC.txt"}, // multiple delete with modifier
|
|
"file_many.300_WlbFWSGmW9.png": nil, // delete by filename
|
|
"file_many.2": "test.png", // should be ignored
|
|
},
|
|
},
|
|
}
|
|
|
|
form := forms.NewRecordUpsert(app, record)
|
|
jsonBody, _ := json.Marshal(testData)
|
|
req := httptest.NewRequest(http.MethodGet, "/", bytes.NewReader(jsonBody))
|
|
req.Header.Set(echo.HeaderContentType, echo.MIMEApplicationJSON)
|
|
loadErr := form.LoadRequest(req, "a.b")
|
|
if loadErr != nil {
|
|
t.Fatal(loadErr)
|
|
}
|
|
|
|
if form.Id != "test_id" {
|
|
t.Fatalf("Expect id field to be %q, got %q", "test_id", form.Id)
|
|
}
|
|
|
|
if v, ok := form.Data()["text"]; !ok || v != "test123" {
|
|
t.Fatalf("Expect title field to be %q, got %q", "test123", v)
|
|
}
|
|
|
|
if v, ok := form.Data()["unknown"]; ok {
|
|
t.Fatalf("Didn't expect unknown field to be set, got %v", v)
|
|
}
|
|
|
|
fileOne, ok := form.Data()["file_one"]
|
|
if !ok {
|
|
t.Fatal("Expect file_one field to be set")
|
|
}
|
|
if fileOne != "" {
|
|
t.Fatalf("Expect file_one field to be empty string, got %v", fileOne)
|
|
}
|
|
|
|
fileMany, ok := form.Data()["file_many"]
|
|
if !ok || fileMany == nil {
|
|
t.Fatal("Expect file_many field to be set")
|
|
}
|
|
manyfilesRemains := len(list.ToUniqueStringSlice(fileMany))
|
|
if manyfilesRemains != 1 {
|
|
t.Fatalf("Expect only 1 file_many to remain, got \n%v", fileMany)
|
|
}
|
|
}
|
|
|
|
func TestRecordUpsertLoadRequestMultipart(t *testing.T) {
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
record, err := app.Dao().FindRecordById("demo1", "84nmscqy84lsi1t")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
formData, mp, err := tests.MockMultipartData(map[string]string{
|
|
"a.b.id": "test_id",
|
|
"a.b.text": "test123",
|
|
"a.b.unknown": "test456",
|
|
// file fields unset/delete
|
|
"a.b.file_one-": "test_d61b33QdDU.txt", // delete with modifier
|
|
"a.b.file_many.0": "", // delete by index
|
|
"a.b.file_many-": "test_tC1Yc87DfC.txt", // delete with modifier
|
|
"a.b.file_many.300_WlbFWSGmW9.png": "", // delete by filename
|
|
"a.b.file_many.2": "test.png", // should be ignored
|
|
}, "a.b.file_many")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
form := forms.NewRecordUpsert(app, record)
|
|
req := httptest.NewRequest(http.MethodGet, "/", formData)
|
|
req.Header.Set(echo.HeaderContentType, mp.FormDataContentType())
|
|
loadErr := form.LoadRequest(req, "a.b")
|
|
if loadErr != nil {
|
|
t.Fatal(loadErr)
|
|
}
|
|
|
|
if form.Id != "test_id" {
|
|
t.Fatalf("Expect id field to be %q, got %q", "test_id", form.Id)
|
|
}
|
|
|
|
if v, ok := form.Data()["text"]; !ok || v != "test123" {
|
|
t.Fatalf("Expect text field to be %q, got %q", "test123", v)
|
|
}
|
|
|
|
if v, ok := form.Data()["unknown"]; ok {
|
|
t.Fatalf("Didn't expect unknown field to be set, got %v", v)
|
|
}
|
|
|
|
fileOne, ok := form.Data()["file_one"]
|
|
if !ok {
|
|
t.Fatal("Expect file_one field to be set")
|
|
}
|
|
if fileOne != "" {
|
|
t.Fatalf("Expect file_one field to be empty string, got %v", fileOne)
|
|
}
|
|
|
|
fileMany, ok := form.Data()["file_many"]
|
|
if !ok || fileMany == nil {
|
|
t.Fatal("Expect file_many field to be set")
|
|
}
|
|
manyfilesRemains := len(list.ToUniqueStringSlice(fileMany))
|
|
expectedRemains := 3 // 5 old; 3 deleted and 1 new uploaded
|
|
if manyfilesRemains != expectedRemains {
|
|
t.Fatalf("Expect file_many to be %d, got %d (%v)", expectedRemains, manyfilesRemains, fileMany)
|
|
}
|
|
}
|
|
|
|
func TestRecordUpsertLoadData(t *testing.T) {
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
record, err := app.Dao().FindRecordById("demo2", "llvuca81nly1qls")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
form := forms.NewRecordUpsert(app, record)
|
|
|
|
loadErr := form.LoadData(map[string]any{
|
|
"title": "test_new",
|
|
"active": true,
|
|
})
|
|
if loadErr != nil {
|
|
t.Fatal(loadErr)
|
|
}
|
|
|
|
if v, ok := form.Data()["title"]; !ok || v != "test_new" {
|
|
t.Fatalf("Expect title field to be %v, got %v", "test_new", v)
|
|
}
|
|
|
|
if v, ok := form.Data()["active"]; !ok || v != true {
|
|
t.Fatalf("Expect active field to be %v, got %v", true, v)
|
|
}
|
|
}
|
|
|
|
func TestRecordUpsertDrySubmitFailure(t *testing.T) {
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
collection, _ := app.Dao().FindCollectionByNameOrId("demo1")
|
|
recordBefore, err := app.Dao().FindRecordById(collection.Id, "al1h9ijdeojtsjy")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
formData, mp, err := tests.MockMultipartData(map[string]string{
|
|
"title": "abc",
|
|
"rel_one": "missing",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
form := forms.NewRecordUpsert(app, recordBefore)
|
|
req := httptest.NewRequest(http.MethodGet, "/", formData)
|
|
req.Header.Set(echo.HeaderContentType, mp.FormDataContentType())
|
|
form.LoadRequest(req, "")
|
|
|
|
callbackCalls := 0
|
|
|
|
// ensure that validate is triggered
|
|
// ---
|
|
result := form.DrySubmit(func(txDao *daos.Dao) error {
|
|
callbackCalls++
|
|
return nil
|
|
})
|
|
if result == nil {
|
|
t.Fatal("Expected error, got nil")
|
|
}
|
|
if callbackCalls != 0 {
|
|
t.Fatalf("Expected callbackCalls to be 0, got %d", callbackCalls)
|
|
}
|
|
|
|
// ensure that the record changes weren't persisted
|
|
// ---
|
|
recordAfter, err := app.Dao().FindRecordById(collection.Id, recordBefore.Id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if recordAfter.GetString("title") == "abc" {
|
|
t.Fatalf("Expected record.title to be %v, got %v", recordAfter.GetString("title"), "abc")
|
|
}
|
|
|
|
if recordAfter.GetString("rel_one") == "missing" {
|
|
t.Fatalf("Expected record.rel_one to be %s, got %s", recordBefore.GetString("rel_one"), "missing")
|
|
}
|
|
}
|
|
|
|
func TestRecordUpsertDrySubmitSuccess(t *testing.T) {
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
collection, _ := app.Dao().FindCollectionByNameOrId("demo1")
|
|
recordBefore, err := app.Dao().FindRecordById(collection.Id, "84nmscqy84lsi1t")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
formData, mp, err := tests.MockMultipartData(map[string]string{
|
|
"title": "dry_test",
|
|
"file_one": "",
|
|
}, "file_many")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
form := forms.NewRecordUpsert(app, recordBefore)
|
|
req := httptest.NewRequest(http.MethodGet, "/", formData)
|
|
req.Header.Set(echo.HeaderContentType, mp.FormDataContentType())
|
|
form.LoadRequest(req, "")
|
|
|
|
callbackCalls := 0
|
|
|
|
result := form.DrySubmit(func(txDao *daos.Dao) error {
|
|
callbackCalls++
|
|
return nil
|
|
})
|
|
if result != nil {
|
|
t.Fatalf("Expected nil, got error %v", result)
|
|
}
|
|
|
|
// ensure callback was called
|
|
if callbackCalls != 1 {
|
|
t.Fatalf("Expected callbackCalls to be 1, got %d", callbackCalls)
|
|
}
|
|
|
|
// ensure that the record changes weren't persisted
|
|
recordAfter, err := app.Dao().FindRecordById(collection.Id, recordBefore.Id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if recordAfter.GetString("title") == "dry_test" {
|
|
t.Fatalf("Expected record.title to be %v, got %v", recordAfter.GetString("title"), "dry_test")
|
|
}
|
|
if recordAfter.GetString("file_one") == "" {
|
|
t.Fatal("Expected record.file_one to not be changed, got empty string")
|
|
}
|
|
|
|
// file wasn't removed
|
|
if !hasRecordFile(app, recordAfter, recordAfter.GetString("file_one")) {
|
|
t.Fatal("file_one file should not have been deleted")
|
|
}
|
|
}
|
|
|
|
func TestRecordUpsertDrySubmitWithNestedTx(t *testing.T) {
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
collection, _ := app.Dao().FindCollectionByNameOrId("demo1")
|
|
recordBefore, err := app.Dao().FindRecordById(collection.Id, "84nmscqy84lsi1t")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
formData, mp, err := tests.MockMultipartData(map[string]string{
|
|
"title": "dry_test",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
txErr := app.Dao().RunInTransaction(func(txDao *daos.Dao) error {
|
|
form := forms.NewRecordUpsert(app, recordBefore)
|
|
form.SetDao(txDao)
|
|
req := httptest.NewRequest(http.MethodGet, "/", formData)
|
|
req.Header.Set(echo.HeaderContentType, mp.FormDataContentType())
|
|
form.LoadRequest(req, "")
|
|
|
|
callbackCalls := 0
|
|
|
|
result := form.DrySubmit(func(innerTxDao *daos.Dao) error {
|
|
callbackCalls++
|
|
return nil
|
|
})
|
|
if result != nil {
|
|
t.Fatalf("Expected nil, got error %v", result)
|
|
}
|
|
|
|
// ensure callback was called
|
|
if callbackCalls != 1 {
|
|
t.Fatalf("Expected callbackCalls to be 1, got %d", callbackCalls)
|
|
}
|
|
|
|
// ensure that the original txDao can still be used after the DrySubmit rollback
|
|
if _, err := txDao.FindRecordById(collection.Id, recordBefore.Id); err != nil {
|
|
t.Fatalf("Expected the dry submit rollback to not affect the outer tx context, got %v", err)
|
|
}
|
|
|
|
// ensure that the record changes weren't persisted
|
|
recordAfter, err := app.Dao().FindRecordById(collection.Id, recordBefore.Id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if recordAfter.GetString("title") == "dry_test" {
|
|
t.Fatalf("Expected record.title to be %v, got %v", recordBefore.GetString("title"), "dry_test")
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if txErr != nil {
|
|
t.Fatalf("Nested transactions failure: %v", txErr)
|
|
}
|
|
}
|
|
|
|
func TestRecordUpsertSubmitFailure(t *testing.T) {
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
collection, err := app.Dao().FindCollectionByNameOrId("demo1")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
recordBefore, err := app.Dao().FindRecordById(collection.Id, "84nmscqy84lsi1t")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
formData, mp, err := tests.MockMultipartData(map[string]string{
|
|
"text": "abc",
|
|
"bool": "false",
|
|
"select_one": "invalid",
|
|
"file_many": "invalid",
|
|
"email": "invalid",
|
|
}, "file_one")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
form := forms.NewRecordUpsert(app, recordBefore)
|
|
req := httptest.NewRequest(http.MethodGet, "/", formData)
|
|
req.Header.Set(echo.HeaderContentType, mp.FormDataContentType())
|
|
form.LoadRequest(req, "")
|
|
|
|
interceptorCalls := 0
|
|
interceptor := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] {
|
|
return func(r *models.Record) error {
|
|
interceptorCalls++
|
|
return next(r)
|
|
}
|
|
}
|
|
|
|
// ensure that validate is triggered
|
|
// ---
|
|
result := form.Submit(interceptor)
|
|
if result == nil {
|
|
t.Fatal("Expected error, got nil")
|
|
}
|
|
|
|
// check interceptor calls
|
|
// ---
|
|
if interceptorCalls != 0 {
|
|
t.Fatalf("Expected interceptor to be called 0 times, got %d", interceptorCalls)
|
|
}
|
|
|
|
// ensure that the record changes weren't persisted
|
|
// ---
|
|
recordAfter, err := app.Dao().FindRecordById(collection.Id, recordBefore.Id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if v := recordAfter.Get("text"); v == "abc" {
|
|
t.Fatalf("Expected record.text not to change, got %v", v)
|
|
}
|
|
if v := recordAfter.Get("bool"); v == false {
|
|
t.Fatalf("Expected record.bool not to change, got %v", v)
|
|
}
|
|
if v := recordAfter.Get("select_one"); v == "invalid" {
|
|
t.Fatalf("Expected record.select_one not to change, got %v", v)
|
|
}
|
|
if v := recordAfter.Get("email"); v == "invalid" {
|
|
t.Fatalf("Expected record.email not to change, got %v", v)
|
|
}
|
|
if v := recordAfter.GetStringSlice("file_many"); len(v) != 5 {
|
|
t.Fatalf("Expected record.file_many not to change, got %v", v)
|
|
}
|
|
|
|
// ensure the files weren't removed
|
|
for _, f := range recordAfter.GetStringSlice("file_many") {
|
|
if !hasRecordFile(app, recordAfter, f) {
|
|
t.Fatal("file_many file should not have been deleted")
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRecordUpsertSubmitSuccess(t *testing.T) {
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
collection, _ := app.Dao().FindCollectionByNameOrId("demo1")
|
|
recordBefore, err := app.Dao().FindRecordById(collection.Id, "84nmscqy84lsi1t")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
formData, mp, err := tests.MockMultipartData(map[string]string{
|
|
"text": "test_save",
|
|
"bool": "true",
|
|
"select_one": "optionA",
|
|
"file_one": "",
|
|
}, "file_many.1", "file_many") // replace + new file
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
form := forms.NewRecordUpsert(app, recordBefore)
|
|
req := httptest.NewRequest(http.MethodGet, "/", formData)
|
|
req.Header.Set(echo.HeaderContentType, mp.FormDataContentType())
|
|
form.LoadRequest(req, "")
|
|
|
|
interceptorCalls := 0
|
|
interceptor := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] {
|
|
return func(r *models.Record) error {
|
|
interceptorCalls++
|
|
return next(r)
|
|
}
|
|
}
|
|
|
|
result := form.Submit(interceptor)
|
|
if result != nil {
|
|
t.Fatalf("Expected nil, got error %v", result)
|
|
}
|
|
|
|
// check interceptor calls
|
|
// ---
|
|
if interceptorCalls != 1 {
|
|
t.Fatalf("Expected interceptor to be called 1 time, got %d", interceptorCalls)
|
|
}
|
|
|
|
// ensure that the record changes were persisted
|
|
// ---
|
|
recordAfter, err := app.Dao().FindRecordById(collection.Id, recordBefore.Id)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if v := recordAfter.GetString("text"); v != "test_save" {
|
|
t.Fatalf("Expected record.text to be %v, got %v", v, "test_save")
|
|
}
|
|
|
|
if hasRecordFile(app, recordAfter, recordAfter.GetString("file_one")) {
|
|
t.Fatal("Expected record.file_one to be deleted")
|
|
}
|
|
|
|
fileMany := (recordAfter.GetStringSlice("file_many"))
|
|
if len(fileMany) != 6 { // 1 replace + 1 new
|
|
t.Fatalf("Expected 6 record.file_many, got %d (%v)", len(fileMany), fileMany)
|
|
}
|
|
for _, f := range fileMany {
|
|
if !hasRecordFile(app, recordAfter, f) {
|
|
t.Fatalf("Expected file %q to exist", f)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRecordUpsertSubmitInterceptors(t *testing.T) {
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
collection, _ := app.Dao().FindCollectionByNameOrId("demo3")
|
|
record, err := app.Dao().FindRecordById(collection.Id, "mk5fmymtx4wsprk")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
form := forms.NewRecordUpsert(app, record)
|
|
form.Data()["title"] = "test_new"
|
|
|
|
testErr := errors.New("test_error")
|
|
interceptorRecordTitle := ""
|
|
|
|
interceptor1Called := false
|
|
interceptor1 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] {
|
|
return func(r *models.Record) error {
|
|
interceptor1Called = true
|
|
return next(r)
|
|
}
|
|
}
|
|
|
|
interceptor2Called := false
|
|
interceptor2 := func(next forms.InterceptorNextFunc[*models.Record]) forms.InterceptorNextFunc[*models.Record] {
|
|
return func(r *models.Record) error {
|
|
interceptorRecordTitle = record.GetString("title") // to check if the record was filled
|
|
interceptor2Called = true
|
|
return testErr
|
|
}
|
|
}
|
|
|
|
submitErr := form.Submit(interceptor1, interceptor2)
|
|
if submitErr != testErr {
|
|
t.Fatalf("Expected submitError %v, got %v", testErr, submitErr)
|
|
}
|
|
|
|
if !interceptor1Called {
|
|
t.Fatalf("Expected interceptor1 to be called")
|
|
}
|
|
|
|
if !interceptor2Called {
|
|
t.Fatalf("Expected interceptor2 to be called")
|
|
}
|
|
|
|
if interceptorRecordTitle != form.Data()["title"].(string) {
|
|
t.Fatalf("Expected the form model to be filled before calling the interceptors")
|
|
}
|
|
}
|
|
|
|
func TestRecordUpsertWithCustomId(t *testing.T) {
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
collection, err := app.Dao().FindCollectionByNameOrId("demo3")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
existingRecord, err := app.Dao().FindRecordById(collection.Id, "mk5fmymtx4wsprk")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
scenarios := []struct {
|
|
name string
|
|
data map[string]string
|
|
record *models.Record
|
|
expectError bool
|
|
}{
|
|
{
|
|
"empty data",
|
|
map[string]string{},
|
|
models.NewRecord(collection),
|
|
false,
|
|
},
|
|
{
|
|
"empty id",
|
|
map[string]string{"id": ""},
|
|
models.NewRecord(collection),
|
|
false,
|
|
},
|
|
{
|
|
"id < 15 chars",
|
|
map[string]string{"id": "a23"},
|
|
models.NewRecord(collection),
|
|
true,
|
|
},
|
|
{
|
|
"id > 15 chars",
|
|
map[string]string{"id": "a234567890123456"},
|
|
models.NewRecord(collection),
|
|
true,
|
|
},
|
|
{
|
|
"id = 15 chars (invalid chars)",
|
|
map[string]string{"id": "a@3456789012345"},
|
|
models.NewRecord(collection),
|
|
true,
|
|
},
|
|
{
|
|
"id = 15 chars (valid chars)",
|
|
map[string]string{"id": "a23456789012345"},
|
|
models.NewRecord(collection),
|
|
false,
|
|
},
|
|
{
|
|
"changing the id of an existing record",
|
|
map[string]string{"id": "b23456789012345"},
|
|
existingRecord,
|
|
true,
|
|
},
|
|
{
|
|
"using the same existing record id",
|
|
map[string]string{"id": existingRecord.Id},
|
|
existingRecord,
|
|
false,
|
|
},
|
|
{
|
|
"skipping the id for existing record",
|
|
map[string]string{},
|
|
existingRecord,
|
|
false,
|
|
},
|
|
}
|
|
|
|
for _, scenario := range scenarios {
|
|
formData, mp, err := tests.MockMultipartData(scenario.data)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
form := forms.NewRecordUpsert(app, scenario.record)
|
|
req := httptest.NewRequest(http.MethodGet, "/", formData)
|
|
req.Header.Set(echo.HeaderContentType, mp.FormDataContentType())
|
|
form.LoadRequest(req, "")
|
|
|
|
dryErr := form.DrySubmit(nil)
|
|
hasDryErr := dryErr != nil
|
|
|
|
submitErr := form.Submit()
|
|
hasSubmitErr := submitErr != nil
|
|
|
|
if hasDryErr != hasSubmitErr {
|
|
t.Errorf("[%s] Expected hasDryErr and hasSubmitErr to have the same value, got %v vs %v", scenario.name, hasDryErr, hasSubmitErr)
|
|
}
|
|
|
|
if hasSubmitErr != scenario.expectError {
|
|
t.Errorf("[%s] Expected hasSubmitErr to be %v, got %v (%v)", scenario.name, scenario.expectError, hasSubmitErr, submitErr)
|
|
}
|
|
|
|
if id, ok := scenario.data["id"]; ok && id != "" && !hasSubmitErr {
|
|
_, err := app.Dao().FindRecordById(collection.Id, id)
|
|
if err != nil {
|
|
t.Errorf("[%s] Expected to find record with id %s, got %v", scenario.name, id, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRecordUpsertAuthRecord(t *testing.T) {
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
scenarios := []struct {
|
|
name string
|
|
existingId string
|
|
data map[string]any
|
|
manageAccess bool
|
|
expectError bool
|
|
}{
|
|
{
|
|
"empty create data",
|
|
"",
|
|
map[string]any{},
|
|
false,
|
|
true,
|
|
},
|
|
{
|
|
"empty update data",
|
|
"4q1xlclmfloku33",
|
|
map[string]any{},
|
|
false,
|
|
false,
|
|
},
|
|
{
|
|
"minimum valid create data",
|
|
"",
|
|
map[string]any{
|
|
"password": "12345678",
|
|
"passwordConfirm": "12345678",
|
|
},
|
|
false,
|
|
false,
|
|
},
|
|
{
|
|
"create with all allowed auth fields",
|
|
"",
|
|
map[string]any{
|
|
"username": "test_new",
|
|
"email": "test_new@example.com",
|
|
"emailVisibility": true,
|
|
"password": "12345678",
|
|
"passwordConfirm": "12345678",
|
|
},
|
|
false,
|
|
false,
|
|
},
|
|
|
|
// username
|
|
{
|
|
"invalid username characters",
|
|
"",
|
|
map[string]any{
|
|
"username": "test abc!@#",
|
|
"password": "12345678",
|
|
"passwordConfirm": "12345678",
|
|
},
|
|
false,
|
|
true,
|
|
},
|
|
{
|
|
"invalid username length (less than 3)",
|
|
"",
|
|
map[string]any{
|
|
"username": "ab",
|
|
"password": "12345678",
|
|
"passwordConfirm": "12345678",
|
|
},
|
|
false,
|
|
true,
|
|
},
|
|
{
|
|
"invalid username length (more than 150)",
|
|
"",
|
|
map[string]any{
|
|
"username": strings.Repeat("a", 151),
|
|
"password": "12345678",
|
|
"passwordConfirm": "12345678",
|
|
},
|
|
false,
|
|
true,
|
|
},
|
|
|
|
// verified
|
|
{
|
|
"try to set verified without managed access",
|
|
"",
|
|
map[string]any{
|
|
"verified": true,
|
|
"password": "12345678",
|
|
"passwordConfirm": "12345678",
|
|
},
|
|
false,
|
|
true,
|
|
},
|
|
{
|
|
"try to update verified without managed access",
|
|
"4q1xlclmfloku33",
|
|
map[string]any{
|
|
"verified": true,
|
|
},
|
|
false,
|
|
true,
|
|
},
|
|
{
|
|
"set verified with managed access",
|
|
"",
|
|
map[string]any{
|
|
"verified": true,
|
|
"password": "12345678",
|
|
"passwordConfirm": "12345678",
|
|
},
|
|
true,
|
|
false,
|
|
},
|
|
{
|
|
"update verified with managed access",
|
|
"4q1xlclmfloku33",
|
|
map[string]any{
|
|
"verified": true,
|
|
},
|
|
true,
|
|
false,
|
|
},
|
|
|
|
// email
|
|
{
|
|
"try to update email without managed access",
|
|
"4q1xlclmfloku33",
|
|
map[string]any{
|
|
"email": "test_update@example.com",
|
|
},
|
|
false,
|
|
true,
|
|
},
|
|
{
|
|
"update email with managed access",
|
|
"4q1xlclmfloku33",
|
|
map[string]any{
|
|
"email": "test_update@example.com",
|
|
},
|
|
true,
|
|
false,
|
|
},
|
|
|
|
// password
|
|
{
|
|
"trigger the password validations if only oldPassword is set",
|
|
"4q1xlclmfloku33",
|
|
map[string]any{
|
|
"oldPassword": "1234567890",
|
|
},
|
|
false,
|
|
true,
|
|
},
|
|
{
|
|
"trigger the password validations if only passwordConfirm is set",
|
|
"4q1xlclmfloku33",
|
|
map[string]any{
|
|
"passwordConfirm": "1234567890",
|
|
},
|
|
false,
|
|
true,
|
|
},
|
|
{
|
|
"try to update password without managed access",
|
|
"4q1xlclmfloku33",
|
|
map[string]any{
|
|
"password": "1234567890",
|
|
"passwordConfirm": "1234567890",
|
|
},
|
|
false,
|
|
true,
|
|
},
|
|
{
|
|
"update password without managed access but with oldPassword",
|
|
"4q1xlclmfloku33",
|
|
map[string]any{
|
|
"oldPassword": "1234567890",
|
|
"password": "1234567890",
|
|
"passwordConfirm": "1234567890",
|
|
},
|
|
false,
|
|
false,
|
|
},
|
|
{
|
|
"update email with managed access (without oldPassword)",
|
|
"4q1xlclmfloku33",
|
|
map[string]any{
|
|
"password": "1234567890",
|
|
"passwordConfirm": "1234567890",
|
|
},
|
|
true,
|
|
false,
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
collection, err := app.Dao().FindCollectionByNameOrId("users")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
record := models.NewRecord(collection)
|
|
if s.existingId != "" {
|
|
var err error
|
|
record, err = app.Dao().FindRecordById(collection.Id, s.existingId)
|
|
if err != nil {
|
|
t.Errorf("[%s] Failed to fetch auth record with id %s", s.name, s.existingId)
|
|
continue
|
|
}
|
|
}
|
|
|
|
form := forms.NewRecordUpsert(app, record)
|
|
form.SetFullManageAccess(s.manageAccess)
|
|
if err := form.LoadData(s.data); err != nil {
|
|
t.Errorf("[%s] Failed to load form data", s.name)
|
|
continue
|
|
}
|
|
|
|
submitErr := form.Submit()
|
|
|
|
hasErr := submitErr != nil
|
|
if hasErr != s.expectError {
|
|
t.Errorf("[%s] Expected hasErr %v, got %v (%v)", s.name, s.expectError, hasErr, submitErr)
|
|
}
|
|
|
|
if !hasErr && record.Username() == "" {
|
|
t.Errorf("[%s] Expected username to be set, got empty string: \n%v", s.name, record)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRecordUpsertUniqueValidator(t *testing.T) {
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
// create a dummy collection
|
|
collection := &models.Collection{
|
|
Name: "test",
|
|
Schema: schema.NewSchema(
|
|
&schema.SchemaField{
|
|
Type: "text",
|
|
Name: "fieldA",
|
|
},
|
|
&schema.SchemaField{
|
|
Type: "text",
|
|
Name: "fieldB",
|
|
},
|
|
&schema.SchemaField{
|
|
Type: "text",
|
|
Name: "fieldC",
|
|
},
|
|
),
|
|
Indexes: types.JsonArray[string]{
|
|
// the field case shouldn't matter
|
|
"create unique index unique_single_idx on test (fielda)",
|
|
"create unique index unique_combined_idx on test (fieldb, FIELDC)",
|
|
},
|
|
}
|
|
if err := app.Dao().SaveCollection(collection); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
dummyRecord := models.NewRecord(collection)
|
|
dummyRecord.Set("fieldA", "a")
|
|
dummyRecord.Set("fieldB", "b")
|
|
dummyRecord.Set("fieldC", "c")
|
|
if err := app.Dao().SaveRecord(dummyRecord); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
scenarios := []struct {
|
|
name string
|
|
data map[string]any
|
|
expectedErrors []string
|
|
}{
|
|
{
|
|
"duplicated unique value",
|
|
map[string]any{
|
|
"fieldA": "a",
|
|
},
|
|
[]string{"fieldA"},
|
|
},
|
|
{
|
|
"duplicated combined unique value",
|
|
map[string]any{
|
|
"fieldB": "b",
|
|
"fieldC": "c",
|
|
},
|
|
[]string{"fieldB", "fieldC"},
|
|
},
|
|
{
|
|
"non-duplicated unique value",
|
|
map[string]any{
|
|
"fieldA": "a2",
|
|
},
|
|
nil,
|
|
},
|
|
{
|
|
"non-duplicated combined unique value",
|
|
map[string]any{
|
|
"fieldB": "b",
|
|
"fieldC": "d",
|
|
},
|
|
nil,
|
|
},
|
|
}
|
|
|
|
for _, s := range scenarios {
|
|
record := models.NewRecord(collection)
|
|
|
|
form := forms.NewRecordUpsert(app, record)
|
|
if err := form.LoadData(s.data); err != nil {
|
|
t.Errorf("[%s] Failed to load form data", s.name)
|
|
continue
|
|
}
|
|
|
|
result := form.Submit()
|
|
|
|
// parse errors
|
|
errs, ok := result.(validation.Errors)
|
|
if !ok && result != nil {
|
|
t.Errorf("[%s] Failed to parse errors %v", s.name, result)
|
|
continue
|
|
}
|
|
|
|
// check errors
|
|
if len(errs) > len(s.expectedErrors) {
|
|
t.Errorf("[%s] Expected error keys %v, got %v", s.name, s.expectedErrors, errs)
|
|
continue
|
|
}
|
|
for _, k := range s.expectedErrors {
|
|
if _, ok := errs[k]; !ok {
|
|
t.Errorf("[%s] Missing expected error key %q in %v", s.name, k, errs)
|
|
continue
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRecordUpsertAddAndRemoveFiles(t *testing.T) {
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
recordBefore, err := app.Dao().FindRecordById("demo1", "84nmscqy84lsi1t")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// create test temp files
|
|
tempDir := filepath.Join(app.DataDir(), "temp")
|
|
if err := os.MkdirAll(app.DataDir(), os.ModePerm); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
tmpFile, _ := os.CreateTemp(os.TempDir(), "tmpfile1-*.txt")
|
|
tmpFile.Close()
|
|
|
|
form := forms.NewRecordUpsert(app, recordBefore)
|
|
|
|
f1, err := filesystem.NewFileFromPath(tmpFile.Name())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
f2, err := filesystem.NewFileFromPath(tmpFile.Name())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
f3, err := filesystem.NewFileFromPath(tmpFile.Name())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
removed0 := "test_d61b33QdDU.txt" // replaced
|
|
removed1 := "300_WlbFWSGmW9.png"
|
|
removed2 := "logo_vcfJJG5TAh.svg"
|
|
|
|
form.AddFiles("file_one", f1) // should replace the existin file
|
|
|
|
form.AddFiles("file_many", f2, f3) // should append
|
|
|
|
form.RemoveFiles("file_many", removed1, removed2) // should remove
|
|
|
|
filesToUpload := form.FilesToUpload()
|
|
if v, ok := filesToUpload["file_one"]; !ok || len(v) != 1 {
|
|
t.Fatalf("Expected filesToUpload[file_one] to have exactly 1 file, got %v", v)
|
|
}
|
|
if v, ok := filesToUpload["file_many"]; !ok || len(v) != 2 {
|
|
t.Fatalf("Expected filesToUpload[file_many] to have exactly 2 file, got %v", v)
|
|
}
|
|
|
|
filesToDelete := form.FilesToDelete()
|
|
if len(filesToDelete) != 3 {
|
|
t.Fatalf("Expected exactly 2 file to delete, got %v", filesToDelete)
|
|
}
|
|
for _, f := range []string{removed0, removed1, removed2} {
|
|
if !list.ExistInSlice(f, filesToDelete) {
|
|
t.Fatalf("Missing file %q from filesToDelete %v", f, filesToDelete)
|
|
}
|
|
}
|
|
|
|
if err := form.Submit(); err != nil {
|
|
t.Fatalf("Failed to submit the RecordUpsert form, got %v", err)
|
|
}
|
|
|
|
recordAfter, err := app.Dao().FindRecordById("demo1", "84nmscqy84lsi1t")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// ensure files deletion
|
|
if hasRecordFile(app, recordAfter, removed0) {
|
|
t.Fatalf("Expected the old file_one file to be deleted")
|
|
}
|
|
if hasRecordFile(app, recordAfter, removed1) {
|
|
t.Fatalf("Expected %s to be deleted", removed1)
|
|
}
|
|
if hasRecordFile(app, recordAfter, removed2) {
|
|
t.Fatalf("Expected %s to be deleted", removed2)
|
|
}
|
|
|
|
fileOne := recordAfter.GetStringSlice("file_one")
|
|
if len(fileOne) == 0 {
|
|
t.Fatalf("Expected new file_one file to be uploaded")
|
|
}
|
|
|
|
fileMany := recordAfter.GetStringSlice("file_many")
|
|
if len(fileMany) != 5 {
|
|
t.Fatalf("Expected file_many to be 5, got %v", fileMany)
|
|
}
|
|
}
|
|
|
|
func TestRecordUpsertUploadFailure(t *testing.T) {
|
|
app, _ := tests.NewTestApp()
|
|
defer app.Cleanup()
|
|
|
|
collection, err := app.Dao().FindCollectionByNameOrId("demo3")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
testDaos := []*daos.Dao{
|
|
app.Dao(), // with hooks
|
|
daos.New(app.Dao().DB()), // without hooks
|
|
}
|
|
|
|
for i, dao := range testDaos {
|
|
// create with invalid file
|
|
{
|
|
prefix := fmt.Sprintf("%d-create", i)
|
|
|
|
new := models.NewRecord(collection)
|
|
new.Id = "123456789012341"
|
|
|
|
form := forms.NewRecordUpsert(app, new)
|
|
form.SetDao(dao)
|
|
form.LoadData(map[string]any{"title": "new_test"})
|
|
form.AddFiles("files", &filesystem.File{Reader: &filesystem.PathReader{Path: "/tmp/__missing__"}})
|
|
|
|
if err := form.Submit(); err == nil {
|
|
t.Fatalf("[%s] Expected error, got nil", prefix)
|
|
}
|
|
|
|
if r, err := app.Dao().FindRecordById(collection.Id, new.Id); err == nil {
|
|
t.Fatalf("[%s] Expected the inserted record to be deleted, found \n%v", prefix, r.PublicExport())
|
|
}
|
|
}
|
|
|
|
// update with invalid file
|
|
{
|
|
prefix := fmt.Sprintf("%d-update", i)
|
|
|
|
record, err := app.Dao().FindRecordById(collection.Id, "1tmknxy2868d869")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
form := forms.NewRecordUpsert(app, record)
|
|
form.SetDao(dao)
|
|
form.LoadData(map[string]any{"title": "update_test"})
|
|
form.AddFiles("files", &filesystem.File{Reader: &filesystem.PathReader{Path: "/tmp/__missing__"}})
|
|
|
|
if err := form.Submit(); err == nil {
|
|
t.Fatalf("[%s] Expected error, got nil", prefix)
|
|
}
|
|
|
|
if r, _ := app.Dao().FindRecordById(collection.Id, record.Id); r == nil || r.GetString("title") == "update_test" {
|
|
t.Fatalf("[%s] Expected the record changes to be reverted, got \n%v", prefix, r.PublicExport())
|
|
}
|
|
}
|
|
}
|
|
}
|