Form schema meta lists enabled locales, Show/save expand mltext maps without D-11 fallback, and the SPA seeds and adopt-merges every locale. Co-authored-by: Cursor <cursoragent@cursor.com>
248 lines
7.0 KiB
Go
248 lines
7.0 KiB
Go
package cabana
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
func TestMLFieldTypes(t *testing.T) {
|
|
accepted := `fields:
|
|
title:
|
|
type: mltext
|
|
label: Title
|
|
excerpt:
|
|
type: markdown
|
|
label: Excerpt
|
|
body:
|
|
type: mlmarkdown
|
|
label: Body
|
|
`
|
|
schema, err := CompileForm("acme.demo", schemaController{model: "Widget"}, formFS(formConfig, accepted))
|
|
if err != nil {
|
|
t.Fatalf("compile accepted types: %v", err)
|
|
}
|
|
got := map[string]string{}
|
|
for _, field := range schema.Fields {
|
|
got[field.Name] = field.Type
|
|
}
|
|
if got["title"] != "mltext" || got["excerpt"] != "markdown" || got["body"] != "mlmarkdown" {
|
|
t.Fatalf("types = %v", got)
|
|
}
|
|
_, err = CompileForm("acme.demo", schemaController{model: "Widget"}, formFS(formConfig, "fields:\n title:\n type: mlunknown\n label: Title\n"))
|
|
if err == nil || !strings.Contains(err.Error(), "unsupported type mlunknown") {
|
|
t.Fatalf("unknown type err = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestMLNestedSave(t *testing.T) {
|
|
_, db := newListService(t)
|
|
if err := db.Migrator().DropTable(&mlPost{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(&mlPost{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writer := &recordingWriter{defaultLocale: "en", enabled: []string{"en", "pl"}}
|
|
svc := CRUDService{DB: db, writer: writer}
|
|
cc := mlCompiled(t)
|
|
|
|
projected := ProjectWritableFields(cc, map[string]any{
|
|
"title": map[string]any{"en": "Hello", "pl": "Witaj"},
|
|
"extra": map[string]any{"nested": true},
|
|
})
|
|
if len(projected) != 0 {
|
|
t.Fatalf("unlifted nested maps reached projection: %#v", projected)
|
|
}
|
|
|
|
rec, err := svc.Create(context.Background(), cc, RecordInput{Body: map[string]any{
|
|
"title": map[string]any{"en": "Hello", "pl": "Witaj"},
|
|
"extra": map[string]any{"nested": true},
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
if rec["title"] != nil {
|
|
want := map[string]string{"en": "Hello", "pl": "Witaj"}
|
|
got, ok := rec["title"].(map[string]string)
|
|
if !ok || !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("projected title = %#v", rec["title"])
|
|
}
|
|
} else {
|
|
t.Fatalf("projected title = %#v", rec["title"])
|
|
}
|
|
var row mlPost
|
|
if err := db.First(&row).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if row.Title != "Hello" {
|
|
t.Fatalf("host title = %q", row.Title)
|
|
}
|
|
if writer.attrs["en"] != nil {
|
|
t.Fatalf("default locale duplicated: %#v", writer.attrs["en"])
|
|
}
|
|
if writer.attrs["pl"]["title"] != "Witaj" {
|
|
t.Fatalf("Polish attributes = %#v", writer.attrs)
|
|
}
|
|
|
|
_, err = svc.Create(context.Background(), cc, RecordInput{Body: map[string]any{
|
|
"title": map[string]any{"en": "Hello", "de": "Hallo"},
|
|
}})
|
|
if err == nil {
|
|
t.Fatal("undeclared locale succeeded")
|
|
}
|
|
_, err = svc.Create(context.Background(), cc, RecordInput{Body: map[string]any{
|
|
"title": map[string]any{"en": 1, "pl": "x"},
|
|
}})
|
|
if err == nil {
|
|
t.Fatal("non-string locale value succeeded")
|
|
}
|
|
}
|
|
|
|
func TestMLHydration(t *testing.T) {
|
|
_, db := newListService(t)
|
|
if err := db.Migrator().DropTable(&mlPost{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(&mlPost{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writer := &recordingWriter{defaultLocale: "en", enabled: []string{"en", "pl"}}
|
|
svc := CRUDService{DB: db, writer: writer}
|
|
cc := mlCompiled(t)
|
|
ctx := context.Background()
|
|
|
|
created, err := svc.CreateRecord(ctx, cc, RecordInput{Body: map[string]any{
|
|
"title": map[string]any{"en": "Hello", "pl": "Witaj"},
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
got, ok := created.Data["title"].(map[string]string)
|
|
if !ok || got["en"] != "Hello" || got["pl"] != "Witaj" {
|
|
t.Fatalf("create title = %#v", created.Data["title"])
|
|
}
|
|
|
|
shown, err := svc.ShowRecord(ctx, cc, created.Data["id"])
|
|
if err != nil {
|
|
t.Fatalf("show: %v", err)
|
|
}
|
|
got, ok = shown.Data["title"].(map[string]string)
|
|
if !ok || got["en"] != "Hello" || got["pl"] != "Witaj" {
|
|
t.Fatalf("show title = %#v", shown.Data["title"])
|
|
}
|
|
|
|
englishOnly := &recordingWriter{defaultLocale: "en", enabled: []string{"en", "pl"}}
|
|
svcOnly := CRUDService{DB: db, writer: englishOnly}
|
|
row := mlPost{Title: "Hello"}
|
|
if err := db.Create(&row).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
shown, err = svcOnly.ShowRecord(ctx, cc, row.ID)
|
|
if err != nil {
|
|
t.Fatalf("show english-only: %v", err)
|
|
}
|
|
got, ok = shown.Data["title"].(map[string]string)
|
|
if !ok || got["en"] != "Hello" || got["pl"] != "" {
|
|
t.Fatalf("english-only title = %#v", shown.Data["title"])
|
|
}
|
|
}
|
|
|
|
func TestML(t *testing.T) {
|
|
t.Run("writer failure rolls back host write", func(t *testing.T) {
|
|
_, db := newListService(t)
|
|
if err := db.Migrator().DropTable(&mlPost{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(&mlPost{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writer := &boomWriter{recordingWriter: recordingWriter{defaultLocale: "en", enabled: []string{"en", "pl"}}}
|
|
svc := CRUDService{DB: db, writer: writer}
|
|
_, err := svc.Create(context.Background(), mlCompiled(t), RecordInput{Body: map[string]any{
|
|
"title": map[string]any{"en": "Hello", "pl": "Witaj"},
|
|
}})
|
|
if err == nil {
|
|
t.Fatal("writer failure succeeded")
|
|
}
|
|
if writer.calls == 0 {
|
|
t.Fatal("writer was not invoked")
|
|
}
|
|
var n int64
|
|
if err := db.Model(&mlPost{}).Count(&n).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if n != 0 {
|
|
t.Fatalf("host rows after writer failure = %d", n)
|
|
}
|
|
})
|
|
|
|
t.Run("writer is not invoked before query checks", func(t *testing.T) {
|
|
_, db := newListService(t)
|
|
if err := db.Migrator().DropTable(&mlPost{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(&mlPost{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.Create(&mlPost{Title: "Hello"}).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writer := &recordingWriter{defaultLocale: "en", enabled: []string{"en", "pl"}}
|
|
svc := CRUDService{DB: db, writer: writer}
|
|
cc := scopedMLCompiled(t)
|
|
_, err := svc.Update(context.Background(), cc, 1, RecordInput{Body: map[string]any{
|
|
"title": map[string]any{"en": "Hello", "pl": "Witaj"},
|
|
}})
|
|
if err == nil {
|
|
t.Fatal("out-of-scope update succeeded")
|
|
}
|
|
if len(writer.ids) != 0 {
|
|
t.Fatalf("writer ran before query scope: %v", writer.ids)
|
|
}
|
|
})
|
|
}
|
|
|
|
type boomWriter struct {
|
|
recordingWriter
|
|
calls int
|
|
}
|
|
|
|
func (w *boomWriter) WriteTranslated(ctx context.Context, tx *gorm.DB, model any, field, locale, value string) error {
|
|
w.calls++
|
|
return errors.New("writer failed")
|
|
}
|
|
|
|
type scopedMLController struct{ mlController }
|
|
|
|
func (scopedMLController) FormExtendQuery(_ context.Context, q *gorm.DB) *gorm.DB {
|
|
return q.Where("1 = 0")
|
|
}
|
|
|
|
func scopedMLCompiled(t *testing.T) *CompiledController {
|
|
t.Helper()
|
|
reg, err := compileRegistry([]controllerRef{{
|
|
plugin: formPlugin{fsys: mlFS()},
|
|
ctl: scopedMLController{},
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("registry: %v", err)
|
|
}
|
|
cc, ok := reg.Get("acme.demo.posts")
|
|
if !ok || cc.Form == nil {
|
|
t.Fatalf("compiled controller missing form: %+v", cc)
|
|
}
|
|
return cc
|
|
}
|
|
|
|
var (
|
|
_ pact.AdminController = scopedMLController{}
|
|
_ pact.AdminRecordSource = scopedMLController{}
|
|
_ pact.FormExtendQuery = scopedMLController{}
|
|
)
|