Files
cgalo5758 d1ac278e6b Open every test database through dbtest
internal/testkit/dbtest opens TEST_DATABASE_URL and E2E_DATABASE_URL, never
migrates, and fails with "the test database is behind; run
test/reset-test-db.sh" when the core ledger lacks the highest version of
db.BaseSources(). The opener helpers except the scratch-database and
container openers (26 of the 29) and the inline openers of the database
tests call it; helpers that did more than open (providers, fake farms, the
FedWiki provider row, cleanup of override rows) keep that setup. The
stripe/workflows and workflows/entitlements openers keep their search_path
through WithSearchPath. The closed-database seam tests use Closed.

TestMigration18SetsExistingFedwikiRulesToForceReduce runs on a scratch
database through Scratch and UpTo instead of rolling the shared core ledger
back through goose's global state, because no opener migrates the shared
database any more.

The in-package forms tests of internal/server reached the integrations'
forms only because an external test file imported internal/migrate; a test
file now links internal/integrations explicitly.

TestRegisterProviders_DB is re-pinned from gocognit 37 to 34.

New tests: TestLedgerCheckRefusesAnEmptyDatabaseAndAcceptsTheTestDatabase
(dbtest), TestOpenRefusesAnEmptyDatabaseAndAcceptsTheTestDatabase (dbtest,
through opened, the path Open and OpenE2E share),
TestKitPackagesReachOnlyTheirAllowedModulePackages (testkit). The skip
messages of the openers are unified to "TEST_DATABASE_URL not set, skipping
integration test". 4 breaks: Open skipping its ledger check and the ledger
check accepting an empty database fail the new tests; a kit row that no
longer allows internal/testkit fails the import guard; leaving the fedwiki
rules as they were in migration 00018 fails the migration 18 test on both
sides.
2026-10-07 16:22:18 -05:00

163 lines
4.8 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
// Package dbtest opens the databases the tests use and prepares scratch
// databases for the tests whose subject is a migration. It imports only
// internal/db and internal/testkit, so every package's tests can import it
// without a cycle (internal/migrate reaches nearly every module package).
//
// Open never migrates: test/reset-test-db.sh is the only migrator of the
// shared test database, and Open fails when the reset has not run.
package dbtest
import (
"context"
"database/sql"
"fmt"
"io/fs"
"os"
"regexp"
"strconv"
"strings"
"testing"
_ "github.com/jackc/pgx/v5/stdlib"
"git.coopcloud.tech/wiki-cafe/member-console/internal/db"
"git.coopcloud.tech/wiki-cafe/member-console/internal/testkit"
)
// behind is what a test sees when the shared database is not at the schema
// the code under test expects.
const behind = "the test database is behind; run test/reset-test-db.sh"
// Option changes how Open reaches the database.
type Option func(*opener)
type opener struct{ searchPath []string }
// WithSearchPath appends search_path=<schemas> to the address when it names
// none, as the app's own connection does.
func WithSearchPath(schemas ...string) Option {
return func(o *opener) { o.searchPath = schemas }
}
// DSN returns TEST_DATABASE_URL and skips the test when it is unset.
func DSN(tb testing.TB) string {
tb.Helper()
dsn := os.Getenv("TEST_DATABASE_URL")
if dsn == "" {
tb.Skip("TEST_DATABASE_URL not set, skipping integration test")
}
return dsn
}
// Open opens TEST_DATABASE_URL, which test/reset-test-db.sh has migrated, and
// closes it when the test ends. It skips the test when the variable is unset
// and fails it when the core ledger lacks the highest version of
// db.BaseSources(). It never migrates.
func Open(tb testing.TB, opts ...Option) *sql.DB {
tb.Helper()
var o opener
for _, opt := range opts {
opt(&o)
}
dsn := DSN(tb)
if len(o.searchPath) > 0 && !strings.Contains(dsn, "search_path") {
dsn += "&search_path=" + strings.Join(o.searchPath, ",")
}
return opened(tb, dsn)
}
// OpenE2E opens E2E_DATABASE_URL, the database test/e2e/plan-management
// commits its fixtures to, as Open opens the test database.
func OpenE2E(tb testing.TB) *sql.DB {
tb.Helper()
dsn := os.Getenv("E2E_DATABASE_URL")
if dsn == "" {
tb.Skip("E2E_DATABASE_URL not set, skipping e2e test")
}
return opened(tb, dsn)
}
// opened connects to dsn and fails the test unless the database is migrated.
func opened(tb testing.TB, dsn string) *sql.DB {
tb.Helper()
database := Connect(tb, dsn)
if err := ledgerBehind(database); err != nil {
tb.Fatalf("%v", err)
}
return database
}
// Connect opens dsn with the pgx driver and closes it when the test ends.
func Connect(tb testing.TB, dsn string) *sql.DB {
tb.Helper()
database, err := sql.Open("pgx", dsn)
if err != nil {
tb.Fatalf("open database: %v", err)
}
tb.Cleanup(func() { testkit.Close(tb, database) })
return database
}
// Closed returns a database that is already closed, so every call on it fails
// with sql.ErrConnDone. It reaches no server.
func Closed(tb testing.TB) *sql.DB {
tb.Helper()
database, err := sql.Open("pgx", "postgres://closed-database-seam/testdb")
if err != nil {
tb.Fatalf("open database: %v", err)
}
if err := database.Close(); err != nil {
tb.Fatalf("close database: %v", err)
}
return database
}
var versionPrefix = regexp.MustCompile(`^(\d+)_`)
// ledgerTable is the goose version table of one migration stream.
func ledgerTable(src db.MigrationSource) string { return "goose_db_version_" + src.Name }
// highestVersion is the highest version among the stream's migration files.
func highestVersion(src db.MigrationSource) (int64, error) {
entries, err := fs.ReadDir(src.Migrations, src.Dir)
if err != nil {
return 0, fmt.Errorf("read %s migrations: %w", src.Name, err)
}
var highest int64
for _, e := range entries {
m := versionPrefix.FindStringSubmatch(e.Name())
if m == nil {
continue
}
v, err := strconv.ParseInt(m[1], 10, 64)
if err != nil {
return 0, fmt.Errorf("migration %s: %w", e.Name(), err)
}
highest = max(highest, v)
}
return highest, nil
}
// ledgerBehind returns an error naming the reset script when the core ledger
// of database lacks the highest version in db.BaseSources().
func ledgerBehind(database *sql.DB) error {
core := db.BaseSources()[0]
want, err := highestVersion(core)
if err != nil {
return err
}
var applied sql.NullInt64
err = database.QueryRowContext(context.Background(),
`SELECT max(version_id) FROM `+ledgerTable(core)+` WHERE is_applied`).Scan(&applied)
if err != nil {
return fmt.Errorf("%s (core ledger: %v)", behind, err)
}
if applied.Int64 < want {
return fmt.Errorf("%s (core is at version %d, want %d)", behind, applied.Int64, want)
}
return nil
}