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{} )