158 lines
4.9 KiB
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)
|
|
}
|
|
}
|