test(06-02): add failing tests for CSVFormatter
This commit is contained in:
171
pkg/output/csv_test.go
Normal file
171
pkg/output/csv_test.go
Normal file
@@ -0,0 +1,171 @@
|
||||
package output
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/csv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/salvacybersec/keyhunter/pkg/engine"
|
||||
)
|
||||
|
||||
func csvSampleFinding() engine.Finding {
|
||||
return engine.Finding{
|
||||
ProviderName: "anthropic",
|
||||
KeyValue: "sk-ant-api03-FULLKEY1234567890",
|
||||
KeyMasked: "sk-ant-a...7890",
|
||||
Confidence: "medium",
|
||||
Source: "src/client.go",
|
||||
SourceType: "file",
|
||||
LineNumber: 17,
|
||||
DetectedAt: time.Date(2026, 4, 5, 8, 15, 0, 0, time.UTC),
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSVFormatter_RegisteredUnderCSV(t *testing.T) {
|
||||
f, err := Get("csv")
|
||||
if err != nil {
|
||||
t.Fatalf("Get(\"csv\") error: %v", err)
|
||||
}
|
||||
if _, ok := f.(CSVFormatter); !ok {
|
||||
t.Fatalf("Get(\"csv\") returned %T, want CSVFormatter", f)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSVFormatter_HeaderOnly(t *testing.T) {
|
||||
var buf bytes.Buffer
|
||||
if err := (CSVFormatter{}).Format(nil, &buf, Options{}); err != nil {
|
||||
t.Fatalf("Format error: %v", err)
|
||||
}
|
||||
r := csv.NewReader(strings.NewReader(buf.String()))
|
||||
rows, err := r.ReadAll()
|
||||
if err != nil {
|
||||
t.Fatalf("csv parse error: %v\n%s", err, buf.String())
|
||||
}
|
||||
if len(rows) != 1 {
|
||||
t.Fatalf("want 1 row (header only), got %d: %v", len(rows), rows)
|
||||
}
|
||||
wantHeader := []string{"id", "provider", "confidence", "key", "source", "line", "detected_at", "verified", "verify_status"}
|
||||
if len(rows[0]) != len(wantHeader) {
|
||||
t.Fatalf("header length: got %d want %d", len(rows[0]), len(wantHeader))
|
||||
}
|
||||
for i, col := range wantHeader {
|
||||
if rows[0][i] != col {
|
||||
t.Errorf("header[%d]: got %q want %q", i, rows[0][i], col)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSVFormatter_RowMasked(t *testing.T) {
|
||||
f := csvSampleFinding()
|
||||
var buf bytes.Buffer
|
||||
if err := (CSVFormatter{}).Format([]engine.Finding{f}, &buf, Options{}); err != nil {
|
||||
t.Fatalf("Format error: %v", err)
|
||||
}
|
||||
r := csv.NewReader(strings.NewReader(buf.String()))
|
||||
rows, err := r.ReadAll()
|
||||
if err != nil {
|
||||
t.Fatalf("csv parse error: %v", err)
|
||||
}
|
||||
if len(rows) != 2 {
|
||||
t.Fatalf("want 2 rows, got %d", len(rows))
|
||||
}
|
||||
row := rows[1]
|
||||
if row[0] != "0" {
|
||||
t.Errorf("id: got %q want 0", row[0])
|
||||
}
|
||||
if row[1] != "anthropic" {
|
||||
t.Errorf("provider: got %q", row[1])
|
||||
}
|
||||
if row[2] != "medium" {
|
||||
t.Errorf("confidence: got %q", row[2])
|
||||
}
|
||||
if row[3] != f.KeyMasked {
|
||||
t.Errorf("key (masked): got %q want %q", row[3], f.KeyMasked)
|
||||
}
|
||||
if row[4] != "src/client.go" {
|
||||
t.Errorf("source: got %q", row[4])
|
||||
}
|
||||
if row[5] != "17" {
|
||||
t.Errorf("line: got %q", row[5])
|
||||
}
|
||||
if row[6] != "2026-04-05T08:15:00Z" {
|
||||
t.Errorf("detected_at: got %q", row[6])
|
||||
}
|
||||
if row[7] != "false" {
|
||||
t.Errorf("verified: got %q", row[7])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSVFormatter_Unmask(t *testing.T) {
|
||||
f := csvSampleFinding()
|
||||
var buf bytes.Buffer
|
||||
if err := (CSVFormatter{}).Format([]engine.Finding{f}, &buf, Options{Unmask: true}); err != nil {
|
||||
t.Fatalf("Format error: %v", err)
|
||||
}
|
||||
rows, err := csv.NewReader(&buf).ReadAll()
|
||||
if err != nil {
|
||||
t.Fatalf("csv parse error: %v", err)
|
||||
}
|
||||
if rows[1][3] != f.KeyValue {
|
||||
t.Errorf("key (unmasked): got %q want %q", rows[1][3], f.KeyValue)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSVFormatter_QuotesCommaInSource(t *testing.T) {
|
||||
f := csvSampleFinding()
|
||||
f.Source = "path, with, commas.txt"
|
||||
var buf bytes.Buffer
|
||||
if err := (CSVFormatter{}).Format([]engine.Finding{f}, &buf, Options{}); err != nil {
|
||||
t.Fatalf("Format error: %v", err)
|
||||
}
|
||||
// Round-trip must preserve the comma-containing source verbatim.
|
||||
rows, err := csv.NewReader(&buf).ReadAll()
|
||||
if err != nil {
|
||||
t.Fatalf("csv parse error: %v", err)
|
||||
}
|
||||
if rows[1][4] != "path, with, commas.txt" {
|
||||
t.Errorf("source: got %q", rows[1][4])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSVFormatter_VerifiedRow(t *testing.T) {
|
||||
f := csvSampleFinding()
|
||||
f.Verified = true
|
||||
f.VerifyStatus = "live"
|
||||
var buf bytes.Buffer
|
||||
if err := (CSVFormatter{}).Format([]engine.Finding{f}, &buf, Options{}); err != nil {
|
||||
t.Fatalf("Format error: %v", err)
|
||||
}
|
||||
rows, err := csv.NewReader(&buf).ReadAll()
|
||||
if err != nil {
|
||||
t.Fatalf("csv parse error: %v", err)
|
||||
}
|
||||
if rows[1][7] != "true" {
|
||||
t.Errorf("verified: got %q want true", rows[1][7])
|
||||
}
|
||||
if rows[1][8] != "live" {
|
||||
t.Errorf("verify_status: got %q want live", rows[1][8])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCSVFormatter_MultipleRowsIncrementID(t *testing.T) {
|
||||
a := csvSampleFinding()
|
||||
b := csvSampleFinding()
|
||||
b.ProviderName = "openai"
|
||||
var buf bytes.Buffer
|
||||
if err := (CSVFormatter{}).Format([]engine.Finding{a, b}, &buf, Options{}); err != nil {
|
||||
t.Fatalf("Format error: %v", err)
|
||||
}
|
||||
rows, err := csv.NewReader(&buf).ReadAll()
|
||||
if err != nil {
|
||||
t.Fatalf("csv parse error: %v", err)
|
||||
}
|
||||
if len(rows) != 3 {
|
||||
t.Fatalf("want 3 rows, got %d", len(rows))
|
||||
}
|
||||
if rows[1][0] != "0" || rows[2][0] != "1" {
|
||||
t.Errorf("ids: got %q,%q want 0,1", rows[1][0], rows[2][0])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user