From c933673ca9d6dcac82d39eef9e3c87dbd26ba62e Mon Sep 17 00:00:00 2001 From: salvacybersec Date: Sun, 5 Apr 2026 23:30:12 +0300 Subject: [PATCH] test(06-02): add failing tests for JSONFormatter --- pkg/output/json_test.go | 149 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 pkg/output/json_test.go diff --git a/pkg/output/json_test.go b/pkg/output/json_test.go new file mode 100644 index 0000000..47dc485 --- /dev/null +++ b/pkg/output/json_test.go @@ -0,0 +1,149 @@ +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 on at least one property line. + if !strings.Contains(buf.String(), "\n \"provider\"") { + t.Errorf("output not indented with 2 spaces:\n%s", buf.String()) + } +}