fix:fix postgres bug

#855
This commit is contained in:
samwaf
2026-07-16 15:45:54 +08:00
parent 7c0d4e0fb0
commit 572ee22eec
10 changed files with 339 additions and 13 deletions
+324
View File
@@ -0,0 +1,324 @@
//go:build crossdb
// 跨三种数据库(SQLite / MySQL / PostgreSQL)兼容回归测试。
//
// 目的:验证「修改」类接口的 Updates(map) 列名在三种引擎下都能落库。
// PostgreSQL 对加引号的标识符大小写敏感,历史上写错大小写的 map key
// (如 "Host_Code")在 SQLite/MySQL 上碰巧能跑、在 PG 上会报 42703。
//
// 运行(三种库都开着时):
//
// go test -tags crossdb ./service/waf_service/ -run CrossEngine -v
//
// 连接可用环境变量覆盖:
//
// SAMWAF_TEST_MYSQL_DSN="root:canteen1@tcp(127.0.0.1:3306)/"
// SAMWAF_TEST_PG_DSN="postgres://postgres:postgres@127.0.0.1:5432/"
//
// 未就绪的引擎会被自动 Skip,不影响其它引擎。
package waf_service
import (
"SamWaf/common/uuid"
"SamWaf/customtype"
"SamWaf/global"
"SamWaf/model"
"SamWaf/model/baseorm"
"SamWaf/model/request"
"SamWaf/wafdb/dialect"
"fmt"
"net/url"
"os"
"path/filepath"
"testing"
"time"
sqlitedriver "github.com/samwafgo/sqlitedriver"
mysqldriver "gorm.io/driver/mysql"
pgdriver "gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
const (
xtestDBName = "samwaf_xtest_core"
xtestUser = "xtest_user"
xtestTenant = "xtest_tenant"
)
// 受测的模型,每个引擎测试前 AutoMigrate。
var xtestModels = []interface{}{
&model.IPBlockList{}, &model.IPAllowList{},
&model.URLBlockList{}, &model.URLAllowList{},
&model.AntiCC{}, &model.LDPUrl{}, &model.LoadBalance{}, &model.Rules{},
}
type xengine struct {
name string
setup func(t *testing.T) (db *gorm.DB, teardown func())
}
func xengines() []xengine {
return []xengine{
{"sqlite", setupSQLite},
{"mysql", setupMySQL},
{"postgres", setupPostgres},
}
}
func setupSQLite(t *testing.T) (*gorm.DB, func()) {
path := filepath.Join(t.TempDir(), "xtest.db")
dsn := path + "?_db_key=" + url.QueryEscape("xtestkey")
db, err := gorm.Open(sqlitedriver.Open(dsn), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
if err != nil {
t.Logf("sqlite 打开失败: %v", err)
return nil, nil
}
dialect.Register(&dialect.SQLiteDialect{})
if err := db.AutoMigrate(xtestModels...); err != nil {
t.Fatalf("sqlite AutoMigrate: %v", err)
}
return db, func() {
if s, e := db.DB(); e == nil {
s.Close()
}
}
}
func setupMySQL(t *testing.T) (*gorm.DB, func()) {
base := os.Getenv("SAMWAF_TEST_MYSQL_DSN")
if base == "" {
base = "root:canteen1@tcp(127.0.0.1:3306)/"
}
// 维护连接:建库
root, err := gorm.Open(mysqldriver.Open(base+"?parseTime=true"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
if err != nil {
t.Logf("mysql 连接失败(跳过): %v", err)
return nil, nil
}
root.Exec("DROP DATABASE IF EXISTS " + xtestDBName)
if err := root.Exec("CREATE DATABASE " + xtestDBName + " CHARACTER SET utf8mb4").Error; err != nil {
t.Logf("mysql 建库失败(跳过): %v", err)
return nil, nil
}
work, err := gorm.Open(mysqldriver.Open(base+xtestDBName+"?charset=utf8mb4&parseTime=True&loc=Local"),
&gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
if err != nil {
t.Fatalf("mysql 打开工作库: %v", err)
}
dialect.Register(&dialect.MySQLDialect{})
if err := work.AutoMigrate(xtestModels...); err != nil {
t.Fatalf("mysql AutoMigrate: %v", err)
}
return work, func() {
if s, e := work.DB(); e == nil {
s.Close()
}
root.Exec("DROP DATABASE IF EXISTS " + xtestDBName)
if s, e := root.DB(); e == nil {
s.Close()
}
}
}
func setupPostgres(t *testing.T) (*gorm.DB, func()) {
base := os.Getenv("SAMWAF_TEST_PG_DSN")
if base == "" {
base = "postgres://postgres:postgres@127.0.0.1:5432/"
}
root, err := gorm.Open(pgdriver.Open(base+"postgres?sslmode=disable"), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
if err != nil {
t.Logf("postgres 连接失败(跳过): %v", err)
return nil, nil
}
root.Exec("DROP DATABASE IF EXISTS " + xtestDBName)
if err := root.Exec("CREATE DATABASE " + xtestDBName + " ENCODING 'UTF8'").Error; err != nil {
t.Logf("postgres 建库失败(跳过): %v", err)
return nil, nil
}
work, err := gorm.Open(pgdriver.Open(base+xtestDBName+"?sslmode=disable&TimeZone=Asia/Shanghai"),
&gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
if err != nil {
t.Fatalf("postgres 打开工作库: %v", err)
}
dialect.Register(&dialect.PostgresDialect{})
if err := work.AutoMigrate(xtestModels...); err != nil {
t.Fatalf("postgres AutoMigrate: %v", err)
}
return work, func() {
if s, e := work.DB(); e == nil {
s.Close() // 先断开工作连接,否则 DROP DATABASE 会被占用
}
root.Exec("DROP DATABASE IF EXISTS " + xtestDBName)
if s, e := root.DB(); e == nil {
s.Close()
}
}
}
func newBase(id string) baseorm.BaseOrm {
return baseorm.BaseOrm{
Id: id,
USER_CODE: xtestUser,
Tenant_ID: xtestTenant,
CREATE_TIME: customtype.JsonTime(time.Now()),
UPDATE_TIME: customtype.JsonTime(time.Now()),
}
}
func TestCrossEngine(t *testing.T) {
global.GWAF_USER_CODE = xtestUser
global.GWAF_TENANT_ID = xtestTenant
for _, e := range xengines() {
e := e
t.Run(e.name, func(t *testing.T) {
// 每个引擎前清空全局句柄,避免复用上一个引擎的连接
global.GWAF_LOCAL_DB = nil
db, teardown := e.setup(t)
if db == nil {
t.Skipf("%s 未就绪,跳过", e.name)
return
}
defer teardown()
global.GWAF_LOCAL_DB = db
// —— 修改类接口:走真实 service ModifyApi,内部即 Updates(map) ——
t.Run("BlockIP", func(t *testing.T) {
id := uuid.GenUUID()
must(t, db.Create(&model.IPBlockList{BaseOrm: newBase(id), HostCode: "h1", Ip: "1.1.1.1"}).Error)
fatalIf(t, WafBlockIpServiceApp.ModifyApi(request.WafBlockIpEditReq{Id: id, HostCode: "h2", Ip: "1.1.1.1", Remarks: "r"}))
assertHostCode(t, db, &model.IPBlockList{}, id)
})
t.Run("AllowIP", func(t *testing.T) {
id := uuid.GenUUID()
must(t, db.Create(&model.IPAllowList{BaseOrm: newBase(id), HostCode: "h1", Ip: "2.2.2.2"}).Error)
fatalIf(t, WafWhiteIpServiceApp.ModifyApi(request.WafAllowIpEditReq{Id: id, HostCode: "h2", Ip: "2.2.2.2", Remarks: "r"}))
assertHostCode(t, db, &model.IPAllowList{}, id)
})
t.Run("AllowUrl", func(t *testing.T) {
id := uuid.GenUUID()
must(t, db.Create(&model.URLAllowList{BaseOrm: newBase(id), HostCode: "h1", CompareType: "suffix", Url: "/a"}).Error)
fatalIf(t, WafWhiteUrlServiceApp.ModifyApi(request.WafAllowUrlEditReq{Id: id, HostCode: "h2", CompareType: "prefix", Url: "/a", Remarks: "r"}))
assertHostCode(t, db, &model.URLAllowList{}, id)
})
t.Run("BlockUrl", func(t *testing.T) {
id := uuid.GenUUID()
must(t, db.Create(&model.URLBlockList{BaseOrm: newBase(id), HostCode: "h1", CompareType: "suffix", Url: "/b"}).Error)
fatalIf(t, WafBlockUrlServiceApp.ModifyApi(request.WafBlockUrlEditReq{Id: id, HostCode: "h2", CompareType: "prefix", Url: "/b", Remarks: "r"}))
assertHostCode(t, db, &model.URLBlockList{}, id)
})
t.Run("AntiCC", func(t *testing.T) {
id := uuid.GenUUID()
must(t, db.Create(&model.AntiCC{BaseOrm: newBase(id), HostCode: "h1", Rate: 1, Limit: 1, LimitMode: "window"}).Error)
fatalIf(t, WafAntiCCServiceApp.ModifyApi(request.WafAntiCCEditReq{
Id: id, HostCode: "h2", Rate: 5, Limit: 5, LockIPMinutes: 5, LimitMode: "window",
}))
assertHostCode(t, db, &model.AntiCC{}, id)
})
t.Run("Ldp", func(t *testing.T) {
id := uuid.GenUUID()
must(t, db.Create(&model.LDPUrl{BaseOrm: newBase(id), HostCode: "h1", CompareType: "suffix", Url: "/c"}).Error)
fatalIf(t, WafLdpUrlServiceApp.ModifyApi(request.WafLdpUrlEditReq{Id: id, HostCode: "h2", CompareType: "prefix", Url: "/c", Remarks: "r"}))
assertHostCode(t, db, &model.LDPUrl{}, id)
})
t.Run("LoadBalance", func(t *testing.T) {
id := uuid.GenUUID()
must(t, db.Create(&model.LoadBalance{BaseOrm: newBase(id), HostCode: "h1", Remote_ip: "3.3.3.3", Remote_port: 80}).Error)
fatalIf(t, WafLoadBalanceServiceApp.ModifyApi(request.WafLoadBalanceEditReq{
Id: id, HostCode: "h2", Remote_ip: "4.4.4.4", Remote_port: 8080, Weight: 2, Remarks: "r",
}))
var got model.LoadBalance
must(t, db.Where("id = ?", id).First(&got).Error)
if got.HostCode != "h2" || got.Remote_ip != "4.4.4.4" || got.Remote_port != 8080 {
t.Fatalf("LoadBalance 更新未落库: %+v", got)
}
})
// Rule 的 ModifyApi 签名较重,直接用它内部同款 Updates(map) 验证 user_code 等 key
t.Run("Rule_UpdateMap", func(t *testing.T) {
code := uuid.GenUUID()
must(t, db.Create(&model.Rules{BaseOrm: newBase(uuid.GenUUID()), RuleCode: code, RuleName: "r1"}).Error)
ruleMap := map[string]interface{}{
"host_code": "hh", // 与修复后一致的列名写法
"RuleName": "r2",
"user_code": xtestUser, // 本次修复点:原为 "User_code"
"UPDATE_TIME": customtype.JsonTime(time.Now()),
}
fatalIf(t, db.Model(model.Rules{}).Where("rule_code = ?", code).Updates(ruleMap).Error)
var got model.Rules
must(t, db.Where("rule_code = ?", code).First(&got).Error)
if got.RuleName != "r2" {
t.Fatalf("Rule 更新未落库: %+v", got)
}
})
// —— 方言 SQL 在各引擎真实执行 ——
t.Run("DialectSQL", func(t *testing.T) {
d := dialect.Get()
db.Exec("DROP TABLE IF EXISTS xtest_probe")
fatalIf(t, db.Exec("CREATE TABLE xtest_probe (id varchar(64), tag varchar(64), cnt int)").Error)
fatalIf(t, db.Exec("INSERT INTO xtest_probe (id,tag,cnt) VALUES ('a','t1',1),('b','t2',20)").Error)
var s string
fatalIf(t, db.Raw("SELECT "+d.GroupConcatDistinct("tag")+" FROM xtest_probe").Scan(&s).Error)
fatalIf(t, db.Exec(d.BatchDeleteSQL("xtest_probe", "cnt < ?", 100), 10).Error)
db.Exec("DROP TABLE IF EXISTS xtest_probe")
})
})
}
}
func must(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("前置数据准备失败: %v", err)
}
}
func fatalIf(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatalf("操作失败(PG 未修复时此处应报 42703): %v", err)
}
}
// assertHostCode 读回记录,断言 host_code 已更新为 "h2"。
func assertHostCode(t *testing.T, db *gorm.DB, dst interface{}, id string) {
t.Helper()
if err := db.Where("id = ?", id).First(dst).Error; err != nil {
t.Fatalf("读回失败: %v", err)
}
got := fmt.Sprintf("%v", getHostCode(dst))
if got != "h2" {
t.Fatalf("host_code 未更新,仍为 %q,模型 %T", got, dst)
}
}
func getHostCode(v interface{}) string {
switch x := v.(type) {
case *model.IPBlockList:
return x.HostCode
case *model.IPAllowList:
return x.HostCode
case *model.URLBlockList:
return x.HostCode
case *model.URLAllowList:
return x.HostCode
case *model.AntiCC:
return x.HostCode
case *model.LDPUrl:
return x.HostCode
default:
return ""
}
}
+1 -1
View File
@@ -44,7 +44,7 @@ func (receiver *WafWhiteIpService) ModifyApi(wafWhiteIpEditReq request.WafAllowI
return errors.New("当前网站和IP已经存在")
}
ipWhiteMap := map[string]interface{}{
"Host_Code": wafWhiteIpEditReq.HostCode,
"host_code": wafWhiteIpEditReq.HostCode,
"Ip": wafWhiteIpEditReq.Ip,
"Remarks": wafWhiteIpEditReq.Remarks,
"UPDATE_TIME": customtype.JsonTime(time.Now()),
+2 -2
View File
@@ -45,8 +45,8 @@ func (receiver *WafWhiteUrlService) ModifyApi(req request.WafAllowUrlEditReq) er
return errors.New("当前网站和url已经存在")
}
ipWhiteMap := map[string]interface{}{
"Host_Code": req.HostCode,
"Compare_Type": req.CompareType,
"host_code": req.HostCode,
"compare_type": req.CompareType,
"Url": req.Url,
"Remarks": req.Remarks,
"UPDATE_TIME": customtype.JsonTime(time.Now()),
+1 -1
View File
@@ -57,7 +57,7 @@ func (receiver *WafAntiCCService) ModifyApi(req request.WafAntiCCEditReq) error
return errors.New("当前网站已经存在")
}
ipWhiteMap := map[string]interface{}{
"Host_Code": req.HostCode,
"host_code": req.HostCode,
"Url": req.Url,
"Rate": req.Rate,
"Limit": req.Limit,
+1 -1
View File
@@ -44,7 +44,7 @@ func (receiver *WafBlockIpService) ModifyApi(req request.WafBlockIpEditReq) erro
return errors.New("当前网站和IP已经存在")
}
ipWhiteMap := map[string]interface{}{
"Host_Code": req.HostCode,
"host_code": req.HostCode,
"Ip": req.Ip,
"Remarks": req.Remarks,
"UPDATE_TIME": customtype.JsonTime(time.Now()),
+1 -1
View File
@@ -45,7 +45,7 @@ func (receiver *WafBlockUrlService) ModifyApi(req request.WafBlockUrlEditReq) er
return errors.New("当前网站和url已经存在")
}
modfiyMap := map[string]interface{}{
"Host_Code": req.HostCode,
"host_code": req.HostCode,
"Url": req.Url,
"Remarks": req.Remarks,
"CompareType": req.CompareType,
+2 -2
View File
@@ -45,10 +45,10 @@ func (receiver *WafLdpUrlService) ModifyApi(req request.WafLdpUrlEditReq) error
return errors.New("当前网站和url已经存在")
}
ipWhiteMap := map[string]interface{}{
"Host_Code": req.HostCode,
"host_code": req.HostCode,
"Url": req.Url,
"Remarks": req.Remarks,
"Compare_Type": req.CompareType,
"compare_type": req.CompareType,
"UPDATE_TIME": customtype.JsonTime(time.Now()),
}
err := global.GWAF_LOCAL_DB.Model(model.LDPUrl{}).Where("id = ?", req.Id).Updates(ipWhiteMap).Error
+3 -3
View File
@@ -46,9 +46,9 @@ func (receiver *WafLoadBalanceService) ModifyApi(req request.WafLoadBalanceEditR
return errors.New("当前网站和IP已经存在")
}
editMap := map[string]interface{}{
"Host_Code": req.HostCode,
"Remote_Ip": req.Remote_ip,
"Remote_Port": req.Remote_port,
"host_code": req.HostCode,
"remote_ip": req.Remote_ip,
"remote_port": req.Remote_port,
"Weight": req.Weight,
"Remarks": req.Remarks,
"UPDATE_TIME": customtype.JsonTime(time.Now()),
+1 -1
View File
@@ -86,7 +86,7 @@ func (receiver *WafRuleService) ModifyApi(wafRuleEditReq request.WafRuleEditReq,
"RuleContentJSON": wafRuleEditReq.RuleJson,
"RuleVersionName": "初版",
"RuleVersion": rule.RuleVersion + 1,
"User_code": global.GWAF_USER_CODE,
"user_code": global.GWAF_USER_CODE,
"IsPublicRule": 0,
"IsManualRule": wafRuleEditReq.IsManualRule,
"RuleStatus": ruleStatus,
+3 -1
View File
@@ -896,7 +896,9 @@ func RunCoreDBMigrations(db *gorm.DB) error {
}
// 历史数据默认值为 false(保持原有行为:继续检测全局CC)
if err := tx.Exec("UPDATE anti_ccs SET skip_global_cc = 0 WHERE skip_global_cc IS NULL").Error; err != nil {
// 用 false 而非 0:skip_global_cc 是 boolean 列,PostgreSQL 不接受整数字面量
// (42804: type boolean but expression is of type integer);false 三种引擎通用。
if err := tx.Exec("UPDATE anti_ccs SET skip_global_cc = false WHERE skip_global_cc IS NULL").Error; err != nil {
zlog.Warn("设置 skip_global_cc 默认值失败", "error", err.Error())
}