- .gitea/workflows/ci.yml:后端 gofmt/vet/test + 前端 lint/test/build 双 job 门禁 - vitest + @testing-library + jsdom 落地,14 个命脉用例:useListQuery 乱序丢弃/卸载中止、 http.ts 401 单飞刷新+队列重放+失败登出+silent 直拒、审计防抖、StepDeploy 部署目标、query-utils - 版本注入断裂修复:main 补 buildTime/gitCommit 符号,build.sh/Dockerfile 注入 main.version, /health 返回真实版本,前端 package.json 版本对齐 2.0.0 - gofmt 清零(8 个存量未格式化文件)
110 lines
3.4 KiB
Go
110 lines
3.4 KiB
Go
package middleware
|
|
|
|
import (
|
|
"net/http"
|
|
"testing"
|
|
)
|
|
|
|
// 回归:inferAuditFields 不能越界。
|
|
//
|
|
// 线上事故:/api/v1/hosts/2 拆出 4 段(api v1 hosts 2),
|
|
// 旧实现 `if len>=4 { resource=segments[3]; resourceID=segments[4] }`
|
|
// 直接 index out of range [4] with length 4 → 中间件 panic →
|
|
// 该路径上的所有写请求 500 且不落审计。
|
|
// 这类 bug 编译期发现不了,必须用表驱动测试把各段数都覆盖一遍。
|
|
func TestInferAuditFieldsNoPanic(t *testing.T) {
|
|
cases := []string{
|
|
"/api/v1", // 2 段
|
|
"/api/v1/hosts", // 3 段
|
|
"/api/v1/hosts/2", // 4 段 ← 原 bug 触发点
|
|
"/api/v1/hosts/2/validate", // 5 段
|
|
"/api/v1/deployments/1/deploy", // 5 段
|
|
"/api/v1/logs/policy-groups/3", // 5 段
|
|
"/api/v1/collectors/2/install", // 5 段
|
|
"/", // 1 段
|
|
"", // 0 段
|
|
}
|
|
for _, path := range cases {
|
|
func() {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
t.Fatalf("inferAuditFields(%q) panic: %v", path, r)
|
|
}
|
|
}()
|
|
inferAuditFields(http.MethodPost, path)
|
|
inferAuditFields(http.MethodPut, path)
|
|
inferAuditFields(http.MethodDelete, path)
|
|
}()
|
|
}
|
|
}
|
|
|
|
// 4 段路径必须能解析出 module 与 resource,且 resourceID 为空(不是越界值)
|
|
func TestInferAuditFieldsFourSegments(t *testing.T) {
|
|
module, _, resource, resourceID := inferAuditFields(http.MethodPut, "/api/v1/hosts/2")
|
|
if module != "hosts" {
|
|
t.Fatalf("module = %q,期望 hosts", module)
|
|
}
|
|
if resource != "2" {
|
|
// 4 段时第 4 段按位置是 resource
|
|
t.Fatalf("resource = %q,期望 2", resource)
|
|
}
|
|
if resourceID != "" {
|
|
t.Fatalf("4 段路径不应解析出 resourceID,实际 %q", resourceID)
|
|
}
|
|
}
|
|
|
|
// 5 段路径要正确解出 resourceID
|
|
func TestInferAuditFieldsFiveSegments(t *testing.T) {
|
|
module, action, resource, resourceID := inferAuditFields(http.MethodPost, "/api/v1/logs/sources/7")
|
|
if module != "logs" || resource != "sources" || resourceID != "7" {
|
|
t.Fatalf("解析错误: module=%q resource=%q resourceID=%q", module, resource, resourceID)
|
|
}
|
|
if action != "create" {
|
|
t.Fatalf("action = %q,期望 create", action)
|
|
}
|
|
}
|
|
|
|
// 语义化动作不能被位置解析覆盖
|
|
func TestInferAuditFieldsSemanticActions(t *testing.T) {
|
|
cases := map[string]string{
|
|
"/api/v1/deployments/1/deploy": "deploy",
|
|
"/api/v1/deployments/1/stop": "stop",
|
|
"/api/v1/collectors/2/install": "install",
|
|
"/api/v1/collectors/2/uninstall": "uninstall",
|
|
"/api/v1/users/3/reset-password": "reset-password",
|
|
"/api/v1/hosts/2/validate": "validate",
|
|
"/api/v1/storage/1/test": "test",
|
|
}
|
|
for path, want := range cases {
|
|
if _, action, _, _ := inferAuditFields(http.MethodPost, path); action != want {
|
|
t.Fatalf("%s action = %q,期望 %q", path, action, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// shouldAudit:只读接口不记,但部署/安装类动作必须记
|
|
func TestShouldAudit(t *testing.T) {
|
|
mustAudit := []string{
|
|
"/api/v1/deployments/1/deploy",
|
|
"/api/v1/collectors/2/install",
|
|
"/api/v1/hosts/2/validate",
|
|
"/api/v1/storage",
|
|
}
|
|
for _, p := range mustAudit {
|
|
if !shouldAudit(p) {
|
|
t.Fatalf("%q 应当被审计", p)
|
|
}
|
|
}
|
|
mustSkip := []string{
|
|
"/api/v1/logs/query",
|
|
"/api/v1/logs/stats",
|
|
"/api/v1/health",
|
|
"/api/v1/metrics",
|
|
}
|
|
for _, p := range mustSkip {
|
|
if shouldAudit(p) {
|
|
t.Fatalf("%q 不应被审计(只读/噪音)", p)
|
|
}
|
|
}
|
|
}
|