package main import ( "encoding/json" "fmt" "net/http" "net/http/httptest" "path/filepath" "strconv" "strings" "testing" "github.com/gin-gonic/gin" "github.com/glebarez/sqlite" "gorm.io/gorm" businessbiz "kra/internal/biz/business" "kra/internal/biz/system" businessdata "kra/internal/data/business" systemdata "kra/internal/data/system" "kra/internal/server/handler" "kra/internal/server/router" businessservice "kra/internal/service/business" ) func TestAlertRecordHTTPCompleteWorkflowAndVisibility(t *testing.T) { db, err := gorm.Open(sqlite.Open(filepath.Join(t.TempDir(), "records.db")), &gorm.Config{}) if err != nil { t.Fatal(err) } pool, _ := db.DB() t.Cleanup(func() { _ = pool.Close() }) for _, step := range append(systemdata.Migrations(), businessdata.Migrations()...) { if err := step.Migrate(db); err != nil { t.Fatal(err) } } for _, sql := range []string{ "INSERT INTO sys_users(id,uuid,username,level,parent_id,id_path,enable) VALUES(1,'root','admin',0,0,'0',1),(7,'owner','owner',1,0,'0',1),(8,'child','child',2,7,'0,7',1),(9,'leaf','leaf',3,8,'0,7,8',1),(10,'sibling','sibling',2,7,'0,7',1),(99,'other','other',1,0,'0',1)", "CREATE TABLE tab_promote(id INTEGER,source_db TEXT,busier_id INTEGER,promote_level INTEGER,status INTEGER)", "INSERT INTO tab_promote VALUES(100,'lx',14,1,0),(100,'hw',14,1,1),(100,'yq',14,1,1)", "INSERT INTO sys_user_bind_platform(user_id,source_db,platform_busier_id,status) VALUES(9,'lx',14,'0'),(10,'hw',14,'0'),(99,'yq',14,'0')", "INSERT INTO sys_alert_record(id,platform_code,target_promote_id,business_id,rule_name,status,created_at,updated_at) VALUES(9007199254740993,'lx',100,7,'目标规则',1,'2026-11-03 08:00:00','2026-11-03 08:00:00'),(2,'hw',100,7,'平级规则',1,'2026-11-03 08:00:00','2026-11-03 08:00:00'),(3,'lx',100,99,'归属错误',1,'2026-11-03 08:00:00','2026-11-03 08:00:00'),(4,'yq',100,99,'其他规则',1,'2026-11-03 08:00:00','2026-11-03 08:00:00'),(5,'lx',100,7,'关闭规则',5,'2026-11-03 08:00:00','2026-11-03 08:00:00'),(6,'lx',100,7,'待审规则',3,'2026-11-03 08:00:00','2026-11-03 08:00:00')", "ALTER TABLE tab_promote ADD COLUMN account TEXT DEFAULT '测试渠道'", "INSERT INTO media_file_upload_and_downloads(id,url) VALUES(1,'/files/one.png'),(2,'/files/two.png')", } { if err := db.Exec(sql).Error; err != nil { t.Fatal(err) } } provider := businessHTTPData{db} accounts := businessbiz.NewAccountUsecase(businessdata.NewAccountRepo(provider)) service := businessservice.NewAlertRecordService(businessbiz.NewAlertRecordUsecase(businessdata.NewAlertRecordRepo(provider), accounts)) engine := gin.New() engine.Use(func(c *gin.Context) { id, _ := strconv.ParseUint(c.GetHeader("X-Test-User"), 10, 64) authority := uint(901) if id == 1 { authority = 888 } c.Request = c.Request.WithContext(system.NewActorContext(c.Request.Context(), system.Actor{UserID: uint(id), AuthorityID: authority})) c.Next() }) router.RegisterAlertRecord(engine.Group(""), handler.NewAlertRecord(service)) router.RegisterAlertGovernance(engine.Group(""), handler.NewAlertGovernance(businessservice.NewAlertGovernanceService(businessbiz.NewAlertGovernanceUsecase(businessdata.NewAlertGovernanceRepo(provider), accounts)))) server := httptest.NewServer(engine) defer server.Close() call := func(user int, method, path, body string, expected int) map[string]any { t.Helper() req, err := http.NewRequest(method, server.URL+path, strings.NewReader(body)) if err != nil { t.Fatal(err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("X-Test-User", strconv.Itoa(user)) response, err := server.Client().Do(req) if err != nil { t.Fatal(err) } defer response.Body.Close() var result map[string]any if err := json.NewDecoder(response.Body).Decode(&result); err != nil { t.Fatal(err) } if result["code"] != float64(expected) { t.Fatalf("%s %s 用户%d 响应错误: %+v", method, path, user, result) } return result } list := func(user int, query string, total int) map[string]any { t.Helper() data := call(user, "GET", "/sw/alertRecord/list"+query, "", 0)["data"].(map[string]any) if data["pageData"].(map[string]any)["total"] != float64(total) { t.Fatalf("列表范围错误: %+v", data) } return data } list(1, "", 6) list(7, "", 4) platforms := call(8, "GET", "/sw/alertRecord/platforms", "", 0)["data"].([]any) if len(platforms) != 1 || platforms[0] != "lx" { t.Fatal("平台下拉必须按当前可见绑定过滤") } list(8, "", 3) list(9, "", 3) list(7, "?businessId=8", 3) list(8, "?businessId=99", 0) list(1, "?businessId=8", 4) list(8, "?platformCodes=hw", 0) data := list(8, "?status=1&ruleName=%E7%9B%AE%E6%A0%87", 1) if len(data["statusCount"].(map[string]any)) != 1 { t.Fatal("汇总应继承名称筛选") } const id = "9007199254740993" detailPath := "/sw/alertRecord/detail/" + id data = call(9, "GET", detailPath, "", 0)["data"].(map[string]any) if data["canAudit"] != false || data["alertRecord"].(map[string]any)["id"] != id { t.Fatal("详情审核权限或大 ID 精度错误") } call(8, "GET", "/sw/alertRecord/detail/2", "", 7) call(8, "GET", "/sw/alertRecord/detail/3", "", 7) call(99, "GET", detailPath, "", 7) body := `{"alertId":"` + id + `"}` process := func(user int, action, note string, code int) { payload := body if note != "" { encoded, _ := json.Marshal(note) payload = fmt.Sprintf(`{"alertId":"%s","note":%s}`, id, encoded) } call(user, "POST", "/sw/alertRecord/"+action, payload, code) } process(9, "submitAudit", "", 7) process(9, "startProcess", "开始排查", 0) process(9, "startProcess", "重复开始", 7) process(9, "submitAudit", "", 0) process(1, "approve", "超管不能绕过等级", 7) process(8, "approve", "", 7) process(9, "approve", "", 7) process(7, "reject", "补充说明", 0) process(9, "reProcess", "", 0) process(9, "submitAudit", "", 0) process(7, "approve", "", 0) process(9, "reProcess", "", 7) data = call(7, "GET", detailPath, "", 0)["data"].(map[string]any) if data["canAudit"] != true || data["alertRecord"].(map[string]any)["status"] != float64(5) || len(data["operationLogs"].([]any)) != 8 { t.Fatalf("完整处理审核日志错误: %+v", data) } call(9, "POST", "/sw/alertRecord/addNote", `{"alertId":"`+id+`","noteContent":" ","attachmentIds":[1]}`, 7) call(9, "POST", "/sw/alertRecord/addNote", `{"alertId":"`+id+`","noteContent":"关闭后补充","attachmentIds":[2,"1"]}`, 7) call(9, "POST", "/sw/alertRecord/addNote", `{"alertId":"6","noteContent":"待审核补充","attachmentIds":[2,"1"]}`, 0) data = call(9, "GET", "/sw/alertRecord/detail/6", "", 0)["data"].(map[string]any) logs := data["operationLogs"].([]any) latest := logs[0].(map[string]any) if len(logs) != 1 || latest["attachments"].([]any)[0] != "/files/two.png" || data["alertRecord"].(map[string]any)["status"] != float64(3) { t.Fatalf("独立备注或附件错误: %+v", data) } call(9, "POST", "/sw/alertRecord/ignore", `{"ids":["6"]}`, 7) call(7, "POST", "/sw/alertRecord/ignore", `{"ids":["6","4"]}`, 7) data = call(7, "GET", "/sw/alertRecord/detail/6", "", 0)["data"].(map[string]any) if data["alertRecord"].(map[string]any)["status"] != float64(3) { t.Fatal("混入越权 ID 时不能部分忽略") } call(7, "POST", "/sw/alertRecord/ignore", `{"ids":["6","6"]}`, 0) call(7, "POST", "/sw/alertRecord/ignore", `{"ids":["6"]}`, 0) data = call(7, "GET", "/sw/alertRecord/detail/6", "", 0)["data"].(map[string]any) if data["alertRecord"].(map[string]any)["status"] != float64(6) || len(data["operationLogs"].([]any)) != 2 { t.Fatal("忽略必须幂等留痕") } call(99, "POST", "/sw/alertRecord/addNote", `{"alertId":"`+id+`","noteContent":"越权"}`, 7) call(1, "POST", "/sw/alertRecord/startProcess", `{"alertId":0}`, 7) call(9, "PUT", "/sw/alertRule/gap", `{"enabled":true,"days":7}`, 7) call(7, "PUT", "/sw/alertRule/gap", `{"enabled":true,"days":0}`, 7) call(7, "PUT", "/sw/alertRule/gap", `{"enabled":true,"days":1000}`, 0) gap := call(9, "GET", "/sw/alertRule/gap", "", 0)["data"].(map[string]any) if gap["days"] != float64(1000) || gap["enabled"] != true { t.Fatal("下级应看到其一级商务设置的统一间隔") } call(9, "GET", "/sw/alertExemption/candidates?keyword=test", "", 7) choices := call(7, "GET", "/sw/alertExemption/candidates", "", 0)["data"].([]any) if len(choices) != 2 { t.Fatalf("候选渠道越权或丢失: %+v", choices) } for _, choice := range choices { if _, ok := choice.(map[string]any)["id"].(string); !ok { t.Fatal("渠道 ID 应保持字符串精度") } } call(9, "POST", "/sw/alertExemption/add", `{"sourceDb":"lx","promoteId":"100","reason":"测试"}`, 7) call(7, "POST", "/sw/alertExemption/add", `{"sourceDb":"yq","promoteId":"100","reason":"越权"}`, 7) call(7, "POST", "/sw/alertExemption/add", `{"sourceDb":"lx","promoteId":"100","reason":"测试免预警"}`, 0) call(7, "POST", "/sw/alertExemption/add", `{"sourceDb":"lx","promoteId":"100","reason":"重复"}`, 7) call(9, "GET", "/sw/alertExemption/list", "", 7) ex := call(7, "GET", "/sw/alertExemption/list?active=true", "", 0)["data"].(map[string]any) entry := ex["rows"].([]any)[0].(map[string]any) eid := entry["id"].(string) call(99, "PUT", "/sw/alertExemption/remove/"+eid, "", 7) call(7, "PUT", "/sw/alertExemption/remove/"+eid, "", 0) ex = call(7, "GET", "/sw/alertExemption/list?active=false", "", 0)["data"].(map[string]any) entry = ex["rows"].([]any)[0].(map[string]any) if entry["removedName"] != "owner" || entry["removeReason"] != "手动移出" || entry["reason"] != "测试免预警" { t.Fatal("移出后应保留完整审计记录") } if err := db.Exec("DROP TABLE tab_promote").Error; err != nil { t.Fatal(err) } data = list(8, "", 0) if data["available"] != false { t.Fatal("缺渠道表应报告未知范围") } call(8, "GET", detailPath, "", 7) process(9, "startProcess", "", 7) list(1, "", 6) }