package output import ( "bytes" "encoding/json" "strings" "testing" "time" "github.com/salvacybersec/keyhunter/pkg/engine" ) func TestJSONFormatter_EmptyFindings(t *testing.T) { var buf bytes.Buffer if err := (JSONFormatter{}).Format(nil, &buf, Options{}); err != nil { t.Fatalf("Format returned error: %v", err) } got := buf.String() if got != "[]\n" { t.Fatalf("empty findings: want %q, got %q", "[]\n", got) } } func TestJSONFormatter_RegisteredUnderJSON(t *testing.T) { f, err := Get("json") if err != nil { t.Fatalf("Get(\"json\") error: %v", err) } if _, ok := f.(JSONFormatter); !ok { t.Fatalf("Get(\"json\") returned %T, want JSONFormatter", f) } } func sampleFinding() engine.Finding { return engine.Finding{ ProviderName: "openai", KeyValue: "sk-proj-abcdef1234567890XYZW", KeyMasked: "sk-proj-...XYZW", Confidence: "high", Source: "src/config.go", SourceType: "file", LineNumber: 42, Offset: 1024, DetectedAt: time.Date(2026, 4, 5, 12, 30, 0, 0, time.UTC), } } func TestJSONFormatter_MaskedByDefault(t *testing.T) { f := sampleFinding() var buf bytes.Buffer if err := (JSONFormatter{}).Format([]engine.Finding{f}, &buf, Options{}); err != nil { t.Fatalf("Format returned error: %v", err) } var out []map[string]any if err := json.Unmarshal(buf.Bytes(), &out); err != nil { t.Fatalf("invalid JSON: %v\n%s", err, buf.String()) } if len(out) != 1 { t.Fatalf("want 1 element, got %d", len(out)) } if out[0]["key"] != f.KeyMasked { t.Errorf("key: want %q (masked), got %v", f.KeyMasked, out[0]["key"]) } if out[0]["key_masked"] != f.KeyMasked { t.Errorf("key_masked: want %q, got %v", f.KeyMasked, out[0]["key_masked"]) } if out[0]["provider"] != "openai" { t.Errorf("provider: got %v", out[0]["provider"]) } if out[0]["confidence"] != "high" { t.Errorf("confidence: got %v", out[0]["confidence"]) } if out[0]["line"].(float64) != 42 { t.Errorf("line: got %v", out[0]["line"]) } if out[0]["detected_at"] != "2026-04-05T12:30:00Z" { t.Errorf("detected_at: got %v", out[0]["detected_at"]) } if out[0]["verified"] != false { t.Errorf("verified: got %v", out[0]["verified"]) } // Unverified: omitempty fields should not appear. if _, ok := out[0]["verify_status"]; ok { t.Errorf("verify_status should be omitted when empty") } } func TestJSONFormatter_UnmaskRevealsKey(t *testing.T) { f := sampleFinding() var buf bytes.Buffer if err := (JSONFormatter{}).Format([]engine.Finding{f}, &buf, Options{Unmask: true}); err != nil { t.Fatalf("Format returned error: %v", err) } var out []map[string]any if err := json.Unmarshal(buf.Bytes(), &out); err != nil { t.Fatalf("invalid JSON: %v", err) } if out[0]["key"] != f.KeyValue { t.Errorf("key: want %q (unmasked), got %v", f.KeyValue, out[0]["key"]) } if out[0]["key_masked"] != f.KeyMasked { t.Errorf("key_masked should remain masked, got %v", out[0]["key_masked"]) } } func TestJSONFormatter_VerifyFieldsPresent(t *testing.T) { f := sampleFinding() f.Verified = true f.VerifyStatus = "live" f.VerifyHTTPCode = 200 f.VerifyMetadata = map[string]string{"org": "acme"} var buf bytes.Buffer if err := (JSONFormatter{}).Format([]engine.Finding{f}, &buf, Options{}); err != nil { t.Fatalf("Format returned error: %v", err) } var out []map[string]any if err := json.Unmarshal(buf.Bytes(), &out); err != nil { t.Fatalf("invalid JSON: %v", err) } if out[0]["verified"] != true { t.Errorf("verified: got %v", out[0]["verified"]) } if out[0]["verify_status"] != "live" { t.Errorf("verify_status: got %v", out[0]["verify_status"]) } if out[0]["verify_http_code"].(float64) != 200 { t.Errorf("verify_http_code: got %v", out[0]["verify_http_code"]) } meta, ok := out[0]["verify_metadata"].(map[string]any) if !ok { t.Fatalf("verify_metadata missing or wrong type: %v", out[0]["verify_metadata"]) } if meta["org"] != "acme" { t.Errorf("verify_metadata.org: got %v", meta["org"]) } } func TestJSONFormatter_Indented(t *testing.T) { f := sampleFinding() var buf bytes.Buffer if err := (JSONFormatter{}).Format([]engine.Finding{f}, &buf, Options{}); err != nil { t.Fatalf("Format returned error: %v", err) } // Expect 2-space indent: array items at 2 spaces, nested fields at 4 spaces. s := buf.String() if !strings.Contains(s, "\n {") { t.Errorf("array item not indented with 2 spaces:\n%s", s) } if !strings.Contains(s, "\n \"provider\"") { t.Errorf("nested field not indented with 4 spaces:\n%s", s) } }