diff --git a/service/waf_service/cross_engine_test.go b/service/waf_service/cross_engine_test.go new file mode 100644 index 0000000..3abc458 --- /dev/null +++ b/service/waf_service/cross_engine_test.go @@ -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 "" + } +} diff --git a/service/waf_service/waf_allow_ip.go b/service/waf_service/waf_allow_ip.go index 150a679..167f915 100644 --- a/service/waf_service/waf_allow_ip.go +++ b/service/waf_service/waf_allow_ip.go @@ -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()), diff --git a/service/waf_service/waf_allow_url.go b/service/waf_service/waf_allow_url.go index c48baea..04e6e2c 100644 --- a/service/waf_service/waf_allow_url.go +++ b/service/waf_service/waf_allow_url.go @@ -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()), diff --git a/service/waf_service/waf_anticc.go b/service/waf_service/waf_anticc.go index d0c62e6..6e25623 100644 --- a/service/waf_service/waf_anticc.go +++ b/service/waf_service/waf_anticc.go @@ -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, diff --git a/service/waf_service/waf_block_ip.go b/service/waf_service/waf_block_ip.go index fff62cf..8fa34f5 100644 --- a/service/waf_service/waf_block_ip.go +++ b/service/waf_service/waf_block_ip.go @@ -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()), diff --git a/service/waf_service/waf_block_url.go b/service/waf_service/waf_block_url.go index 3731489..271b59b 100644 --- a/service/waf_service/waf_block_url.go +++ b/service/waf_service/waf_block_url.go @@ -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, diff --git a/service/waf_service/waf_ldp.go b/service/waf_service/waf_ldp.go index d0e5c62..ffa0f7f 100644 --- a/service/waf_service/waf_ldp.go +++ b/service/waf_service/waf_ldp.go @@ -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 diff --git a/service/waf_service/waf_loadbalance.go b/service/waf_service/waf_loadbalance.go index 513ded4..c488c5b 100644 --- a/service/waf_service/waf_loadbalance.go +++ b/service/waf_service/waf_loadbalance.go @@ -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()), diff --git a/service/waf_service/waf_rule.go b/service/waf_service/waf_rule.go index b027ee6..7467750 100644 --- a/service/waf_service/waf_rule.go +++ b/service/waf_service/waf_rule.go @@ -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, diff --git a/wafdb/migrations_core.go b/wafdb/migrations_core.go index 40436e4..7749962 100644 --- a/wafdb/migrations_core.go +++ b/wafdb/migrations_core.go @@ -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()) }