214 lines
8.4 KiB
Go
214 lines
8.4 KiB
Go
package cabana
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io/fs"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
"time"
|
|
|
|
"git.golem15.com/golem15/summercms/backpack"
|
|
"git.golem15.com/golem15/summercms/bouncer"
|
|
"git.golem15.com/golem15/summercms/pact"
|
|
"git.golem15.com/golem15/summercms/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)
|
|
}
|
|
}
|