Files
go-omni/cmd/go-omni/scaffold/templates/databaseIntegration_test.go.tmpl

107 lines
4.0 KiB
Cheetah

package servicecontext_test
import (
"context"
"database/sql"
"errors"
"fmt"
"io"
"os"
"testing"
"time"
"git.yuntuo.hk/k2safe/go-omni/database"
"git.yuntuo.hk/k2safe/go-omni/logx"
"{{.Module}}/internal/model"
"{{.Module}}/internal/repository"
"{{.Module}}/internal/servicecontext"
)
// These tests require a dedicated database with schema.sql applied. Ordinary
// go test runs skip them; scripts/verifyDatabase.py provisions an isolated DB.
func databaseContext(t *testing.T) *servicecontext.Context {
t.Helper()
dsn := os.Getenv("GO_OMNI_TEST_MYSQL_DSN")
if dsn == "" { t.Skip("set GO_OMNI_TEST_MYSQL_DSN to a dedicated test database") }
for _, key := range []string{"MAX_OPEN_CONNS", "MAX_IDLE_CONNS", "CONN_MAX_LIFETIME", "CONN_MAX_IDLE_TIME", "PING_TIMEOUT"} {
t.Setenv("GO_OMNI_DB_"+key, "")
}
t.Setenv("GO_OMNI_DB_DSN", dsn)
t.Setenv("GO_OMNI_DB_DRIVER", "mysql")
logger, err := logx.New(logx.Config{Writer: io.Discard})
if err != nil { t.Fatal(err) }
t.Cleanup(func() { _ = logger.Close() })
services, err := servicecontext.New(context.Background(), logger)
if err != nil { t.Fatal(err) }
t.Cleanup(func() { _ = services.Close() })
return services
}
func TestDatabaseTransactions(t *testing.T) {
services := databaseContext(t)
ctx := context.Background()
for _, mode := range []string{"commit", "error", "panic", "cancel"} {
t.Run(mode, func(t *testing.T) {
email := fmt.Sprintf("transaction-%d-%s@example.com", time.Now().UnixNano(), mode)
t.Cleanup(func() { _, _ = services.DB.ExecContext(ctx, "DELETE FROM users WHERE email = ?", email) })
txCtx, cancel := context.WithCancel(ctx)
defer cancel()
abort := errors.New("abort business operation")
var err error
var recovered any
func() {
defer func() { recovered = recover() }()
err = services.DB.InTx(txCtx, nil, func(tx *sql.Tx) error {
users := repository.NewMySQLUsers(tx)
if _, err := users.Create(txCtx, model.UserInput{Name: "transaction", Email: email}); err != nil { return err }
switch mode {
case "error": return abort
case "panic": panic(abort)
case "cancel": cancel(); return txCtx.Err()
}
return nil
})
}()
switch mode {
case "commit": if err != nil || recovered != nil { t.Fatalf("commit: error=%v panic=%v", err, recovered) }
case "error": if !errors.Is(err, abort) { t.Fatalf("lost business error: %v", err) }
case "panic": if recovered != abort { t.Fatalf("lost panic: %v", recovered) }
case "cancel": if !errors.Is(err, context.Canceled) { t.Fatalf("lost cancellation: %v", err) }
}
var count int
if err := services.DB.QueryRowContext(ctx, "SELECT COUNT(*) FROM users WHERE email = ?", email).Scan(&count); err != nil { t.Fatal(err) }
want := 0
if mode == "commit" { want = 1 }
if count != want { t.Fatalf("%s persisted %d rows, want %d", mode, count, want) }
})
}
}
func TestDatabasePoolHealthAndClose(t *testing.T) {
services := databaseContext(t)
ctx := context.Background()
if result := services.Health.Check(ctx); !result.Ready() || len(result.Checks) != 1 { t.Fatalf("missing database readiness: %+v", result) }
cfg := services.DB.Config()
cfg.MaxOpenConns, cfg.MaxIdleConns = 1, 0
pool, err := database.Open(ctx, cfg)
if err != nil { t.Fatal(err) }
if idle := pool.Stats().Idle; idle != 0 { t.Fatalf("zero idle config retained %d connections", idle) }
_ = pool.Close()
cfg.MaxIdleConns, cfg.PingTimeout = 1, 50*time.Millisecond
pool, err = database.Open(ctx, cfg)
if err != nil { t.Fatal(err) }
defer pool.Close()
connection, err := pool.Conn(ctx)
if err != nil { t.Fatal(err) }
defer connection.Close()
started := time.Now()
err = pool.Health(ctx)
if !errors.Is(err, context.DeadlineExceeded) || time.Since(started) > time.Second { t.Fatalf("health did not bound pool waiting: %v", err) }
if err := services.Close(); err != nil { t.Fatal(err) }
if err := services.Close(); err != nil { t.Fatal(err) }
if result := services.Health.Check(ctx); result.Ready() { t.Fatal("closed database was still ready") }
}