fix(10.1): CR-01 answer a value that does not fit its column with a 422

lagoon.Fill now returns a *lagoon.FillTypeError naming the key when a
requested value cannot be stored in its column (a fraction, exponent or
overflow for an integer field, or a value of the wrong type). The admin
save path maps it to a validation_failed 422 on that field instead of a
500 CapabilityError; genuine capability failures keep the 500.
This commit is contained in:
Jakub Zych
2026-09-29 09:44:14 +02:00
parent 74e30c9878
commit e60e69745e
6 changed files with 152 additions and 4 deletions

View File

@@ -469,3 +469,86 @@ func crudBody(t *testing.T, raw string) map[string]any {
}
return body
}
type crudYearRow struct {
ID uint `gorm:"column:id;primaryKey"`
Name string `gorm:"column:name"`
Year *int `gorm:"column:year"`
}
func (crudYearRow) TableName() string { return "cabana_crud_year_rows" }
func (crudYearRow) Fillable() []string { return []string{"name", "year"} }
func (crudYearRow) Rules() map[string]string { return map[string]string{"name": "required"} }
// TestCRUDFillTypeIsValidation covers CR-01: a value lagoon.Fill cannot store
// in its column (a fraction, an exponent or an overflow for an integer field,
// or a value of the wrong type) is the admin's input, so create and update
// answer 422 with a message on that field rather than a 500 capability error.
func TestCRUDFillTypeIsValidation(t *testing.T) {
_, db := newListService(t)
if err := db.Migrator().DropTable(&crudYearRow{}); err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&crudYearRow{}); err != nil {
t.Fatal(err)
}
fsys := crudFS()
fsys["models/record/fields.yaml"] = &fstest.MapFile{Data: []byte(`fields:
name:
label: Name
type: text
required: true
year:
label: Year
type: number
`)}
reg, err := compileRegistry([]controllerRef{{
plugin: formPlugin{fsys: fsys},
ctl: crudController{rec: func() any { return &crudYearRow{} }},
}})
if err != nil {
t.Fatalf("registry: %v", err)
}
cc, ok := reg.Get("acme.demo.records")
if !ok {
t.Fatal("compiled controller missing")
}
svc := CRUDService{DB: db}
ctx := context.Background()
count := func() int64 {
t.Helper()
var n int64
if err := db.Model(&crudYearRow{}).Count(&n).Error; err != nil {
t.Fatal(err)
}
return n
}
for _, raw := range []string{`{"name":"Ada","year":1977.5}`, `{"name":"Ada","year":1e21}`, `{"name":"Ada","year":99999999999999999999}`, `{"name":"Ada","year":"1977"}`} {
_, err := svc.Create(ctx, cc, RecordInput{Body: crudBody(t, raw)})
assertValidation(t, err, "year", "The year field has an invalid value.")
if n := count(); n != 0 {
t.Fatalf("%s: rows=%d, an invalid year persisted", raw, n)
}
}
_, err = svc.Create(ctx, cc, RecordInput{Body: crudBody(t, `{"name":true}`)})
assertValidation(t, err, "name", "The name field has an invalid value.")
rec, err := svc.Create(ctx, cc, RecordInput{Body: crudBody(t, `{"name":"Ada","year":1977}`)})
if err != nil || rec["year"] == nil {
t.Fatalf("create=%#v err=%v, want year 1977", rec, err)
}
var stored crudYearRow
if err := db.Where("name = ?", "Ada").Take(&stored).Error; err != nil {
t.Fatal(err)
}
_, err = svc.Update(ctx, cc, stored.ID, RecordInput{Body: crudBody(t, `{"year":1977.5}`)})
assertValidation(t, err, "year", "The year field has an invalid value.")
var after crudYearRow
if err := db.Take(&after, stored.ID).Error; err != nil {
t.Fatal(err)
}
if after.Year == nil || *after.Year != 1977 {
t.Fatalf("year after rejected update = %v, want 1977", after.Year)
}
}