Files
keyhunter/pkg/output/csv_test.go
2026-04-05 23:31:44 +03:00

172 lines
4.5 KiB
Go

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])
}
}