107 lines
4.0 KiB
Cheetah
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") }
|
|
}
|