Files
cgalo5758 11e0f6ee86 Run the migration tests on scratch databases
dbtest gains UpSQL, ApplyVersion, SimpleProtocol, Tx, On and Q (Exec, Scan,
ID, Int, String, Strings, Attempt, Refuse), WantPgError, Name and the
SQLState constants. internal/db's mustExec, mustScan, mustStrings,
deliveryTx, attempt, mustRefuse, assertPgError, the sqlstate constants and
deliveryName become their dbtest counterparts at every caller; the fx
builders' id scans become Q.ID. The fx builders and the helpers built on
them lose their unused ctx parameter. The two tests that open one
transaction in the parent (TestProviderConnectionConstraints,
TestDeliverySchemaFacts) rebind it in each subtest that runs a statement
with dbtest.On(t, q.Tx()).

TestTaxonomyMigrationBuckets runs on a scratch database through two
connections (simple protocol to apply, extended to assert) instead of a
docker container; its checks are tables of (query, want) in three named
phases, and its gocognit pin is deleted.
TestIntegrationDeliveryMigrationUpDownUp uses Scratch, UpTo and
ApplyVersion; TestInvoiceIssuedPaymentPaidBackfill replays 00022 through
UpSQL on a simple-protocol transaction; the reader tests in internal/migrate
use Tx; TestFullMigrationSequence and the delivery benchmark migrate a
scratch database with migrate.Sources().

The delivery benchmark's skip text is unified with the openers'. No test is
added or renamed. 13 breaks, each failed the old and the new test: the 11 on
the migration subjects, one on a WantPgError caller and one on a Q.Refuse
caller.
2026-10-07 16:22:18 -05:00

111 lines
3.4 KiB
Go

// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
package dbtest
import (
"context"
"database/sql"
"fmt"
"io/fs"
"net/url"
"strings"
"sync/atomic"
"testing"
"time"
"github.com/pressly/goose/v3"
goosedb "github.com/pressly/goose/v3/database"
"git.coopcloud.tech/wiki-cafe/member-console/internal/db"
)
var scratchSeq atomic.Int64
// Scratch creates an empty database in the cluster TEST_DATABASE_URL points
// at, drops it when the test ends, and returns its address. The test stack's
// login role may create databases; a cluster where it cannot skips the test.
func Scratch(tb testing.TB) string {
tb.Helper()
dsn := DSN(tb)
admin := Connect(tb, dsn)
name := fmt.Sprintf("mc_scratch_%d_%d", time.Now().UnixNano(), scratchSeq.Add(1))
if _, err := admin.ExecContext(context.Background(), "CREATE DATABASE "+name); err != nil {
tb.Skipf("cannot create a scratch database: %v", err)
}
tb.Cleanup(func() {
if _, err := admin.ExecContext(context.Background(), "DROP DATABASE IF EXISTS "+name+" WITH (FORCE)"); err != nil {
tb.Errorf("drop scratch database %s: %v", name, err)
}
})
u, err := url.Parse(dsn)
if err != nil {
tb.Fatalf("parse TEST_DATABASE_URL: %v", err)
}
u.Path = "/" + name
return u.String()
}
// UpTo applies the stream's migrations up to version on its own ledger table,
// through a goose provider that shares no state with the process.
func UpTo(tb testing.TB, d *sql.DB, src db.MigrationSource, version int64) {
tb.Helper()
if _, err := provider(tb, d, src).UpTo(context.Background(), version); err != nil {
tb.Fatalf("migrate %s to %d: %v", src.Name, version, err)
}
}
func provider(tb testing.TB, d *sql.DB, src db.MigrationSource) *goose.Provider {
tb.Helper()
fsys, err := fs.Sub(src.Migrations, src.Dir)
if err != nil {
tb.Fatalf("%s migrations: %v", src.Name, err)
}
store, err := goosedb.NewStore(goosedb.DialectPostgres, ledgerTable(src))
if err != nil {
tb.Fatalf("goose store: %v", err)
}
p, err := goose.NewProvider("", d, fsys, goose.WithStore(store), goose.WithDisableGlobalRegistry(true))
if err != nil {
tb.Fatalf("goose provider: %v", err)
}
return p
}
// ApplyVersion applies one migration version of the stream, up or down, on
// the stream's own ledger, as UpTo does.
func ApplyVersion(tb testing.TB, d *sql.DB, src db.MigrationSource, version int64, up bool) {
tb.Helper()
if _, err := provider(tb, d, src).ApplyVersion(context.Background(), version, up); err != nil {
direction := "down"
if up {
direction = "up"
}
tb.Fatalf("apply %s %d %s: %v", src.Name, version, direction, err)
}
}
// UpSQL returns the Up section of one migration file of the stream, everything
// before its "-- +goose Down" marker. The caller runs it on its own executor:
// a transaction that holds locks, or a connection whose error the test checks.
func UpSQL(tb testing.TB, src db.MigrationSource, file string) string {
tb.Helper()
raw, err := fs.ReadFile(src.Migrations, src.Dir+"/"+file)
if err != nil {
tb.Fatalf("read migration %s: %v", file, err)
}
up, _, _ := strings.Cut(string(raw), "-- +goose Down")
return up
}
// SimpleProtocol returns dsn with the simple query protocol, so a whole
// migration file runs as one multi-statement string in an implicit
// transaction.
func SimpleProtocol(dsn string) string {
sep := "?"
if strings.Contains(dsn, "?") {
sep = "&"
}
return dsn + sep + "default_query_exec_mode=simple_protocol"
}