Address second-round review on PR #555: - Move CloseAuditSink into the unconditional Execute defer so async remote forwards are drained on BOTH success and failure paths. Cobra skips PersistentPostRunE when RunE returns an error, which previously dropped in-flight forwards for failed commands. Make CloseAuditSink idempotent via sync.Once so the success-path hook and the defer can both call it. - CSV export now returns a "文件:行号" error on malformed JSONL instead of silently skipping the line and exiting 0. - Add regressions: TestCloseAuditSinkDrainsOnErrorPath (error-path drain), TestExportCSVFailsOnMalformedJSON (corrupt JSONL visible), and TestAuditIdentityReresolvesOnProfileSwitch (per-profile Actor via an injectable token loader seam).
107 lines
3.0 KiB
Go
107 lines
3.0 KiB
Go
package app
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestAuditTailRejectsNonPositiveLines(t *testing.T) {
|
|
for _, n := range []string{"0", "-1"} {
|
|
cmd := newAuditTailCommand()
|
|
cmd.SetArgs([]string{"--lines", n})
|
|
cmd.SilenceUsage = true
|
|
cmd.SilenceErrors = true
|
|
err := cmd.Execute()
|
|
if err == nil {
|
|
t.Fatalf("--lines %s: expected error, got nil", n)
|
|
}
|
|
if !strings.Contains(err.Error(), "正整数") {
|
|
t.Fatalf("--lines %s: unexpected error: %v", n, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTailFileReturnsLastN(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "audit-20260101.jsonl")
|
|
if err := os.WriteFile(path, []byte("a\nb\nc\nd\ne\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
lines, err := tailFile(path, 2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(lines) != 2 || lines[0] != "d" || lines[1] != "e" {
|
|
t.Fatalf("got %v, want [d e]", lines)
|
|
}
|
|
}
|
|
|
|
func TestExportCSVWritesHeaderAndRows(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "audit-20260101.jsonl")
|
|
rec := `{"timestamp":"2026-01-01T00:00:00Z","execution_id":"e1","actor":{"user_id":"u1","corp_id":"c1"},"product":"calendar","command":"event_list","result":"success","duration_ms":12,"hash":"h","prev_hash":""}`
|
|
if err := os.WriteFile(path, []byte(rec+"\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stdout := os.Stdout
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
os.Stdout = w
|
|
exportErr := exportCSV([]string{path})
|
|
w.Close()
|
|
os.Stdout = stdout
|
|
|
|
if exportErr != nil {
|
|
t.Fatalf("exportCSV error: %v", exportErr)
|
|
}
|
|
buf := make([]byte, 4096)
|
|
n, _ := r.Read(buf)
|
|
out := string(buf[:n])
|
|
if !strings.Contains(out, "timestamp,execution_id") {
|
|
t.Fatalf("missing CSV header, got: %q", out)
|
|
}
|
|
if !strings.Contains(out, "e1") || !strings.Contains(out, "event_list") {
|
|
t.Fatalf("missing CSV row data, got: %q", out)
|
|
}
|
|
}
|
|
|
|
// TestExportCSVFailsOnMalformedJSON guards the reviewer's V9 finding: a corrupt
|
|
// JSONL line must surface an error with file/line evidence instead of being
|
|
// silently skipped while the command exits 0.
|
|
func TestExportCSVFailsOnMalformedJSON(t *testing.T) {
|
|
dir := t.TempDir()
|
|
path := filepath.Join(dir, "audit-20260101.jsonl")
|
|
good := `{"timestamp":"2026-01-01T00:00:00Z","execution_id":"e1","actor":{"user_id":"u1"},"product":"calendar","command":"event_list","result":"success","duration_ms":1,"hash":"h","prev_hash":""}`
|
|
if err := os.WriteFile(path, []byte(good+"\nnot-json\n"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
stdout := os.Stdout
|
|
r, w, err := os.Pipe()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
os.Stdout = w
|
|
exportErr := exportCSV([]string{path})
|
|
w.Close()
|
|
os.Stdout = stdout
|
|
// Drain the pipe so the writer never blocks.
|
|
buf := make([]byte, 4096)
|
|
_, _ = r.Read(buf)
|
|
|
|
if exportErr == nil {
|
|
t.Fatal("expected error on malformed JSONL, got nil")
|
|
}
|
|
if !strings.Contains(exportErr.Error(), "解析审计记录失败") {
|
|
t.Fatalf("error missing parse context: %v", exportErr)
|
|
}
|
|
if !strings.Contains(exportErr.Error(), ":2") {
|
|
t.Fatalf("error missing line evidence: %v", exportErr)
|
|
}
|
|
}
|