- table-driven hostScalarString cases for nil and live pointers to string, ints, uint, float, bool, []byte, named string and double pointers - DB-free hydrateMLRecord cases for nil, empty and filled *string hosts
255 lines
7.7 KiB
Go
255 lines
7.7 KiB
Go
package cabana
|
|
|
|
import (
|
|
"context"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const mlNullableFields = `fields:
|
|
title:
|
|
label: Title
|
|
type: mltext
|
|
required: true
|
|
description:
|
|
label: Description
|
|
type: mltext
|
|
`
|
|
|
|
type mlNullablePost struct {
|
|
ID uint `gorm:"column:id;primaryKey"`
|
|
Title string `gorm:"column:title"`
|
|
Description *string `gorm:"column:description"`
|
|
}
|
|
|
|
func (mlNullablePost) TableName() string { return "cabana_ml_nullable_posts" }
|
|
func (mlNullablePost) Fillable() []string { return []string{"title", "description"} }
|
|
func (mlNullablePost) Rules() map[string]string { return map[string]string{"title": "required"} }
|
|
|
|
type mlNullableController struct{}
|
|
|
|
func (mlNullableController) ID() string { return "acme.demo.posts" }
|
|
func (mlNullableController) ModelName() string { return "Post" }
|
|
func (mlNullableController) ConfigDir() string { return "controllers/posts" }
|
|
func (mlNullableController) NewRecord() any { return &mlNullablePost{} }
|
|
|
|
func (mlNullableController) FormExtendQuery(ctx context.Context, q *gorm.DB) *gorm.DB {
|
|
return q
|
|
}
|
|
|
|
var (
|
|
_ pact.AdminController = mlNullableController{}
|
|
_ pact.AdminRecordSource = mlNullableController{}
|
|
_ pact.FormExtendQuery = mlNullableController{}
|
|
)
|
|
|
|
func mlNullableFS() fstest.MapFS {
|
|
fsys := mlFS()
|
|
fsys["models/post/fields.yaml"] = &fstest.MapFile{Data: []byte(mlNullableFields)}
|
|
return fsys
|
|
}
|
|
|
|
func mlNullableCompiled(t *testing.T) *CompiledController {
|
|
t.Helper()
|
|
reg, err := compileRegistry([]controllerRef{{
|
|
plugin: formPlugin{fsys: mlNullableFS()},
|
|
ctl: mlNullableController{},
|
|
}})
|
|
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
|
|
}
|
|
|
|
// assertNoPointerLeak fails when a hydrated value carries the text of a Go
|
|
// pointer instead of the value it points at.
|
|
func assertNoPointerLeak(t *testing.T, where, got string) {
|
|
t.Helper()
|
|
if strings.Contains(got, "<nil>") || strings.HasPrefix(got, "0x") {
|
|
t.Fatalf("%s = %q, leaked a pointer instead of its value", where, got)
|
|
}
|
|
}
|
|
|
|
// mlLocaleMap asserts data[field] is a hydrated locale map with no pointer
|
|
// leak in any locale and returns it.
|
|
func mlLocaleMap(t *testing.T, where string, data map[string]any, field string) map[string]string {
|
|
t.Helper()
|
|
got, ok := data[field].(map[string]string)
|
|
if !ok {
|
|
t.Fatalf("%s %s = %#v, want hydrated locale map", where, field, data[field])
|
|
}
|
|
for code, text := range got {
|
|
assertNoPointerLeak(t, where+" "+field+"."+code, text)
|
|
}
|
|
return got
|
|
}
|
|
|
|
func TestMLNullablePointerHost(t *testing.T) {
|
|
_, db := newListService(t)
|
|
if err := db.Migrator().DropTable(&mlNullablePost{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(&mlNullablePost{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writer := &recordingWriter{defaultLocale: "en", enabled: []string{"en", "pl"}}
|
|
svc := CRUDService{DB: db, writer: writer}
|
|
cc := mlNullableCompiled(t)
|
|
ctx := context.Background()
|
|
|
|
row := mlNullablePost{Title: "Hello"}
|
|
if err := db.Create(&row).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
title := map[string]any{"en": "Hello", "pl": "Witaj"}
|
|
|
|
t.Run("nil pointer host hydrates to empty strings", func(t *testing.T) {
|
|
shown, err := svc.ShowRecord(ctx, cc, row.ID)
|
|
if err != nil {
|
|
t.Fatalf("show: %v", err)
|
|
}
|
|
got := mlLocaleMap(t, "show", shown.Data, "description")
|
|
if got["en"] != "" || got["pl"] != "" || len(got) != 2 {
|
|
t.Fatalf("nil description = %#v, want en and pl empty", got)
|
|
}
|
|
})
|
|
|
|
t.Run("cleared value round-trips as empty string", func(t *testing.T) {
|
|
saved, err := svc.UpdateRecord(ctx, cc, row.ID, RecordInput{Body: map[string]any{
|
|
"title": title,
|
|
"description": map[string]any{"en": "", "pl": ""},
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("update: %v", err)
|
|
}
|
|
if got := mlLocaleMap(t, "save", saved.Data, "description"); got["en"] != "" {
|
|
t.Fatalf("saved description = %#v, want en empty", got)
|
|
}
|
|
shown, err := svc.ShowRecord(ctx, cc, row.ID)
|
|
if err != nil {
|
|
t.Fatalf("show: %v", err)
|
|
}
|
|
if got := mlLocaleMap(t, "show", shown.Data, "description"); got["en"] != "" || got["pl"] != "" {
|
|
t.Fatalf("shown description = %#v, want en and pl empty", got)
|
|
}
|
|
var stored mlNullablePost
|
|
if err := db.First(&stored, row.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stored.Description == nil || *stored.Description != "" {
|
|
t.Fatalf("stored description = %v, want pointer to empty string", stored.Description)
|
|
}
|
|
})
|
|
|
|
t.Run("filled value stores host text and writes translation", func(t *testing.T) {
|
|
saved, err := svc.UpdateRecord(ctx, cc, row.ID, RecordInput{Body: map[string]any{
|
|
"title": title,
|
|
"description": map[string]any{"en": "Opis kategorii", "pl": "Opis"},
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("update: %v", err)
|
|
}
|
|
if got := mlLocaleMap(t, "save", saved.Data, "description"); got["en"] != "Opis kategorii" {
|
|
t.Fatalf("saved description = %#v", got)
|
|
}
|
|
shown, err := svc.ShowRecord(ctx, cc, row.ID)
|
|
if err != nil {
|
|
t.Fatalf("show: %v", err)
|
|
}
|
|
got := mlLocaleMap(t, "show", shown.Data, "description")
|
|
if got["en"] != "Opis kategorii" || got["pl"] != "Opis" {
|
|
t.Fatalf("shown description = %#v", got)
|
|
}
|
|
var stored mlNullablePost
|
|
if err := db.First(&stored, row.ID).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if stored.Description == nil || *stored.Description != "Opis kategorii" {
|
|
t.Fatalf("stored description = %v, want Opis kategorii", stored.Description)
|
|
}
|
|
if writer.attrs["pl"]["description"] != "Opis" {
|
|
t.Fatalf("Polish attributes = %#v", writer.attrs)
|
|
}
|
|
})
|
|
}
|
|
|
|
type nullableSlug string
|
|
|
|
func (nullableSlug) String() string { return "decorated" }
|
|
|
|
func TestHostScalarString(t *testing.T) {
|
|
var nilString *string
|
|
var nilBytes *[]byte
|
|
inner := new("x")
|
|
slug := nullableSlug("raw-slug")
|
|
cases := []struct {
|
|
name string
|
|
in any
|
|
want string
|
|
}{
|
|
{"nil interface", nil, ""},
|
|
{"nil *string", (*string)(nil), ""},
|
|
{"nil *int", (*int)(nil), ""},
|
|
{"**string to nil", &nilString, ""},
|
|
{"nil *[]byte", nilBytes, ""},
|
|
{"pointer to empty", new(""), ""},
|
|
{"pointer to text", new("Opis"), "Opis"},
|
|
{"**string to text", &inner, "x"},
|
|
{"pointer to int", new(42), "42"},
|
|
{"pointer to int64", new(int64(-7)), "-7"},
|
|
{"pointer to uint", new(uint(9)), "9"},
|
|
{"pointer to float64", new(1.5), "1.5"},
|
|
{"pointer to bool", new(true), "true"},
|
|
{"named string value", slug, "raw-slug"},
|
|
{"pointer to named string", &slug, "raw-slug"},
|
|
{"byte slice", []byte("abc"), "abc"},
|
|
{"pointer to byte slice", new([]byte("abc")), "abc"},
|
|
{"plain string", "plain", "plain"},
|
|
{"plain int", 7, "7"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := hostScalarString(tc.in)
|
|
assertNoPointerLeak(t, tc.name, got)
|
|
if got != tc.want {
|
|
t.Fatalf("hostScalarString(%#v) = %q, want %q", tc.in, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestMLHydrationPointerHost(t *testing.T) {
|
|
cc := mlCompiled(t)
|
|
cases := []struct {
|
|
name string
|
|
in any
|
|
want map[string]string
|
|
}{
|
|
{"nil pointer", (*string)(nil), map[string]string{"en": "", "pl": ""}},
|
|
{"pointer to empty", new(""), map[string]string{"en": "", "pl": ""}},
|
|
{"pointer to text", new("Hello"), map[string]string{"en": "Hello", "pl": ""}},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
writer := &recordingWriter{defaultLocale: "en", enabled: []string{"en", "pl"}}
|
|
data := map[string]any{"title": tc.in}
|
|
if err := hydrateMLRecord(context.Background(), nil, cc, writer, nil, data, "update"); err != nil {
|
|
t.Fatalf("hydrate: %v", err)
|
|
}
|
|
got := mlLocaleMap(t, tc.name, data, "title")
|
|
if !reflect.DeepEqual(got, tc.want) {
|
|
t.Fatalf("title = %#v, want %#v", got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|