package cabana import ( "context" "errors" "io/fs" "reflect" "strings" "testing" "testing/fstest" "time" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bouncer" "git.golem15.com/golem15/summercms/modules/pact" "git.golem15.com/golem15/summercms/modules/party" "gorm.io/gorm" ) type contributionPlugin struct { id string permissions []pact.Permission navigation []pact.NavigationItem settings []pact.SettingsItem fsys fs.FS } func (p contributionPlugin) ID() string { return p.id } func (p contributionPlugin) Requires() []string { return nil } func (p contributionPlugin) Register(*backpack.App) error { return nil } func (p contributionPlugin) Boot(*backpack.App) error { return nil } func (p contributionPlugin) Permissions() []pact.Permission { return p.permissions } func (p contributionPlugin) Navigation() []pact.NavigationItem { return p.navigation } func (p contributionPlugin) Settings() []pact.SettingsItem { return p.settings } func (p contributionPlugin) AdminFS() fs.FS { return p.fsys } type metadataController struct{ id string } func (c metadataController) ID() string { return c.id } func (metadataController) ModelName() string { return "Metadata" } func (metadataController) ConfigDir() string { return "controllers/metadata" } func (metadataController) RequiredPermissions() []string { return []string{"acme.demo.access"} } type singletonSetting struct { ID uint `gorm:"column:id;primaryKey"` Enabled bool `gorm:"column:enabled"` CreatedAt time.Time `gorm:"column:created_at"` UpdatedAt time.Time `gorm:"column:updated_at"` } func (singletonSetting) TableName() string { return "cabana_singleton_settings" } func (singletonSetting) Fillable() []string { return []string{"enabled"} } func (singletonSetting) Rules() map[string]string { return map[string]string{"enabled": "boolean"} } func TestMetadataPermissionRegistry(t *testing.T) { plugin := metadataPlugin() reg := metadataRegistry() if err := compileContributions(reg, []party.Plugin{plugin}); err != nil { t.Fatal(err) } grants := reg.rolePermissions("developer") if !grants["acme.demo.access"] || !grants["acme.demo.manage_settings"] { t.Fatalf("developer grants = %#v", grants) } if grants := reg.rolePermissions("publisher"); len(grants) != 0 { t.Fatalf("publisher grants = %#v", grants) } bad := metadataRegistry() bad.byID["acme.demo.widgets"].Controller = metadataController{id: "acme.demo.widgets"} bad.byID["acme.demo.widgets"].Controller = unknownPermissionController{} if err := compileContributions(bad, []party.Plugin{plugin}); err == nil || !strings.Contains(err.Error(), "unknown permission") { t.Fatalf("unknown permission error = %v", err) } duplicate := contributionPlugin{id: "other.demo", permissions: plugin.permissions} if err := compileContributions(metadataRegistry(), []party.Plugin{plugin, duplicate}); err == nil || !strings.Contains(err.Error(), "duplicate permission") { t.Fatalf("duplicate permission error = %v", err) } } func TestMetadataFiltering(t *testing.T) { plugin := metadataPlugin() reg := metadataRegistry() if err := compileContributions(reg, []party.Plugin{plugin}); err != nil { t.Fatal(err) } developer := &bouncer.Principal{ID: 1, Backend: true, PermissionGrants: reg.rolePermissions("developer")} navigation, settings := reg.Metadata(context.Background(), developer, nil) if len(navigation) != 1 || len(navigation[0].SideMenu) != 1 || navigation[0].Code != "demo" || navigation[0].SideMenu[0].Code != "widgets" { t.Fatalf("developer navigation = %#v", navigation) } if len(settings) != 1 || settings[0].Code != "demo" || !reflect.DeepEqual(settings[0].Keywords, []string{"demo", "settings"}) { t.Fatalf("developer settings = %#v", settings) } denied := &bouncer.Principal{ID: 2, Backend: true, PermissionGrants: map[string]bool{}} navigation, settings = reg.Metadata(context.Background(), denied, nil) if navigation == nil || settings == nil || len(navigation) != 0 || len(settings) != 0 { t.Fatalf("denied metadata = %#v %#v", navigation, settings) } frontend := &bouncer.Principal{ID: 1, PermissionGrants: reg.rolePermissions("developer")} navigation, settings = reg.Metadata(context.Background(), frontend, nil) if len(navigation) != 0 || len(settings) != 0 { t.Fatalf("frontend principal leaked metadata = %#v %#v", navigation, settings) } } func TestSettingsSingletonLifecycle(t *testing.T) { _, db := newListService(t) if err := db.Migrator().DropTable(&singletonSetting{}); err != nil { t.Fatal(err) } if err := db.AutoMigrate(&singletonSetting{}); err != nil { t.Fatal(err) } setting, err := compileSetting("acme.demo", metadataPlugin().settings[0], metadataFS()) if err != nil { t.Fatal(err) } svc := SettingsService{DB: db} missing, err := svc.Get(context.Background(), setting) if err != nil || missing.Exists || missing.Data["enabled"] != false { t.Fatalf("missing = %#v err=%v", missing, err) } assertSettingCount(t, db, 0) if _, err := svc.Put(context.Background(), setting, map[string]any{}); err == nil { t.Fatal("missing required setting succeeded") } created, err := svc.Put(context.Background(), setting, map[string]any{"enabled": true, "id": 99}) if err != nil || !created.Exists || created.Data["enabled"] != true { t.Fatalf("created = %#v err=%v", created, err) } assertSettingCount(t, db, 1) var before singletonSetting if err := db.First(&before, 1).Error; err != nil { t.Fatal(err) } replayed, err := svc.Put(context.Background(), setting, map[string]any{"enabled": true}) if err != nil || replayed.Data["enabled"] != true { t.Fatalf("replay = %#v err=%v", replayed, err) } var after singletonSetting if err := db.First(&after, 1).Error; err != nil { t.Fatal(err) } if !after.UpdatedAt.Equal(before.UpdatedAt) || after.ID != 1 { t.Fatalf("replay mutated singleton before=%+v after=%+v", before, after) } if _, err := svc.Put(context.Background(), setting, map[string]any{"enabled": "not-bool"}); err == nil { t.Fatal("invalid boolean succeeded") } else { var invalid *ValidationError if !errors.As(err, &invalid) { t.Fatalf("invalid boolean error = %T %v", err, err) } } var preserved singletonSetting if err := db.First(&preserved, 1).Error; err != nil || !preserved.Enabled { t.Fatalf("failed update was not rolled back: %+v err=%v", preserved, err) } } type unknownPermissionController struct{} func (unknownPermissionController) ID() string { return "acme.demo.widgets" } func (unknownPermissionController) ModelName() string { return "Metadata" } func (unknownPermissionController) ConfigDir() string { return "controllers/metadata" } func (unknownPermissionController) RequiredPermissions() []string { return []string{"acme.demo.unknown"} } func metadataRegistry() *Registry { return &Registry{byID: map[string]*CompiledController{ "acme.demo.widgets": {Controller: metadataController{id: "acme.demo.widgets"}, Relations: map[string]*CompiledRelation{}}, }} } func metadataPlugin() contributionPlugin { return contributionPlugin{ id: "acme.demo", permissions: []pact.Permission{ {Code: "acme.demo.access", Roles: []string{"developer"}}, {Code: "acme.demo.manage_settings", Roles: []string{"developer"}}, }, navigation: []pact.NavigationItem{{ Code: "demo", Label: "Demo", Icon: "icon", Order: 20, Controller: "acme.demo.widgets", Permissions: []string{"acme.demo.*"}, SideMenu: []pact.NavigationItem{{Code: "widgets", Label: "Widgets", Controller: "acme.demo.widgets", Permissions: []string{"acme.demo.access"}}}, }}, settings: []pact.SettingsItem{{ Code: "demo", Label: "Settings", Description: "Description", Category: "Demo", Icon: "icon-cog", Model: "Settings", Order: 20, Keywords: []string{"demo", "settings"}, Permissions: []string{"acme.demo.manage_settings"}, Form: "models/settings/fields.yaml", NewModel: func() any { return &singletonSetting{} }, }}, fsys: metadataFS(), } } func metadataFS() fs.FS { return fstest.MapFS{"models/settings/fields.yaml": {Data: []byte("fields:\n enabled:\n type: switch\n default: 0\n required: true\n")}} } func assertSettingCount(t *testing.T, db *gorm.DB, want int64) { t.Helper() var got int64 if err := db.Model(&singletonSetting{}).Count(&got).Error; err != nil { t.Fatal(err) } if got != want { t.Fatalf("settings count=%d want=%d", got, want) } }