// audit_l5_test.go — L5 回归:admin 类动作(如登录失败)必须落审计。 package middleware import ( "context" "net/http" "net/http/httptest" "testing" "time" "github.com/gin-gonic/gin" "fileshare/internal/audit" "fileshare/internal/model" ) type captureSink struct { logs []model.AuditLog } func (s *captureSink) Save(_ context.Context, logs []model.AuditLog) error { s.logs = append(s.logs, logs...) return nil } // TestAuditRecordsAdminActions L5:/admin/login 失败(401)后应产生一条 // result=denied 的 admin 审计记录(此前 skip 条件把 admin 动作整体跳过)。 func TestAuditRecordsAdminActions(t *testing.T) { gin.SetMode(gin.TestMode) sink := &captureSink{} svc := audit.NewService(sink) r := gin.New() r.Use(Audit(svc, nil)) // DefaultClassifier r.POST("/admin/login", func(c *gin.Context) { c.JSON(http.StatusUnauthorized, gin.H{"code": 401}) }) r.POST("/share/text", func(c *gin.Context) { c.JSON(http.StatusOK, gin.H{"code": 200}) }) r.GET("/healthz", func(c *gin.Context) { c.Status(http.StatusOK) // 未分类动作:不应产生审计 }) // 管理端:401 → denied w := httptest.NewRecorder() r.ServeHTTP(w, httptest.NewRequest("POST", "/admin/login", nil)) if w.Code != http.StatusUnauthorized { t.Fatalf("login should 401, got %d", w.Code) } // 上传类:200 → success w2 := httptest.NewRecorder() r.ServeHTTP(w2, httptest.NewRequest("POST", "/share/text", nil)) // 未分类:不落库 w3 := httptest.NewRecorder() r.ServeHTTP(w3, httptest.NewRequest("GET", "/healthz", nil)) // audit.Service 异步落库,轮询等待 var actions []string for i := 0; i < 50; i++ { if len(sink.logs) >= 2 { break } waitMillis(20) } if len(sink.logs) != 2 { t.Fatalf("应恰好 2 条审计记录, got %d", len(sink.logs)) } for _, l := range sink.logs { actions = append(actions, l.Action) switch l.Action { case audit.ActionAdmin: if l.Result != model.AuditResultDenied { t.Fatalf("admin 401 应记 denied, got %q", l.Result) } case audit.ActionUpload: if l.Result != model.AuditResultSuccess { t.Fatalf("upload 200 应记 success, got %q", l.Result) } default: t.Fatalf("意外动作 %q", l.Action) } } _ = actions } func waitMillis(ms int) { time.Sleep(time.Duration(ms) * time.Millisecond) }