test(11-07): cover jobs, scheduler and lagoon seams branch by branch

- conga: TestOutcome*, TestCancelQueuedNeverRuns, TestCancelRunningCancelsCtx,
  TestStopJobFromWorker, TestManagerPHPSemantics, principal, delay,
  registration, worker and queue-setting branches; TestQueueClear and
  TestQueueWork move to commands_test.go with the batch/state and
  queue-filter cases (coverage 92.5%)
- scheduler: validation, ordering, missing catalog, log writer, dueAt
- lagoon: TestQueueMigrationsUpDown (River v7 + summer_jobs, rollback,
  idempotent rerun), OnDatabase isolation, Transaction edges
- bonfire TestCallEdges, pact TestCadence
This commit is contained in:
Jakub Zych
2026-09-30 14:14:21 +02:00
parent 6f50b6c940
commit 35ac96d664
10 changed files with 1705 additions and 432 deletions

View File

@@ -83,3 +83,50 @@ func TestOnDatabaseAfterActivate(t *testing.T) {
}
})
}
// TestOnDatabaseIsolationAndErrors covers the per-app hook queue: two apps
// keep separate queues, a published app runs later hooks at once, and nil
// arguments are refused.
func TestOnDatabaseIsolationAndErrors(t *testing.T) {
db, _ := dedicatedDB(t, "lagoon_ondatabase_isolation")
gdb, err := Use(t.Context(), db)
if err != nil {
t.Fatal(err)
}
a, b := backpack.New(nil), backpack.New(nil)
var ranA, ranB int
if err := OnDatabase(a, func(*sql.DB, *gorm.DB) error { ranA++; return nil }); err != nil {
t.Fatal(err)
}
if err := OnDatabase(b, func(*sql.DB, *gorm.DB) error { ranB++; return nil }); err != nil {
t.Fatal(err)
}
if err := Publish(a, db, gdb); err != nil {
t.Fatal(err)
}
if ranA != 1 || ranB != 0 {
t.Fatalf("after publishing app a: a ran %d, b ran %d; want 1 and 0", ranA, ranB)
}
if err := Publish(b, db, gdb); err != nil {
t.Fatal(err)
}
if ranA != 1 || ranB != 1 {
t.Fatalf("after publishing app b: a ran %d, b ran %d; want 1 and 1", ranA, ranB)
}
if err := OnDatabase(nil, func(*sql.DB, *gorm.DB) error { return nil }); err == nil {
t.Fatal("nil app accepted")
}
if err := OnDatabase(a, nil); err == nil {
t.Fatal("nil hook accepted")
}
// A second failing queued hook is never reached: Publish returns the
// first error.
c := backpack.New(nil)
boom := errors.New("first")
second := false
_ = OnDatabase(c, func(*sql.DB, *gorm.DB) error { return boom })
_ = OnDatabase(c, func(*sql.DB, *gorm.DB) error { second = true; return nil })
if err := Publish(c, db, gdb); !errors.Is(err, boom) || second {
t.Fatalf("Publish = %v (second ran %v), want the first error only", err, second)
}
}

View File

@@ -0,0 +1,146 @@
package lagoon
import (
"context"
"reflect"
"strings"
"testing"
"github.com/riverqueue/river/rivermigrate"
"gorm.io/gorm"
)
func riverTables(t *testing.T, gdb *gorm.DB) []string {
t.Helper()
var names []string
if err := gdb.Raw(`SELECT tablename FROM pg_tables WHERE schemaname = 'public' AND tablename LIKE 'river\_%' ORDER BY tablename`).Scan(&names).Error; err != nil {
t.Fatal(err)
}
return names
}
func tableExists(t *testing.T, gdb *gorm.DB, name string) bool {
t.Helper()
var n int
if err := gdb.Raw(`SELECT count(*) FROM pg_tables WHERE schemaname = 'public' AND tablename = ?`, name).Scan(&n).Error; err != nil {
t.Fatal(err)
}
return n == 1
}
// TestQueueMigrationsUpDown covers the summercms.conga migration set on a
// fresh database: River's schema pinned at version 7 and summer_jobs with
// the WinterCMS apparatus columns plus river_job_id; rolling the set back
// one step at a time removes summer_jobs and then River's schema; running
// Migrate again restores both and a further run is a no-op.
func TestQueueMigrationsUpDown(t *testing.T) {
db, _ := dedicatedDB(t, "lagoon_queue_migrations")
gdb, err := Use(t.Context(), db)
if err != nil {
t.Fatal(err)
}
if err := Migrate(gdb, nil); err != nil {
t.Fatal(err)
}
// River v7's table set, read from pg_tables after migrating (11-01).
wantRiver := []string{"river_job", "river_leader", "river_migration", "river_notification", "river_queue"}
if got := riverTables(t, gdb); !reflect.DeepEqual(got, wantRiver) {
t.Fatalf("river tables = %v, want %v", got, wantRiver)
}
var version int
if err := gdb.Raw(`SELECT max(version) FROM river_migration`).Scan(&version).Error; err != nil {
t.Fatal(err)
}
if version != RiverSchemaVersion {
t.Fatalf("river schema version = %d, want %d", version, RiverSchemaVersion)
}
type column struct {
Name string `gorm:"column:column_name"`
Type string `gorm:"column:data_type"`
Nullable string `gorm:"column:is_nullable"`
Default *string
}
var cols []column
if err := gdb.Raw(`SELECT column_name, data_type, is_nullable, column_default AS "default"
FROM information_schema.columns WHERE table_name = ? ORDER BY ordinal_position`, JobsTable).Scan(&cols).Error; err != nil {
t.Fatal(err)
}
want := []struct {
name, typ, nullable, def string
}{
{"id", "integer", "NO", "nextval"},
{"label", "character varying", "NO", ""},
{"status", "integer", "NO", "0"},
{"progress", "integer", "NO", "0"},
{"progress_max", "integer", "NO", "0"},
{"user_id", "integer", "YES", ""},
{"is_admin", "boolean", "NO", "false"},
{"is_canceled", "boolean", "NO", "false"},
{"metadata", "text", "NO", ""},
{"river_job_id", "bigint", "YES", ""},
{"created_at", "timestamp with time zone", "YES", ""},
{"updated_at", "timestamp with time zone", "YES", ""},
}
if len(cols) != len(want) {
t.Fatalf("summer_jobs has %d columns, want %d: %+v", len(cols), len(want), cols)
}
for i, w := range want {
c := cols[i]
def := ""
if c.Default != nil {
def = *c.Default
}
if c.Name != w.name || c.Type != w.typ || c.Nullable != w.nullable || !strings.HasPrefix(def, w.def) || (w.def == "" && def != "") {
t.Errorf("column %d = %s %s nullable=%s default=%q, want %s %s nullable=%s default %q", i, c.Name, c.Type, c.Nullable, def, w.name, w.typ, w.nullable, w.def)
}
}
sqlDB, err := gdb.DB()
if err != nil {
t.Fatal(err)
}
m, err := migrator(gdb, QueueHistoryID, QueueMigrations(sqlDB))
if err != nil {
t.Fatal(err)
}
if err := m.RollbackLast(); err != nil {
t.Fatal(err)
}
if tableExists(t, gdb, JobsTable) {
t.Fatal("summer_jobs survived the rollback of its migration")
}
if !tableExists(t, gdb, "river_job") {
t.Fatal("rolling back summer_jobs removed River's schema")
}
if err := m.RollbackLast(); err != nil {
t.Fatal(err)
}
if left := riverTables(t, gdb); len(left) != 0 {
t.Fatalf("River tables left after the rollback: %v", left)
}
if err := Migrate(gdb, nil); err != nil {
t.Fatalf("migrate after rollback: %v", err)
}
if !tableExists(t, gdb, JobsTable) || !tableExists(t, gdb, "river_job") {
t.Fatal("migrate after rollback did not restore the queue tables")
}
if err := Migrate(gdb, nil); err != nil {
t.Fatalf("second migrate is not idempotent: %v", err)
}
var history int
if err := gdb.Raw(`SELECT count(*) FROM summer_migrations_summercms_conga`).Scan(&history).Error; err != nil {
t.Fatal(err)
}
if history != 2 {
t.Fatalf("history rows = %d, want 2", history)
}
if err := migrateRiver(context.Background(), nil, rivermigrate.DirectionUp, nil); err == nil {
t.Fatal("migrateRiver accepted a nil pool")
}
if txContext(nil) == nil || txContext(&gorm.DB{}) == nil {
t.Fatal("txContext returned a nil context")
}
}

View File

@@ -275,3 +275,43 @@ func TestTransactionAfterCommit(t *testing.T) {
}
})
}
// TestTransactionEdges covers the argument edges of Transaction and
// AfterCommit.
func TestTransactionEdges(t *testing.T) {
if err := Transaction(context.Background(), nil, func(context.Context, *gorm.DB) error { return nil }); err == nil {
t.Fatal("Transaction accepted a nil handle")
}
AfterCommit(context.Background(), nil, nil) // a nil fn is a no-op
ran := false
AfterCommit(nil, nil, func(ctx context.Context, db *gorm.DB) {
if ctx == nil || db != nil {
t.Error("nil ctx and handle were not normalized")
}
ran = true
})
if !ran {
t.Fatal("AfterCommit without a handle did not run immediately")
}
if cleanHandle(nil, context.Background()) != nil {
t.Fatal("cleanHandle(nil) is not nil")
}
db, _ := dedicatedDB(t, "lagoon_after_commit_edges")
gdb, err := Use(t.Context(), db)
if err != nil {
t.Fatal(err)
}
var got context.Context
if err := Transaction(nil, gdb, func(ctx context.Context, tx *gorm.DB) error {
AfterCommit(ctx, tx, func(ctx context.Context, _ *gorm.DB) { got = ctx })
return nil
}); err != nil {
t.Fatal(err)
}
if got == nil {
t.Fatal("callback of a Transaction with a nil ctx did not run with a context")
}
if err := registerAfterCommit(gdb); err != nil {
t.Fatalf("registering the after-commit callback twice: %v", err)
}
}