package security import ( "errors" "testing" "time" ) // ============================================================ // NoopAuditSink // ============================================================ func TestNoopAuditSink_Write_ReturnsNil(t *testing.T) { var s NoopAuditSink err := s.Write(AuditEntry{ SessionID: "s1", ToolName: "Write", Operation: "write", Resource: "/tmp/foo.go", Outcome: "allowed", Timestamp: time.Now(), }) if err != nil { t.Errorf("NoopAuditSink.Write should return nil, got %v", err) } } func TestNoopAuditSink_Close_ReturnsNil(t *testing.T) { var s NoopAuditSink if err := s.Close(); err != nil { t.Errorf("NoopAuditSink.Close should return nil, got %v", err) } } // ============================================================ // CompositeAuditSink - 正常路径 // ============================================================ func TestCompositeAuditSink_Write_AllSucceed(t *testing.T) { written := 0 s1 := &stubSink{onWrite: func(AuditEntry) error { written++; return nil }} s2 := &stubSink{onWrite: func(AuditEntry) error { written++; return nil }} c := NewCompositeAuditSink(s1, s2) if err := c.Write(AuditEntry{ToolName: "Bash"}); err != nil { t.Errorf("expected nil error, got %v", err) } if written != 2 { t.Errorf("expected both sinks written, got %d", written) } } func TestCompositeAuditSink_Close_AllSucceed(t *testing.T) { closed := 0 s1 := &stubSink{onClose: func() error { closed++; return nil }} s2 := &stubSink{onClose: func() error { closed++; return nil }} c := NewCompositeAuditSink(s1, s2) if err := c.Close(); err != nil { t.Errorf("expected nil error, got %v", err) } if closed != 2 { t.Errorf("expected both sinks closed, got %d", closed) } } // ============================================================ // CompositeAuditSink - 错误继续写入(核心行为) // ============================================================ // TestCompositeAuditSink_Write_ContinuesAfterFirstFailure 验证 // 第一个 Sink 失败后,第二个 Sink 仍然被写入. // // 精妙之处(CLEVER): 审计不应因单点后端故障整体失败-- // 合规场景要求"至少一路"成功落地(双写本地+远端). // 如果第一个远端报错但本地 JSONL 还能写,审计记录不会丢. func TestCompositeAuditSink_Write_ContinuesAfterFirstFailure(t *testing.T) { errFirst := errors.New("sink1 failed") s1Written := false s2Written := false s1 := &stubSink{onWrite: func(AuditEntry) error { s1Written = true; return errFirst }} s2 := &stubSink{onWrite: func(AuditEntry) error { s2Written = true; return nil }} c := NewCompositeAuditSink(s1, s2) err := c.Write(AuditEntry{ToolName: "Edit"}) // s1 失败后 s2 仍然写入 if !s1Written { t.Error("s1 should have been called") } if !s2Written { t.Error("s2 should have been called even though s1 failed") } // 返回最后一个错误(s2 成功,所以 lastErr=nil,但 s1 失败过) // 注意:当 s2 成功时 lastErr 被 nil 覆盖--只要有一个成功结尾就不报错 _ = err // 行为由 Write 实现决定,此处验证"继续执行"比"返回值"更重要 } // TestCompositeAuditSink_Write_ReturnsLastError 验证 // 多个 Sink 失败时返回最后一个错误. func TestCompositeAuditSink_Write_ReturnsLastError(t *testing.T) { err1 := errors.New("sink1 failed") err2 := errors.New("sink2 failed") s1 := &stubSink{onWrite: func(AuditEntry) error { return err1 }} s2 := &stubSink{onWrite: func(AuditEntry) error { return err2 }} c := NewCompositeAuditSink(s1, s2) err := c.Write(AuditEntry{ToolName: "Bash"}) if !errors.Is(err, err2) { t.Errorf("expected last error (err2), got %v", err) } } func TestCompositeAuditSink_Close_ContinuesAfterFirstFailure(t *testing.T) { s2Closed := false s1 := &stubSink{onClose: func() error { return errors.New("close failed") }} s2 := &stubSink{onClose: func() error { s2Closed = true; return nil }} c := NewCompositeAuditSink(s1, s2) c.Close() //nolint: errcheck if !s2Closed { t.Error("s2 should be closed even though s1.Close() failed") } } // ============================================================ // CompositeAuditSink - 边界情况 // ============================================================ func TestCompositeAuditSink_Empty(t *testing.T) { c := NewCompositeAuditSink() if err := c.Write(AuditEntry{}); err != nil { t.Errorf("empty composite Write should return nil, got %v", err) } if err := c.Close(); err != nil { t.Errorf("empty composite Close should return nil, got %v", err) } } func TestCompositeAuditSink_Single(t *testing.T) { written := 0 s := &stubSink{onWrite: func(AuditEntry) error { written++; return nil }} c := NewCompositeAuditSink(s) c.Write(AuditEntry{ToolName: "Read"}) //nolint: errcheck if written != 1 { t.Errorf("expected 1 write, got %d", written) } } // ============================================================ // AuditEntry - Extra 字段跨行业扩展 // ============================================================ func TestAuditEntry_ExtraFields(t *testing.T) { // 验证 Extra 字段能存储任意键值(跨行业扩展点) entry := AuditEntry{ SessionID: "sess-1", ToolName: "Write", Operation: "write", Resource: "/warehouse/sku-001", Outcome: "allowed", Timestamp: time.Now(), Extra: map[string]string{ "sku": "SKU-001", "qty_before": "100", "qty_after": "80", }, } written := false s := &stubSink{onWrite: func(e AuditEntry) error { written = true if e.Extra["sku"] != "SKU-001" { t.Errorf("Extra[sku] lost in transit, got %q", e.Extra["sku"]) } return nil }} NewCompositeAuditSink(s).Write(entry) //nolint: errcheck if !written { t.Error("sink should have been called") } } // ============================================================ // stubSink - 测试用桩 // ============================================================ type stubSink struct { onWrite func(AuditEntry) error onClose func() error } func (s *stubSink) Write(e AuditEntry) error { if s.onWrite != nil { return s.onWrite(e) } return nil } func (s *stubSink) Close() error { if s.onClose != nil { return s.onClose() } return nil }