test(12.2-05): cover every Go behaviour of phase 12.2
- lagoon: Date and TimeOfDay through JSON, text and real DATE/TIME columns; Fill text fallback without changing earlier conversions; required on zero dates; deferred_bindings shape, store isolation and envelope; PurgeDeferred cut-off, after-commit blobs, SKIP LOCKED, skipped types and the deferred:purge command - attach: Store limits, extensions, MIME patterns, default lists, key shape and blob cleanup; IsAllowedImage formats, polyglots, ceiling - conga and pact: framework purge schedule entry and forged jobs; the six relation child hook interfaces - cabana: fileupload and datepicker compile, upload, remove, caption, reorder and bounds; deferred commit order, rollback, applied-only and concurrent saves; relation contracts, forms, CRUD, deferral, schema
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"time"
|
||||
|
||||
"git.golem15.com/golem15/summercms/modules/backpack"
|
||||
@@ -59,11 +60,13 @@ type dfGadget struct {
|
||||
Hidden bool `gorm:"column:hidden;not null;default:false"`
|
||||
ReleasedOn lagoon.Date `gorm:"column:released_on;type:date"`
|
||||
StartsAt *time.Time `gorm:"column:starts_at"`
|
||||
OpensAt lagoon.TimeOfDay `gorm:"column:opens_at;type:time"`
|
||||
OpensAt lagoon.TimeOfDay `gorm:"column:opens_at;type:time without time zone"`
|
||||
Members []dfMember `gorm:"-"`
|
||||
Parts []dfPart `gorm:"-"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||
// StrictParts backs a test-only hasMany on the NOT NULL dfStrictPart.
|
||||
StrictParts []dfStrictPart `gorm:"-"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||
}
|
||||
|
||||
func (dfGadget) TableName() string { return "cabana_deferred_gadgets" }
|
||||
@@ -183,13 +186,24 @@ func (r *dfRecorder) hook(ctx context.Context, name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type dfPlugin struct{ rec *dfRecorder }
|
||||
// dfPlugin is the fixture plugin. The zero options give the testdata tree,
|
||||
// the gadgets and locked controllers and the full Models() list; a test
|
||||
// may replace the tree (dfOverlay), the controllers or the models.
|
||||
type dfPlugin struct {
|
||||
rec *dfRecorder
|
||||
fsys fs.FS
|
||||
controllers func(rec *dfRecorder) []pact.AdminController
|
||||
noModels bool
|
||||
}
|
||||
|
||||
func (dfPlugin) ID() string { return "acme.deferred" }
|
||||
func (dfPlugin) Requires() []string { return nil }
|
||||
func (dfPlugin) Register(*backpack.App) error { return nil }
|
||||
func (dfPlugin) Boot(*backpack.App) error { return nil }
|
||||
func (p dfPlugin) AdminControllers() []pact.AdminController {
|
||||
if p.controllers != nil {
|
||||
return p.controllers(p.rec)
|
||||
}
|
||||
return []pact.AdminController{
|
||||
dfController{rec: p.rec, id: "acme.deferred.gadgets", dir: "controllers/gadgets"},
|
||||
dfController{rec: p.rec, id: "acme.deferred.locked", dir: "controllers/locked"},
|
||||
@@ -197,7 +211,12 @@ func (p dfPlugin) AdminControllers() []pact.AdminController {
|
||||
}
|
||||
|
||||
// Models lists every fixture model deferred:purge may delete.
|
||||
func (dfPlugin) Models() []any { return []any{&dfGadget{}, &dfPart{}, &dfMember{}} }
|
||||
func (p dfPlugin) Models() []any {
|
||||
if p.noModels {
|
||||
return nil
|
||||
}
|
||||
return []any{&dfGadget{}, &dfPart{}, &dfMember{}}
|
||||
}
|
||||
|
||||
func (dfPlugin) Permissions() []pact.Permission {
|
||||
return []pact.Permission{{Code: "acme.deferred.access", Roles: []string{"developer"}}}
|
||||
@@ -207,13 +226,84 @@ func (dfPlugin) Navigation() []pact.NavigationItem {
|
||||
Permissions: []string{"acme.deferred.access"}}}
|
||||
}
|
||||
|
||||
// AdminFS is the YAML tree under testdata/deferred.
|
||||
func (dfPlugin) AdminFS() fs.FS { return os.DirFS(filepath.Join("testdata", "deferred")) }
|
||||
// AdminFS is the YAML tree under testdata/deferred, or the test's overlay.
|
||||
func (p dfPlugin) AdminFS() fs.FS {
|
||||
if p.fsys != nil {
|
||||
return p.fsys
|
||||
}
|
||||
return os.DirFS(filepath.Join("testdata", "deferred"))
|
||||
}
|
||||
|
||||
// dfOverlay is the testdata tree with files replaced; an empty value
|
||||
// removes the file.
|
||||
func dfOverlay(t *testing.T, files map[string]string) fs.FS {
|
||||
t.Helper()
|
||||
base := os.DirFS(filepath.Join("testdata", "deferred"))
|
||||
out := fstest.MapFS{}
|
||||
err := fs.WalkDir(base, ".", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil || d.IsDir() {
|
||||
return err
|
||||
}
|
||||
data, err := fs.ReadFile(base, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
out[path] = &fstest.MapFile{Data: data}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, data := range files {
|
||||
if data == "" {
|
||||
delete(out, name)
|
||||
continue
|
||||
}
|
||||
out[name] = &fstest.MapFile{Data: []byte(data)}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// dfFile reads one testdata file, for a test that edits it.
|
||||
func dfFile(t *testing.T, name string) string {
|
||||
t.Helper()
|
||||
raw, err := os.ReadFile(filepath.Join("testdata", "deferred", filepath.FromSlash(name)))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return string(raw)
|
||||
}
|
||||
|
||||
// activateDeferred boots cabana with the fixture plugin p (no database)
|
||||
// and returns the boot error.
|
||||
func activateDeferred(t *testing.T, p dfPlugin, config map[string]any) error {
|
||||
t.Helper()
|
||||
dir := t.TempDir()
|
||||
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: cabana-deferred-boot\n"), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cfg, err := compass.Open(compass.Options{Dir: dir, Environ: []string{"SUMMER_ENV=development", "SUMMER_ADMIN__JWT__SECRET=" + adminTestSecret}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for k, v := range config {
|
||||
if err := cfg.Set(k, v); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if p.rec == nil {
|
||||
p.rec = &dfRecorder{fail: map[string]bool{}}
|
||||
}
|
||||
_, err = cabana.Activate(backpack.New(cfg), []party.Plugin{p})
|
||||
return err
|
||||
}
|
||||
|
||||
type dfController struct {
|
||||
rec *dfRecorder
|
||||
id string
|
||||
dir string
|
||||
// contracts, when set, replaces the relation contracts.
|
||||
contracts func() []cabana.RelationContract
|
||||
}
|
||||
|
||||
func (c dfController) ID() string { return c.id }
|
||||
@@ -221,7 +311,16 @@ func (dfController) ModelName() string { return "Gadget" }
|
||||
func (c dfController) ConfigDir() string { return c.dir }
|
||||
func (dfController) RequiredPermissions() []string { return []string{"acme.deferred.access"} }
|
||||
func (dfController) NewRecord() any { return &dfGadget{} }
|
||||
func (dfController) AdminRelationContracts() []cabana.RelationContract {
|
||||
func (c dfController) AdminRelationContracts() []cabana.RelationContract {
|
||||
if c.contracts != nil {
|
||||
return c.contracts()
|
||||
}
|
||||
return dfContracts()
|
||||
}
|
||||
|
||||
// dfContracts are the fixture's relation contracts: members (belongsToMany
|
||||
// by the empty Kind) and parts (hasMany on the nullable gadget_id).
|
||||
func dfContracts() []cabana.RelationContract {
|
||||
return []cabana.RelationContract{{
|
||||
Name: "members", NewRelated: func() any { return &dfMember{} }, NewPivot: func() any { return &dfGadgetMember{} },
|
||||
ParentForeignKey: "gadget_id", RelatedForeignKey: "member_id", Columns: map[string]string{"email": "email"},
|
||||
@@ -232,6 +331,15 @@ func (dfController) AdminRelationContracts() []cabana.RelationContract {
|
||||
}}
|
||||
}
|
||||
|
||||
// AdminActions registers one widget action, so a relation form's widget
|
||||
// field reaches the D-23 refusal instead of failing on its action.
|
||||
func (dfController) AdminActions() []pact.AdminAction {
|
||||
return []pact.AdminAction{{Name: "lookup", Label: "Look up", Permissions: []string{"acme.deferred.access"},
|
||||
Run: func(context.Context, pact.AdminActionInput) (pact.AdminActionResult, error) {
|
||||
return pact.AdminActionResult{}, nil
|
||||
}}}
|
||||
}
|
||||
|
||||
// FormExtendQuery hides gadgets marked hidden.
|
||||
func (dfController) FormExtendQuery(_ context.Context, db *gorm.DB) *gorm.DB {
|
||||
return db.Where("hidden = ?", false)
|
||||
@@ -298,6 +406,13 @@ type dfClient struct {
|
||||
var dfModels = []any{&dfGadget{}, &dfPart{}, &dfMember{}, &dfGadgetMember{}}
|
||||
|
||||
func newDeferredEnv(t *testing.T) *dfEnv {
|
||||
t.Helper()
|
||||
return newDeferredEnvWith(t, dfPlugin{})
|
||||
}
|
||||
|
||||
// newDeferredEnvWith assembles the router over the plugin p (its recorder
|
||||
// is replaced by the env's).
|
||||
func newDeferredEnvWith(t *testing.T, p dfPlugin) *dfEnv {
|
||||
t.Helper()
|
||||
gdb := adminGorm(t)
|
||||
if err := gdb.Migrator().DropTable(dfModels...); err != nil {
|
||||
@@ -339,7 +454,8 @@ func newDeferredEnv(t *testing.T) *dfEnv {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := &dfRecorder{fail: map[string]bool{}}
|
||||
plugins := []party.Plugin{dfPlugin{rec: rec}}
|
||||
p.rec = rec
|
||||
plugins := []party.Plugin{p}
|
||||
if err := phrasebook.Activate(app, plugins); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -572,11 +688,19 @@ func (e *dfEnv) state(t *testing.T) dfState {
|
||||
for _, q := range []struct {
|
||||
dest any
|
||||
order string
|
||||
}{{&s.Gadgets, "id"}, {&s.Members, "id"}, {&s.Pivots, "id"}, {&s.Files, "id"}, {&s.Bindings, "id"}} {
|
||||
}{{&s.Gadgets, "id"}, {&s.Members, "id"}, {&s.Pivots, "id"}} {
|
||||
if err := e.db.Order(q.order).Find(q.dest).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
// Other tests share the database: only the fixture's files (and the
|
||||
// unattached ones, cleared when the env starts) and bindings count.
|
||||
if err := e.db.Where("attachment_type IN ? OR attachment_id IS NULL OR attachment_id = ''", []string{dfGadgetMorph, dfPartMorph}).Order("id").Find(&s.Files).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := e.db.Where("master_type IN ?", []string{dfGadgetMorph, dfPartMorph}).Order("id").Find(&s.Bindings).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := e.db.Unscoped().Order("id").Find(&s.Parts).Error; err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user