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:
Jakub Zych
2026-10-02 20:34:23 +02:00
parent 9c87a59532
commit c8d41a68c6
16 changed files with 3509 additions and 10 deletions

View File

@@ -14,6 +14,7 @@ import (
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bonfire"
"git.golem15.com/golem15/summercms/modules/compass"
"git.golem15.com/golem15/summercms/modules/lagoon"
"git.golem15.com/golem15/summercms/modules/pact"
"git.golem15.com/golem15/summercms/modules/party"
"gorm.io/gorm"
@@ -843,3 +844,171 @@ func TestFrameworkScheduleSmoke(t *testing.T) {
t.Fatalf("malformed purge_at = %v", err)
}
}
// frameworkApp is an app with config and extra keys set.
func frameworkApp(t *testing.T, extra map[string]any) *backpack.App {
t.Helper()
cfg, err := compass.Open(compass.Options{Dir: t.TempDir(), Env: "testing", Environ: []string{}})
if err != nil {
t.Fatal(err)
}
for k, v := range extra {
if err := cfg.Set(k, v); err != nil {
t.Fatal(err)
}
}
return backpack.New(cfg)
}
// TestFrameworkSchedulePurgeAt covers lagoon.FrameworkSchedule's reading
// of database.deferred_bindings.purge_at (D-05): the default 03:00, every
// valid HH:MM, an empty or null value disabling the entry, and malformed
// values or types failing with the key named.
func TestFrameworkSchedulePurgeAt(t *testing.T) {
const key = "database.deferred_bindings.purge_at"
for _, tc := range []struct {
value any
set bool
cadence pact.Cadence
none bool
err bool
}{
{set: false, cadence: pact.DailyAt(3, 0)},
{value: "00:00", set: true, cadence: pact.DailyAt(0, 0)},
{value: "23:59", set: true, cadence: pact.DailyAt(23, 59)},
{value: " 07:05 ", set: true, cadence: pact.DailyAt(7, 5)},
{value: "", set: true, none: true},
{value: " ", set: true, none: true},
{value: nil, set: true, none: true},
{value: "24:00", set: true, err: true},
{value: "7:00", set: true, err: true},
{value: "12:60", set: true, err: true},
{value: "12:00:00", set: true, err: true},
{value: "noon", set: true, err: true},
{value: 300, set: true, err: true},
} {
extra := map[string]any{}
if tc.set {
extra[key] = tc.value
}
got, err := lagoon.FrameworkSchedule(frameworkApp(t, extra))
switch {
case tc.err:
if err == nil || !strings.Contains(err.Error(), key) {
t.Fatalf("%v: err = %v, want one naming %s", tc.value, err, key)
}
case tc.none:
if err != nil || len(got) != 0 {
t.Fatalf("%q: %+v %v, want no entry", tc.value, got, err)
}
default:
if err != nil || len(got) != 1 || got[0].Command != "deferred:purge" || len(got[0].Args) != 0 || got[0].Cadence != tc.cadence {
t.Fatalf("%v: %+v %v", tc.value, got, err)
}
}
}
for name, app := range map[string]*backpack.App{"nil app": nil, "no config": backpack.New(nil)} {
if got, err := lagoon.FrameworkSchedule(app); err != nil || got != nil {
t.Fatalf("%s: %+v %v", name, got, err)
}
}
}
// TestFrameworkScheduleEntries: the framework entry is first, listed under
// summercms.lagoon with id summercms.lagoon[0]:deferred:purge, joins the
// compiled table at the configured time, and leaves plugin entry ids
// unchanged.
func TestFrameworkScheduleEntries(t *testing.T) {
plugins := schedulePlugins(
pact.ScheduledCommand{Command: "acme:tick", Cadence: pact.Daily()},
pact.ScheduledCommand{Command: "acme:tock", Cadence: pact.Every(time.Hour)},
)
entries, err := scheduleEntries(frameworkApp(t, map[string]any{"database.deferred_bindings.purge_at": "05:15"}), plugins)
if err != nil {
t.Fatal(err)
}
ids := make([]string, 0, len(entries))
for _, e := range entries {
ids = append(ids, e.id)
}
if strings.Join(ids, ",") != "summercms.lagoon[0]:deferred:purge,acme.test[0]:acme:tick,acme.test[1]:acme:tock" {
t.Fatalf("ids = %v", ids)
}
if entries[0].plugin != lagoon.FrameworkScheduleID || entries[0].cmd.Cadence != pact.DailyAt(5, 15) {
t.Fatalf("framework entry = %+v", entries[0])
}
_, table, err := periodicJobs(frameworkApp(t, nil), plugins)
if err != nil {
t.Fatal(err)
}
if fw, ok := table["summercms.lagoon[0]:deferred:purge"]; !ok || fw.Command != "deferred:purge" || fw.Cadence != pact.DailyAt(3, 0) {
t.Fatalf("compiled table = %+v", table)
}
if _, _, err := periodicJobs(frameworkApp(t, map[string]any{"database.deferred_bindings.purge_at": "25:00"}), plugins); err == nil {
t.Fatal("a malformed purge_at compiled")
}
}
// TestFrameworkScheduleForgedArgs: a job naming the framework entry with
// other args or another command is skipped like any forged scheduled job;
// the exact entry runs deferred:purge with no args.
func TestFrameworkScheduleForgedArgs(t *testing.T) {
db, dsn := migratedDB(t)
app, _ := testApp(t, db, dsn, nil)
logs := &captureHandler{}
if err := app.Publish(slog.New(logs)); err != nil {
t.Fatal(err)
}
var mu sync.Mutex
var ran [][]string
record := func(name string) bonfire.Command {
return bonfire.Command{Name: name, Flags: []bonfire.Flag{{Name: "days"}}, Run: func(ctx context.Context, in bonfire.Input, out bonfire.Output) error {
mu.Lock()
defer mu.Unlock()
days, _ := in.Flag("days")
ran = append(ran, append([]string{name, "days=" + days}, in.Args()...))
return nil
}}
}
if err := app.Publish(bonfire.NewCatalog([]bonfire.Command{record("deferred:purge"), record("acme:other")})); err != nil {
t.Fatal(err)
}
w, err := StartWorker(t.Context(), app, schedulePlugins(pact.ScheduledCommand{Command: "acme:tick", Cadence: pact.Daily()}), WorkerOptions{})
if err != nil {
t.Fatal(err)
}
defer func() { _ = w.Stop(context.Background()) }()
m, err := From(app)
if err != nil {
t.Fatal(err)
}
const entry = "summercms.lagoon[0]:deferred:purge"
const skip = "schedule: job does not match a compiled schedule entry; skipping"
for _, forged := range []ScheduledCommandArgs{
{Entry: entry, Command: "deferred:purge", Args: []string{"--days", "0"}},
{Entry: entry, Command: "acme:other"},
{Entry: "summercms.lagoon[1]:deferred:purge", Command: "deferred:purge"},
} {
if err := m.runScheduled(t.Context(), forged); err != nil {
t.Fatalf("%+v: %v", forged, err)
}
if !logs.find(slog.LevelWarn, skip, "command", forged.Command) {
t.Fatalf("%+v: no skip warning", forged)
}
}
mu.Lock()
got := fmt.Sprint(ran)
mu.Unlock()
if got != "[]" {
t.Fatalf("forged framework jobs ran %s", got)
}
if err := m.runScheduled(t.Context(), ScheduledCommandArgs{Entry: entry, Command: "deferred:purge"}); err != nil {
t.Fatal(err)
}
mu.Lock()
got = fmt.Sprint(ran)
mu.Unlock()
if got != "[[deferred:purge days=]]" {
t.Fatalf("ran = %s", got)
}
}