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.
111 lines
3.4 KiB
Go
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"
|
|
}
|