Files
go-omni/database/gormx/integration_test.go

158 lines
4.9 KiB
Go

package gormx_test
import (
"bytes"
"context"
"errors"
"net"
"os"
"strconv"
"strings"
"testing"
"time"
"git.yuntuo.hk/k2safe/go-omni/database/gormx"
adapter "git.yuntuo.hk/k2safe/go-omni/database/mysql"
"git.yuntuo.hk/k2safe/go-omni/logx"
"gorm.io/gorm"
)
type record struct {
ID int64 `gorm:"primaryKey;autoIncrement:false"`
Name string
}
func (record) TableName() string { return "gorm_adapter_records" }
func TestGORMMySQLTransactionsCancellationAndLogs(t *testing.T) {
if os.Getenv("GO_OMNI_CONNECTOR_MYSQL_TEST") != "1" {
t.Skip("use scripts/verifyConnector.py --gorm with isolated MySQL")
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
cfg := adapter.DefaultConfig()
host, port, err := net.SplitHostPort(os.Getenv("GO_OMNI_CONNECTOR_ADDR"))
if err != nil {
t.Fatal(err)
}
cfg.Host = host
cfg.Port, _ = strconv.Atoi(port)
cfg.User = os.Getenv("GO_OMNI_CONNECTOR_USER")
cfg.Password = os.Getenv("GO_OMNI_CONNECTOR_PASSWORD")
cfg.Database = os.Getenv("GO_OMNI_CONNECTOR_DATABASE")
cfg.TLS.CAFile = os.Getenv("GO_OMNI_CONNECTOR_CA_FILE")
cfg.TLS.ServerName = "localhost"
cfg.Pool.MaxOpenConns = 2
cfg.Pool.MaxIdleConns = 2
pool, err := adapter.Open(ctx, cfg)
if err != nil {
t.Fatal(err)
}
defer pool.Close()
var logs bytes.Buffer
logger, err := logx.New(logx.Config{Writer: &logs})
if err != nil {
t.Fatal(err)
}
defer logger.Close()
logConfig := gormx.DefaultLogConfig()
logConfig.Level = "info"
orm, err := gormx.MySQL(ctx, pool, gormx.WithLogger(logger), gormx.WithLogConfig(logConfig), gormx.WithConfig(gorm.Config{PrepareStmt: true, TranslateError: true}))
if err != nil {
t.Fatal(err)
}
sqlDB, err := orm.DB()
if err != nil || sqlDB != pool.DB {
t.Fatal("pool not shared", err)
}
if sqlDB.Stats().MaxOpenConnections != 2 {
t.Fatal("pool limits changed")
}
var tables int
if err = pool.QueryRowContext(ctx, "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'gorm_adapter_records'").Scan(&tables); err != nil || tables != 0 {
t.Fatal("adapter must not migrate schema", err)
}
if _, err = pool.ExecContext(ctx, "CREATE TABLE gorm_adapter_records (id BIGINT PRIMARY KEY, name VARCHAR(100) UNIQUE NOT NULL)"); err != nil {
t.Fatal(err)
}
defer pool.ExecContext(context.Background(), "DROP TABLE gorm_adapter_records")
requestCtx := logx.WithRequestID(ctx, "gorm-request")
orm = orm.WithContext(requestCtx)
secret := "private-bound-name"
if err = orm.Transaction(func(tx *gorm.DB) error { return tx.Create(&record{ID: 1, Name: secret}).Error }); err != nil {
t.Fatal(err)
}
wanted := errors.New("rollback sentinel")
if err = orm.Transaction(func(tx *gorm.DB) error {
if e := tx.Create(&record{ID: 2, Name: "rollback"}).Error; e != nil {
return e
}
return wanted
}); !errors.Is(err, wanted) {
t.Fatal(err)
}
func() {
defer func() {
if recover() != "rollback panic" {
t.Error("panic changed")
}
}()
_ = orm.Transaction(func(tx *gorm.DB) error {
if e := tx.Create(&record{ID: 3, Name: "panic"}).Error; e != nil {
t.Fatal(e)
}
panic("rollback panic")
})
}()
var count int64
if err = orm.Model(&record{}).Count(&count).Error; err != nil || count != 1 {
t.Fatal("commit/rollback differs", count, err)
}
if err = orm.Transaction(func(tx *gorm.DB) error {
if e := tx.Create(&record{ID: 4, Name: "outer"}).Error; e != nil {
return e
}
if e := tx.Transaction(func(inner *gorm.DB) error {
if e := inner.Create(&record{ID: 5, Name: "inner"}).Error; e != nil {
return e
}
return wanted
}); !errors.Is(e, wanted) {
return errors.New("nested rollback failed")
}
return nil
}); err != nil {
t.Fatal(err)
}
if err = orm.Model(&record{}).Count(&count).Error; err != nil || count != 2 {
t.Fatal(count, err)
}
if err = orm.Create(&record{ID: 6, Name: secret}).Error; !errors.Is(err, gorm.ErrDuplicatedKey) {
t.Fatal("native error translation lost", err)
}
var missing record
if err = orm.First(&missing, "id = ?", 999).Error; !errors.Is(err, gorm.ErrRecordNotFound) {
t.Fatal("not-found semantics changed", err)
}
deadline, stop := context.WithTimeout(requestCtx, 30*time.Millisecond)
start := time.Now()
err = orm.WithContext(deadline).Exec("SELECT SLEEP(3)").Error
stop()
if !errors.Is(err, context.DeadlineExceeded) || time.Since(start) > time.Second {
t.Fatal("query cancellation lost", err)
}
if err = pool.Health(ctx); err != nil {
t.Fatal("pool did not recover", err)
}
if strings.Contains(logs.String(), secret) || !strings.Contains(logs.String(), `"requestId":"gorm-request"`) || !strings.Contains(logs.String(), "gorm.queryFailed") {
t.Fatal("SQL log privacy/correlation differs")
}
var cipher struct {
VariableName string
Value string
}
if err = orm.Raw("SHOW SESSION STATUS LIKE 'Ssl_cipher'").Scan(&cipher).Error; err != nil || cipher.Value == "" {
t.Fatal("GORM bypassed TLS pool", err)
}
}