- 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
250 lines
7.4 KiB
Go
250 lines
7.4 KiB
Go
package pact
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"io/fs"
|
|
"strings"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/bonfire"
|
|
"github.com/go-gormigrate/gormigrate/v2"
|
|
)
|
|
|
|
type configOnly struct{}
|
|
|
|
func (configOnly) ConfigFS() fs.FS {
|
|
return fstest.MapFS{"config/config.yaml": {Data: []byte("k: 1\n")}}
|
|
}
|
|
|
|
type commandsOnly struct{}
|
|
|
|
func (commandsOnly) Commands() []bonfire.Command {
|
|
return []bonfire.Command{{Name: "demo:run"}}
|
|
}
|
|
|
|
type bothCaps struct {
|
|
configOnly
|
|
commandsOnly
|
|
}
|
|
|
|
type neither struct{}
|
|
|
|
type migrationsOnly struct{}
|
|
|
|
func (migrationsOnly) Migrations() []*gormigrate.Migration {
|
|
return []*gormigrate.Migration{{ID: "202609170001_create_users"}}
|
|
}
|
|
|
|
func TestOptionalCapabilitiesDiscoveredByTypeAssertion(t *testing.T) {
|
|
plugins := []any{configOnly{}, commandsOnly{}, bothCaps{}, neither{}, extraMessage{}, migrationsOnly{}}
|
|
|
|
var configs, commands, messages, migrations, none int
|
|
for _, p := range plugins {
|
|
_, hasCfg := p.(HasConfig)
|
|
_, hasCmd := p.(HasCommands)
|
|
_, hasMsg := p.(OptionalMessage)
|
|
_, hasMig := p.(HasMigrations)
|
|
if hasCfg {
|
|
configs++
|
|
}
|
|
if hasCmd {
|
|
commands++
|
|
}
|
|
if hasMsg {
|
|
messages++
|
|
}
|
|
if hasMig {
|
|
migrations++
|
|
}
|
|
if !hasCfg && !hasCmd && !hasMsg && !hasMig {
|
|
none++
|
|
}
|
|
}
|
|
if configs != 2 {
|
|
t.Fatalf("HasConfig count = %d, want 2", configs)
|
|
}
|
|
if commands != 2 {
|
|
t.Fatalf("HasCommands count = %d, want 2", commands)
|
|
}
|
|
if messages != 1 {
|
|
t.Fatalf("OptionalMessage count = %d, want 1", messages)
|
|
}
|
|
if migrations != 1 {
|
|
t.Fatalf("HasMigrations count = %d, want 1", migrations)
|
|
}
|
|
if none != 1 {
|
|
t.Fatalf("neither count = %d, want 1", none)
|
|
}
|
|
}
|
|
|
|
func TestHasMigrationsReturnsOrderedSet(t *testing.T) {
|
|
var p HasMigrations = migrationsOnly{}
|
|
set := p.Migrations()
|
|
if len(set) != 1 || set[0].ID != "202609170001_create_users" {
|
|
t.Fatalf("Migrations = %+v", set)
|
|
}
|
|
}
|
|
|
|
func TestHasConfigReturnsFilesystem(t *testing.T) {
|
|
var p HasConfig = configOnly{}
|
|
fsys := p.ConfigFS()
|
|
if fsys == nil {
|
|
t.Fatal("ConfigFS returned nil")
|
|
}
|
|
raw, err := fs.ReadFile(fsys, "config/config.yaml")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(raw) != "k: 1\n" {
|
|
t.Fatalf("ConfigFS body = %q", raw)
|
|
}
|
|
}
|
|
|
|
func TestHasCommandsReturnsNamespacedCommand(t *testing.T) {
|
|
var p HasCommands = commandsOnly{}
|
|
cmds := p.Commands()
|
|
if len(cmds) != 1 || cmds[0].Name != "demo:run" {
|
|
t.Fatalf("Commands = %+v", cmds)
|
|
}
|
|
}
|
|
|
|
func TestOptionalMessageContract(t *testing.T) {
|
|
var msg OptionalMessage = extraMessage{s: "hello-from-optional"}
|
|
if msg.Message() != "hello-from-optional" {
|
|
t.Fatalf("Message = %q", msg.Message())
|
|
}
|
|
}
|
|
|
|
type extraMessage struct{ s string }
|
|
|
|
func (e extraMessage) Message() string { return e.s }
|
|
|
|
type routesOnly struct{}
|
|
|
|
func (routesOnly) Routes(Router) error { return nil }
|
|
|
|
type middlewareOnly struct{}
|
|
|
|
func (middlewareOnly) Middlewares() map[string]Middleware {
|
|
return map[string]Middleware{"jwt.auth": nil}
|
|
}
|
|
|
|
type modelsOnly struct{}
|
|
|
|
func (modelsOnly) Models() []any { return []any{struct{}{}} }
|
|
|
|
func TestHTTPCapabilitiesDiscoveredByTypeAssertion(t *testing.T) {
|
|
var r HasRoutes = routesOnly{}
|
|
if err := r.Routes(nil); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m HasMiddleware = middlewareOnly{}
|
|
if _, ok := m.Middlewares()["jwt.auth"]; !ok {
|
|
t.Fatal("missing jwt.auth")
|
|
}
|
|
var models HasModels = modelsOnly{}
|
|
if len(models.Models()) != 1 {
|
|
t.Fatalf("Models = %+v", models.Models())
|
|
}
|
|
}
|
|
|
|
// relationHooks implements every relation child hook with the documented
|
|
// signature and records the calls.
|
|
type relationHooks struct{ calls *[]string }
|
|
|
|
func (h relationHooks) record(name, relation string, parent, child any) error {
|
|
*h.calls = append(*h.calls, name+":"+relation+":"+parent.(string)+":"+child.(string))
|
|
if relation == "fail" {
|
|
return errors.New(name + " refused")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (h relationHooks) RelationBeforeCreate(_ context.Context, relation string, parent, child any) error {
|
|
return h.record("BeforeCreate", relation, parent, child)
|
|
}
|
|
func (h relationHooks) RelationAfterCreate(_ context.Context, relation string, parent, child any) error {
|
|
return h.record("AfterCreate", relation, parent, child)
|
|
}
|
|
func (h relationHooks) RelationBeforeUpdate(_ context.Context, relation string, parent, child any) error {
|
|
return h.record("BeforeUpdate", relation, parent, child)
|
|
}
|
|
func (h relationHooks) RelationAfterUpdate(_ context.Context, relation string, parent, child any) error {
|
|
return h.record("AfterUpdate", relation, parent, child)
|
|
}
|
|
func (h relationHooks) RelationBeforeDelete(_ context.Context, relation string, parent, child any) error {
|
|
return h.record("BeforeDelete", relation, parent, child)
|
|
}
|
|
func (h relationHooks) RelationAfterDelete(_ context.Context, relation string, parent, child any) error {
|
|
return h.record("AfterDelete", relation, parent, child)
|
|
}
|
|
|
|
var (
|
|
_ RelationBeforeCreate = relationHooks{}
|
|
_ RelationAfterCreate = relationHooks{}
|
|
_ RelationBeforeUpdate = relationHooks{}
|
|
_ RelationAfterUpdate = relationHooks{}
|
|
_ RelationBeforeDelete = relationHooks{}
|
|
_ RelationAfterDelete = relationHooks{}
|
|
)
|
|
|
|
// TestRelationHookInterfaces: each of the six optional relation child hooks
|
|
// is discovered by a type assertion on the controller value, receives the
|
|
// relation name, the parent and the child, and passes its error back; a
|
|
// controller without them asserts false.
|
|
func TestRelationHookInterfaces(t *testing.T) {
|
|
var calls []string
|
|
var ctl any = relationHooks{calls: &calls}
|
|
hooks := []func(context.Context, string, any, any) error{}
|
|
if h, ok := ctl.(RelationBeforeCreate); ok {
|
|
hooks = append(hooks, h.RelationBeforeCreate)
|
|
}
|
|
if h, ok := ctl.(RelationAfterCreate); ok {
|
|
hooks = append(hooks, h.RelationAfterCreate)
|
|
}
|
|
if h, ok := ctl.(RelationBeforeUpdate); ok {
|
|
hooks = append(hooks, h.RelationBeforeUpdate)
|
|
}
|
|
if h, ok := ctl.(RelationAfterUpdate); ok {
|
|
hooks = append(hooks, h.RelationAfterUpdate)
|
|
}
|
|
if h, ok := ctl.(RelationBeforeDelete); ok {
|
|
hooks = append(hooks, h.RelationBeforeDelete)
|
|
}
|
|
if h, ok := ctl.(RelationAfterDelete); ok {
|
|
hooks = append(hooks, h.RelationAfterDelete)
|
|
}
|
|
if len(hooks) != 6 {
|
|
t.Fatalf("discovered %d hooks, want 6", len(hooks))
|
|
}
|
|
for _, hook := range hooks {
|
|
if err := hook(t.Context(), "parts", "gadget", "part"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := hook(t.Context(), "fail", "gadget", "part"); err == nil {
|
|
t.Fatal("hook error was not passed back")
|
|
}
|
|
}
|
|
want := "BeforeCreate:parts:gadget:part,BeforeCreate:fail:gadget:part,AfterCreate:parts:gadget:part,AfterCreate:fail:gadget:part," +
|
|
"BeforeUpdate:parts:gadget:part,BeforeUpdate:fail:gadget:part,AfterUpdate:parts:gadget:part,AfterUpdate:fail:gadget:part," +
|
|
"BeforeDelete:parts:gadget:part,BeforeDelete:fail:gadget:part,AfterDelete:parts:gadget:part,AfterDelete:fail:gadget:part"
|
|
if got := strings.Join(calls, ","); got != want {
|
|
t.Fatalf("calls = %s", got)
|
|
}
|
|
var plain any = struct{}{}
|
|
for name, ok := range map[string]bool{
|
|
"BeforeCreate": func() bool { _, ok := plain.(RelationBeforeCreate); return ok }(),
|
|
"AfterCreate": func() bool { _, ok := plain.(RelationAfterCreate); return ok }(),
|
|
"BeforeUpdate": func() bool { _, ok := plain.(RelationBeforeUpdate); return ok }(),
|
|
"AfterUpdate": func() bool { _, ok := plain.(RelationAfterUpdate); return ok }(),
|
|
"BeforeDelete": func() bool { _, ok := plain.(RelationBeforeDelete); return ok }(),
|
|
"AfterDelete": func() bool { _, ok := plain.(RelationAfterDelete); return ok }(),
|
|
} {
|
|
if ok {
|
|
t.Fatalf("a plain value implements Relation%s", name)
|
|
}
|
|
}
|
|
}
|