Merge branch 'main' into feat/huma-api-docs

This commit is contained in:
Kyle Mendell
2026-07-12 12:48:50 -05:00
committed by GitHub
44 changed files with 705 additions and 274 deletions
+16 -12
View File
@@ -1,3 +1,5 @@
//go:build unit
// This file is only imported by unit tests
package testing
@@ -18,25 +20,27 @@ import (
const testActorHostPSK = "pocket-id-test-actor-host-psk-32bytes"
// NewActorHostForTest starts a single-host Francis cluster backed by the in-memory provider, runs it, and waits until it is ready to serve invocations
// Pass built-in actors with local.WithBuiltInActor; the host is stopped when the test ends
// The register callback, if not nil, runs after the host is created but before it starts, so callers can register actors with host.RegisterActor/host.RegisterBuiltInActor (must be called before the host is running)
// The host is stopped when the test ends
// The in-memory provider keeps no state on disk, so the test never touches a real database
func NewActorHostForTest(t *testing.T, opts ...local.HostOption) *local.Host {
func NewActorHostForTest(t *testing.T, register func(t *testing.T, h *local.Host)) *local.Host {
t.Helper()
// The defaults come first so callers can register their built-in actors (and override any default) through opts
hostOpts := append(
[]local.HostOption{
local.WithAddress(freeLoopbackAddr(t)),
local.WithRuntimePSKs([]byte(testActorHostPSK)),
local.WithStandaloneMemoryProvider(standalone.StandaloneMemoryOptions{}),
local.WithShutdownGracePeriod(time.Second),
},
opts...,
)
hostOpts := []local.HostOption{
local.WithAddress(freeLoopbackAddr(t)),
local.WithRuntimePSKs([]byte(testActorHostPSK)),
local.WithStandaloneMemoryProvider(standalone.StandaloneMemoryOptions{}),
local.WithShutdownGracePeriod(time.Second),
}
h, err := local.NewHost(hostOpts...)
require.NoError(t, err)
// Register built-in actors before the host starts
if register != nil {
register(t, h)
}
// Run the host in the background and stop it when the test ends
ctx, cancel := context.WithCancel(context.Background())
errCh := make(chan error, 1)
+91 -17
View File
@@ -1,8 +1,13 @@
//go:build unit
// This file is only imported by unit tests
package testing
import (
"errors"
"log/slog"
"path/filepath"
"testing"
"time"
@@ -11,6 +16,7 @@ import (
"github.com/golang-migrate/migrate/v4"
sqliteMigrate "github.com/golang-migrate/migrate/v4/database/sqlite3"
"github.com/golang-migrate/migrate/v4/source/iofs"
sqlitekit "github.com/italypaleale/go-sql-utils/sqlite"
"github.com/libtnb/sqlite"
"github.com/stretchr/testify/require"
"gorm.io/gorm"
@@ -28,28 +34,43 @@ func init() {
// NewDatabaseForTest returns a new instance of GORM connected to an in-memory SQLite database.
// Each database connection is unique for the test.
// All migrations are automatically performed.
// Note: the in-memory database is limited to a single connection, so it cannot be used to test concurrent access: use NewConcurrentDatabaseForTest for that.
func NewDatabaseForTest(t *testing.T) *gorm.DB {
t.Helper()
db := openInMemoryTestDB(t)
runMigrations(t, db, 0, nil)
return db
}
// NewDatabaseForTestWithMigrationSeed behaves like NewDatabaseForTest, but pauses the migrations right after the migration whose version is stopAfterVersion has been applied.
// It then invokes seed, so the test can insert data into the intermediate schema, before applying the remaining migrations.
// This is meant to test data migrations, which operate on data that already exists in the database.
func NewDatabaseForTestWithMigrationSeed(t *testing.T, stopAfterVersion uint, seed func(t *testing.T, db *gorm.DB)) *gorm.DB {
t.Helper()
db := openInMemoryTestDB(t)
runMigrations(t, db, stopAfterVersion, seed)
return db
}
// NewConcurrentDatabaseForTest returns a new instance of GORM connected to a temporary file-based SQLite database
// Unlike NewDatabaseForTest, which forces a single connection to an in-memory database, this one can be used to test concurrent access to the database.
// All migrations are automatically performed.
func NewConcurrentDatabaseForTest(t *testing.T) *gorm.DB {
t.Helper()
db := openFileTestDB(t)
runMigrations(t, db, 0, nil)
return db
}
// openInMemoryTestDB opens a GORM instance backed by an in-memory SQLite database, unique to the test.
func openInMemoryTestDB(t *testing.T) *gorm.DB {
t.Helper()
// Get a name for this in-memory database that is specific to the test
dbName := utils.CreateSha256Hash(t.Name())
// Connect to a new in-memory SQL database
db, err := gorm.Open(
sqlite.Open("file:"+dbName+"?mode=memory"),
&gorm.Config{
TranslateError: true,
Logger: logger.New(
testLoggerAdapter{t: t},
logger.Config{
SlowThreshold: 200 * time.Millisecond,
LogLevel: logger.Info,
IgnoreRecordNotFoundError: false,
ParameterizedQueries: false,
Colorful: false,
},
),
})
db, err := gorm.Open(sqlite.Open("file:"+dbName+"?mode=memory"), newTestGormConfig(t))
require.NoError(t, err, "Failed to connect to test database")
sqlDB, err := db.DB()
@@ -59,6 +80,31 @@ func NewDatabaseForTest(t *testing.T) *gorm.DB {
// The other workaround, of using shared caches, doesn't work well with multiple write transactions trying to happen at once
sqlDB.SetMaxOpenConns(1)
return db
}
// openFileTestDB opens a GORM instance backed by a temporary file-based SQLite database, configured like the production database
func openFileTestDB(t *testing.T) *gorm.DB {
t.Helper()
dbPath := filepath.Join(t.TempDir(), "test.db")
connString, _, _, err := sqlitekit.ParseConnectionString(dbPath, slog.Default())
require.NoError(t, err, "Failed to parse connection string")
db, err := gorm.Open(sqlite.Open(connString), newTestGormConfig(t))
require.NoError(t, err, "Failed to connect to test database")
return db
}
// runMigrations applies the embedded SQLite migrations to db.
// If seed is not nil, migrations are first applied up to and including stopAfterVersion, then seed is invoked so the test can insert data into the intermediate schema, before the remaining migrations are applied.
func runMigrations(t *testing.T, db *gorm.DB, stopAfterVersion uint, seed func(t *testing.T, db *gorm.DB)) {
t.Helper()
sqlDB, err := db.DB()
require.NoError(t, err, "Failed to get sql.DB")
// Perform migrations with the embedded migrations
driver, err := sqliteMigrate.WithInstance(sqlDB, &sqliteMigrate.Config{
NoTxWrap: true,
@@ -68,12 +114,40 @@ func NewDatabaseForTest(t *testing.T) *gorm.DB {
require.NoError(t, err, "Failed to create embedded migration source")
m, err := migrate.NewWithInstance("iofs", source, "pocket-id", driver)
require.NoError(t, err, "Failed to create migration instance")
// If the test wants to seed data partway through the migrations, apply migrations up to and including stopAfterVersion first, then let it seed
if seed != nil {
err = m.Migrate(stopAfterVersion)
require.NoErrorf(t, err, "Failed to perform migrations up to version %d", stopAfterVersion)
seed(t, db)
}
// Apply all the remaining migrations
// ErrNoChange means we were already at the latest version, which is not an error here
err = m.Up()
require.NoError(t, err, "Failed to perform migrations")
if !errors.Is(err, migrate.ErrNoChange) {
require.NoError(t, err, "Failed to perform migrations")
}
_, err = sqlDB.Exec("PRAGMA foreign_keys = OFF;")
require.NoError(t, err, "Failed to disable foreign keys")
}
return db
// newTestGormConfig returns the GORM configuration shared by the test databases, wiring the logger to the test's output.
func newTestGormConfig(t *testing.T) *gorm.Config {
t.Helper()
return &gorm.Config{
TranslateError: true,
Logger: logger.New(
testLoggerAdapter{t: t},
logger.Config{
SlowThreshold: 200 * time.Millisecond,
LogLevel: logger.Info,
IgnoreRecordNotFoundError: false,
ParameterizedQueries: false,
Colorful: false,
},
),
}
}
// Implements gorm's logger.Writer interface
@@ -1,3 +1,5 @@
//go:build unit
// This file is only imported by unit tests
package testing