test(03-04): cover framework database, routing and JWT boundaries

- Shared pgx/GORM pool, ICU locale fail, and isolated plugin migrations
- Seven-stage middleware order, missing-guard boot failure, typed 404s
- Adversarial JWT matrix including alg:none, empty secret, and no leak

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jakub Zych
2026-09-17 20:29:46 +02:00
parent f4fecc959f
commit 92255a46ed
10 changed files with 989 additions and 11 deletions

162
lagoon/connection_test.go Normal file
View File

@@ -0,0 +1,162 @@
package lagoon
import (
"database/sql"
"os"
"path/filepath"
"strings"
"testing"
"git.golem15.com/golem15/summercms/backpack"
"git.golem15.com/golem15/summercms/compass"
"gorm.io/gorm"
)
func TestUseRejectsNilSQL(t *testing.T) {
_, err := Use(t.Context(), nil)
if err == nil || !strings.Contains(err.Error(), "sql db is nil") {
t.Fatalf("got %v", err)
}
}
func TestCheckLocaleRejectsNilSQL(t *testing.T) {
err := CheckLocale(t.Context(), nil)
if err == nil || !strings.Contains(err.Error(), "sql db is nil") {
t.Fatalf("got %v", err)
}
}
func TestPublishRejectsNilHandles(t *testing.T) {
app := backpack.New(nil)
if err := Publish(nil, nil, nil); err == nil {
t.Fatal("want nil app error")
}
if err := Publish(app, nil, &gorm.DB{}); err == nil || !strings.Contains(err.Error(), "nil") {
t.Fatalf("got %v", err)
}
}
func TestDSNEmptyWithoutConfig(t *testing.T) {
if DSN(nil) != "" {
t.Fatal("nil config must yield empty DSN")
}
}
func TestOpenFromAppMissingConfig(t *testing.T) {
_, _, err := OpenFromApp(t.Context(), nil)
if err == nil || !strings.Contains(err.Error(), "app config is missing") {
t.Fatalf("got %v", err)
}
_, _, err = OpenFromApp(t.Context(), backpack.New(nil))
if err == nil || !strings.Contains(err.Error(), "app config is missing") {
t.Fatalf("got %v", err)
}
}
func TestSharedSQLPoolUsedByGORMAndClosed(t *testing.T) {
_, dsn := dedicatedDB(t, "lagoon_shared")
sqlDB, gdb, err := Open(t.Context(), dsn)
if err != nil {
t.Fatal(err)
}
raw, err := gdb.DB()
if err != nil {
t.Fatal(err)
}
if raw != sqlDB {
t.Fatal("GORM is not using the shared *sql.DB")
}
app := backpack.New(nil)
if err := Publish(app, sqlDB, gdb); err != nil {
t.Fatal(err)
}
gotSQL, ok := app.Lookup[*sql.DB]()
if !ok || gotSQL != sqlDB {
t.Fatal("published *sql.DB is not the shared pool")
}
gotGORM, ok := app.Lookup[*gorm.DB]()
if !ok || gotGORM != gdb {
t.Fatal("published *gorm.DB is missing")
}
viaUse, err := Use(t.Context(), sqlDB)
if err != nil {
t.Fatal(err)
}
useRaw, err := viaUse.DB()
if err != nil {
t.Fatal(err)
}
if useRaw != sqlDB {
t.Fatal("Use must open GORM on the caller pool, not a second connection")
}
if err := sqlDB.Close(); err != nil {
t.Fatal(err)
}
if err := sqlDB.PingContext(t.Context()); err == nil {
t.Fatal("closed pool must reject Ping")
}
if _, err := Use(t.Context(), sqlDB); err == nil {
t.Fatal("Use must fail after the shared pool is closed")
}
}
func TestWrongICULocaleFailsOpen(t *testing.T) {
admin := lagoonDB(t)
ctx := t.Context()
if _, err := admin.ExecContext(ctx, `CREATE DATABASE lagoon_locale_fail TEMPLATE template0 ENCODING 'UTF8' LOCALE_PROVIDER libc LOCALE 'C'`); err != nil && !strings.Contains(err.Error(), "already exists") {
t.Fatalf("create libc database: %v", err)
}
t.Cleanup(func() {
_, _ = admin.ExecContext(ctx, `DROP DATABASE IF EXISTS lagoon_locale_fail WITH (FORCE)`)
})
failDSN, err := dsnWithDB(lagoonDSN, "lagoon_locale_fail")
if err != nil {
t.Fatal(err)
}
_, _, err = Open(ctx, failDSN)
if err == nil {
t.Fatal("wrong locale must fail boot")
}
msg := err.Error()
for _, want := range []string{"ICU", "pl-PL", "CREATE DATABASE", "LOCALE_PROVIDER icu"} {
if !strings.Contains(msg, want) {
t.Fatalf("missing %q in %s", want, msg)
}
}
}
func TestOpenFromAppReadsDSN(t *testing.T) {
_, dsn := dedicatedDB(t, "lagoon_from_app")
dir := t.TempDir()
if err := os.WriteFile(filepath.Join(dir, "app.yaml"), []byte("name: lagoon-test\n"), 0o644); err != nil {
t.Fatal(err)
}
cfg, err := compass.Open(compass.Options{
Dir: dir,
Environ: []string{
"SUMMER_ENV=development",
"SUMMER_DATABASE__DSN=" + dsn,
},
})
if err != nil {
t.Fatal(err)
}
if got := DSN(cfg); got != dsn {
t.Fatalf("DSN = %q want %q", got, dsn)
}
sqlDB, gdb, err := OpenFromApp(t.Context(), backpack.New(cfg))
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = sqlDB.Close() })
raw, err := gdb.DB()
if err != nil {
t.Fatal(err)
}
if raw != sqlDB {
t.Fatal("OpenFromApp must share the same *sql.DB with GORM")
}
}

View File

@@ -10,7 +10,9 @@ import (
"git.golem15.com/golem15/summercms/backpack"
"git.golem15.com/golem15/summercms/bonfire"
"git.golem15.com/golem15/summercms/pact"
"git.golem15.com/golem15/summercms/party"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
@@ -123,3 +125,139 @@ func TestRollbackMissingPluginIsNamedError(t *testing.T) {
t.Fatalf("got %v", err)
}
}
type migPlugin struct {
id string
migrations []*gormigrate.Migration
}
func (p migPlugin) ID() string { return p.id }
func (p migPlugin) Requires() []string { return nil }
func (p migPlugin) Register(*backpack.App) error { return nil }
func (p migPlugin) Boot(*backpack.App) error { return nil }
func (p migPlugin) Migrations() []*gormigrate.Migration { return p.migrations }
func TestTwoPluginMigrationSetsIsolated(t *testing.T) {
db, _ := dedicatedDB(t, "lagoon_mig_iso")
gdb, err := Use(t.Context(), db)
if err != nil {
t.Fatal(err)
}
alpha := migPlugin{
id: "demo.alpha",
migrations: []*gormigrate.Migration{{
ID: "202609170001_create_alpha",
Migrate: func(tx *gorm.DB) error {
return tx.Exec(`CREATE TABLE lagoon_alpha (id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL)`).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Exec(`DROP TABLE IF EXISTS lagoon_alpha`).Error
},
}},
}
beta := migPlugin{
id: "demo.beta",
migrations: []*gormigrate.Migration{
{
ID: "202609170001_create_beta",
Migrate: func(tx *gorm.DB) error {
return tx.Exec(`CREATE TABLE lagoon_beta (id BIGSERIAL PRIMARY KEY, name TEXT NOT NULL)`).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Exec(`DROP TABLE IF EXISTS lagoon_beta`).Error
},
},
{
ID: "202609170002_seed_beta",
Migrate: func(tx *gorm.DB) error {
return tx.Exec(`INSERT INTO lagoon_beta (name) VALUES ('seed')`).Error
},
Rollback: func(tx *gorm.DB) error {
return tx.Exec(`DELETE FROM lagoon_beta WHERE name = 'seed'`).Error
},
},
},
}
plugins := []party.Plugin{alpha, beta}
if err := Migrate(gdb, plugins); err != nil {
t.Fatal(err)
}
if err := Migrate(gdb, plugins); err != nil {
t.Fatalf("idempotent migrate: %v", err)
}
status, err := Status(gdb, plugins)
if err != nil {
t.Fatal(err)
}
if len(status) != 2 {
t.Fatalf("status rows = %+v", status)
}
if status[0].Plugin != "demo.alpha" || status[0].Table != "summer_migrations_demo_alpha" || strings.Join(status[0].IDs, ",") != "202609170001_create_alpha" {
t.Fatalf("alpha status = %+v", status[0])
}
if status[1].Plugin != "demo.beta" || status[1].Table != "summer_migrations_demo_beta" || strings.Join(status[1].IDs, ",") != "202609170001_create_beta,202609170002_seed_beta" {
t.Fatalf("beta status = %+v", status[1])
}
if !gdb.Migrator().HasTable("lagoon_alpha") || !gdb.Migrator().HasTable("lagoon_beta") {
t.Fatal("both plugin tables must exist after migrate")
}
var seedName string
if err := gdb.Raw(`SELECT name FROM lagoon_beta`).Scan(&seedName).Error; err != nil {
t.Fatal(err)
}
if seedName != "seed" {
t.Fatalf("seed = %q", seedName)
}
if err := RollbackLast(gdb, plugins, "demo.beta"); err != nil {
t.Fatal(err)
}
var n int64
if err := gdb.Table("lagoon_beta").Count(&n).Error; err != nil {
t.Fatal(err)
}
if n != 0 {
t.Fatalf("beta seed rollback left %d rows", n)
}
if !gdb.Migrator().HasTable("lagoon_alpha") || !gdb.Migrator().HasTable("lagoon_beta") {
t.Fatal("schema rollback must not run on the first beta RollbackLast")
}
status, err = Status(gdb, plugins)
if err != nil {
t.Fatal(err)
}
if strings.Join(status[0].IDs, ",") != "202609170001_create_alpha" {
t.Fatalf("alpha history changed: %+v", status[0])
}
if strings.Join(status[1].IDs, ",") != "202609170001_create_beta" {
t.Fatalf("beta history = %+v", status[1])
}
if err := RollbackLast(gdb, plugins, "demo.beta"); err != nil {
t.Fatal(err)
}
if gdb.Migrator().HasTable("lagoon_beta") {
t.Fatal("second beta rollback must drop lagoon_beta")
}
if !gdb.Migrator().HasTable("lagoon_alpha") {
t.Fatal("alpha table must survive beta rollback")
}
status, err = Status(gdb, plugins)
if err != nil {
t.Fatal(err)
}
if len(status[0].IDs) != 1 {
t.Fatalf("alpha history after beta down = %+v", status[0])
}
if len(status[1].IDs) != 0 {
t.Fatalf("beta history should be empty, got %+v", status[1])
}
}
var _ pact.HasMigrations = migPlugin{}

View File

@@ -36,4 +36,7 @@ func TestOrderClauseAllowList(t *testing.T) {
if _, err := OrderBy(nil, "items.title", "asc", allowed); err == nil {
t.Fatal("want nil db error")
}
if _, err := orderClause("items.title", "asc", nil); err == nil {
t.Fatal("empty allow-list must reject")
}
}

139
lagoon/postgres_test.go Normal file
View File

@@ -0,0 +1,139 @@
package lagoon
import (
"context"
"database/sql"
"fmt"
"net/url"
"os"
"strings"
"testing"
"time"
_ "github.com/jackc/pgx/v5/stdlib"
"github.com/testcontainers/testcontainers-go"
"github.com/testcontainers/testcontainers-go/modules/postgres"
)
var (
lagoonPG *postgres.PostgresContainer
lagoonSQL *sql.DB
lagoonDSN string
lagoonPGErr error
)
func TestMain(m *testing.M) {
code := 1
if !testShort() {
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute)
lagoonPGErr = startLagoonPostgres(ctx)
cancel()
if lagoonPGErr != nil {
fmt.Fprintf(os.Stderr, "lagoon: testcontainers postgres: %v\n", lagoonPGErr)
stopLagoonPostgres()
os.Exit(1)
}
}
code = m.Run()
stopLagoonPostgres()
os.Exit(code)
}
func testShort() bool {
for _, a := range os.Args {
if a == "-test.short" {
return true
}
}
return false
}
func startLagoonPostgres(ctx context.Context) error {
ctr, err := postgres.Run(ctx,
"postgres:16-alpine",
postgres.WithDatabase("lagoon"),
postgres.WithUsername("lagoon"),
postgres.WithPassword("lagoon"),
postgres.BasicWaitStrategies(),
testcontainers.WithEnv(map[string]string{
"POSTGRES_INITDB_ARGS": "--locale-provider=icu --icu-locale=pl-PL --encoding=UTF8",
}),
)
if err != nil {
return err
}
lagoonPG = ctr
dsn, err := ctr.ConnectionString(ctx, "sslmode=disable")
if err != nil {
return err
}
db, err := sql.Open("pgx", dsn)
if err != nil {
return err
}
if err := db.PingContext(ctx); err != nil {
_ = db.Close()
return err
}
lagoonSQL = db
lagoonDSN = dsn
return nil
}
func stopLagoonPostgres() {
if lagoonSQL != nil {
_ = lagoonSQL.Close()
}
if lagoonPG != nil {
_ = testcontainers.TerminateContainer(lagoonPG)
}
}
func lagoonDB(t *testing.T) *sql.DB {
t.Helper()
if testing.Short() {
t.Skip("requires testcontainers postgres")
}
if lagoonPGErr != nil {
t.Fatalf("postgres unavailable: %v", lagoonPGErr)
}
if lagoonSQL == nil {
t.Fatal("postgres unavailable: container was not started")
}
return lagoonSQL
}
func dedicatedDB(t *testing.T, name string) (*sql.DB, string) {
t.Helper()
admin := lagoonDB(t)
ctx := t.Context()
quoted := `"` + strings.ReplaceAll(name, `"`, `""`) + `"`
if _, err := admin.ExecContext(ctx, `CREATE DATABASE `+quoted+` TEMPLATE template0 ENCODING 'UTF8' LOCALE_PROVIDER icu ICU_LOCALE 'pl-PL'`); err != nil && !strings.Contains(err.Error(), "already exists") {
t.Fatalf("create %s: %v", name, err)
}
dsn, err := dsnWithDB(lagoonDSN, name)
if err != nil {
t.Fatal(err)
}
db, err := sql.Open("pgx", dsn)
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
_ = db.Close()
_, _ = admin.ExecContext(context.Background(), `DROP DATABASE IF EXISTS `+quoted+` WITH (FORCE)`)
})
if err := db.PingContext(ctx); err != nil {
t.Fatal(err)
}
return db, dsn
}
func dsnWithDB(dsn, name string) (string, error) {
u, err := url.Parse(dsn)
if err != nil {
return "", err
}
u.Path = "/" + name
return u.String(), nil
}