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) } }