package cabana import ( "context" "errors" "reflect" "strings" "testing" "testing/fstest" "git.golem15.com/golem15/summercms/modules/lagoon" "git.golem15.com/golem15/summercms/modules/pact" ) // stringURLSettings is a settings model whose rules use the Laravel string // and url tokens the way plugin settings models do (title, feed URL, // language code); one rule (code) has no string token, so its max is a // length all the same. type stringURLSettings struct { ID uint `gorm:"column:id;primaryKey"` Title *string `gorm:"column:title"` Website *string `gorm:"column:website"` Language string `gorm:"column:language"` Code *string `gorm:"column:code"` } func (stringURLSettings) TableName() string { return "cabana_string_url_settings" } func (stringURLSettings) Fillable() []string { return []string{"title", "website", "language", "code"} } func (stringURLSettings) Rules() map[string]string { return map[string]string{ "title": "nullable|string|max:255", "website": "nullable|url|max:500", "language": "nullable|string|max:10", "code": "nullable|max:5", } } const stringURLSettingsFields = "fields:\n title:\n type: text\n website:\n type: text\n language:\n type: text\n code:\n type: text\n" func compileStringURLSetting(t *testing.T) *CompiledSetting { t.Helper() fsys := fstest.MapFS{"models/settings/fields.yaml": &fstest.MapFile{Data: []byte(stringURLSettingsFields)}} item := pact.SettingsItem{Code: "feed", Label: "Feed", Form: "models/settings/fields.yaml", Model: "StringURLSettings", NewModel: func() any { return &stringURLSettings{} }} setting, err := compileSetting("acme.unit", item, fsys) if err != nil { t.Fatal(err) } return setting } // TestSettingsStringURLRules runs the validation sequence of // SettingsService.Put without a database: the string and url rules are // accepted, valid, empty and null values pass, and an invalid URL or an // over-long value (with or without the string token) is a message on the // field. func TestSettingsStringURLRules(t *testing.T) { setting := compileStringURLSetting(t) cases := []struct { name string body map[string]any want map[string][]string }{ {"valid", map[string]any{"title": "Feed", "website": "https://example.com/feed.png", "language": "pl"}, nil}, {"empty website", map[string]any{"website": ""}, nil}, {"null website and title", map[string]any{"website": nil, "title": nil}, nil}, {"invalid website", map[string]any{"website": "not a url"}, map[string][]string{"website": {"The website format is invalid."}}}, {"title too long", map[string]any{"title": strings.Repeat("a", 256)}, map[string][]string{"title": {"The title may not be greater than 255 characters."}}}, {"code at max", map[string]any{"code": "abcde"}, nil}, {"empty code", map[string]any{"code": ""}, nil}, {"code too long", map[string]any{"code": "abcdef"}, map[string][]string{"code": {"The code may not be greater than 5 characters."}}}, } for _, tc := range cases { model := &stringURLSettings{} if err := lagoon.Fill(model, settingWritable(setting), projectSettingBody(setting, tc.body), false); err != nil { t.Fatalf("%s: fill: %v", tc.name, err) } rules := settingRules(setting, model) got, err := lagoon.Validate(context.Background(), nil, model, rules, valuesForRules(model, rules), nil) if err != nil { t.Fatalf("%s: validate: %v", tc.name, err) } if len(got) == 0 && len(tc.want) == 0 { continue } if !reflect.DeepEqual(got, tc.want) { t.Fatalf("%s: messages = %v, want %v", tc.name, got, tc.want) } } } // TestSettingsStringURLRulesPostgres saves the settings row through // SettingsService.Put: a valid body is stored, an invalid URL is a 422 on // the field and leaves the stored row as it was. func TestSettingsStringURLRulesPostgres(t *testing.T) { _, db := newListService(t) if err := db.Migrator().DropTable(&stringURLSettings{}); err != nil { t.Fatal(err) } if err := db.AutoMigrate(&stringURLSettings{}); err != nil { t.Fatal(err) } setting := compileStringURLSetting(t) svc := SettingsService{DB: db} ctx := context.Background() const feedURL = "https://example.com/feed.png" saved, err := svc.Put(ctx, setting, map[string]any{"title": "Feed", "website": feedURL, "language": "pl"}) if err != nil || !saved.Exists { t.Fatalf("valid put = %#v err=%v", saved, err) } var count int64 if err := db.Model(&stringURLSettings{}).Count(&count).Error; err != nil || count != 1 { t.Fatalf("rows = %d err=%v", count, err) } _, err = svc.Put(ctx, setting, map[string]any{"website": "not a url"}) var invalid *ValidationError if !errors.As(err, &invalid) { t.Fatalf("invalid url error = %T %v", err, err) } if got, _ := invalid.Details["website"].([]string); !reflect.DeepEqual(got, []string{"The website format is invalid."}) { t.Fatalf("details = %#v", invalid.Details) } var stored stringURLSettings if err := db.First(&stored, 1).Error; err != nil { t.Fatal(err) } if stored.Website == nil || *stored.Website != feedURL { t.Fatalf("stored website = %v", stored.Website) } _, err = svc.Put(ctx, setting, map[string]any{"title": strings.Repeat("a", 256)}) if !errors.As(err, &invalid) { t.Fatalf("long title error = %T %v", err, err) } if got, _ := invalid.Details["title"].([]string); !reflect.DeepEqual(got, []string{"The title may not be greater than 255 characters."}) { t.Fatalf("details = %#v", invalid.Details) } stored = stringURLSettings{} if err := db.First(&stored, 1).Error; err != nil { t.Fatal(err) } if stored.Title == nil || *stored.Title != "Feed" { t.Fatalf("stored title = %v", stored.Title) } }