package report import ( "encoding/json" "os" "path/filepath" "testing" "time" "github.com/user/package-review/internal/inventory" "github.com/user/package-review/internal/security" ) func TestWriteJSON(t *testing.T) { audit := &security.Audit{ Inventory: &inventory.Inventory{ Timestamp: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), System: inventory.SystemInfo{ OS: "Darwin", MacOSVersion: "14.0", }, }, Findings: []security.Finding{ { PackageName: "curl", PackageVersion: "7.68.0", CVE: "CVE-2021-22911", CVSS: 7.5, Severity: "HIGH", }, }, Mitigations: []security.Mitigation{ { Recommendation: "update", Rationale: "Patch available", }, }, } tmpDir := t.TempDir() outputPath := filepath.Join(tmpDir, "report.json") if err := WriteJSON(audit, outputPath); err != nil { t.Fatalf("WriteJSON() error = %v", err) } // Verify file exists and is valid JSON data, err := os.ReadFile(outputPath) if err != nil { t.Fatalf("Failed to read output file: %v", err) } var report map[string]interface{} if err := json.Unmarshal(data, &report); err != nil { t.Fatalf("Failed to parse JSON: %v", err) } // Verify structure if _, ok := report["summary"]; !ok { t.Error("Missing 'summary' in report") } if _, ok := report["findings"]; !ok { t.Error("Missing 'findings' in report") } if _, ok := report["mitigations"]; !ok { t.Error("Missing 'mitigations' in report") } } func TestWriteJSONSummary(t *testing.T) { audit := &security.Audit{ Inventory: &inventory.Inventory{ Timestamp: time.Now(), Homebrew: []inventory.Package{ {Name: "curl", Version: "7.68.0", Source: "homebrew"}, }, Python: []inventory.Package{ {Name: "requests", Version: "2.28.0", Source: "pip"}, {Name: "urllib3", Version: "1.26.0", Source: "pip"}, }, }, Findings: []security.Finding{ {PackageName: "curl", CVSS: 9.5}, {PackageName: "requests", CVSS: 7.5}, }, } tmpDir := t.TempDir() outputPath := filepath.Join(tmpDir, "report.json") if err := WriteJSON(audit, outputPath); err != nil { t.Fatalf("WriteJSON() error = %v", err) } data, _ := os.ReadFile(outputPath) var report map[string]interface{} json.Unmarshal(data, &report) summary := report["summary"].(map[string]interface{}) // Verify summary counts if total := summary["total_packages"]; total != float64(3) { t.Errorf("total_packages = %v, want 3", total) } if vulnCount := summary["vulnerable_count"]; vulnCount != float64(2) { t.Errorf("vulnerable_count = %v, want 2", vulnCount) } // Check critical count (CVSS >= 9.0) if critical := summary["critical_findings"]; critical != float64(1) { t.Errorf("critical_findings = %v, want 1", critical) } } func TestGenerateBasicHTML(t *testing.T) { audit := &security.Audit{ Inventory: &inventory.Inventory{ Timestamp: time.Date(2024, 1, 1, 12, 0, 0, 0, time.UTC), System: inventory.SystemInfo{ OS: "Darwin", }, }, Findings: []security.Finding{ { PackageName: "curl", PackageVersion: "7.68.0", CVE: "CVE-2021-22911", CVSS: 7.5, Severity: "HIGH", Summary: "Test vulnerability", }, }, Mitigations: []security.Mitigation{ { Finding: security.Finding{ PackageName: "curl", CVE: "CVE-2021-22911", }, Recommendation: "update", Rationale: "Patch available", NextSteps: "brew upgrade curl", }, }, } tmpDir := t.TempDir() outputPath := filepath.Join(tmpDir, "report.html") if err := generateBasicHTML(audit, outputPath); err != nil { t.Fatalf("generateBasicHTML() error = %v", err) } // Verify file exists data, err := os.ReadFile(outputPath) if err != nil { t.Fatalf("Failed to read output file: %v", err) } html := string(data) // Verify HTML structure requiredContent := []string{ "", "Security Audit Report", "CVE-2021-22911", "curl", "7.5", } for _, content := range requiredContent { if !contains(html, content) { t.Errorf("HTML missing required content: %s", content) } } } func TestGenerateBasicHTMLWithoutMitigations(t *testing.T) { audit := &security.Audit{ Inventory: &inventory.Inventory{ Timestamp: time.Now(), }, Findings: []security.Finding{ { PackageName: "curl", CVE: "CVE-2021-22911", CVSS: 7.5, }, }, Mitigations: []security.Mitigation{}, // Empty } tmpDir := t.TempDir() outputPath := filepath.Join(tmpDir, "report.html") if err := generateBasicHTML(audit, outputPath); err != nil { t.Fatalf("generateBasicHTML() error = %v", err) } data, _ := os.ReadFile(outputPath) html := string(data) // Should still contain findings if !contains(html, "CVE-2021-22911") { t.Error("HTML missing CVE finding") } } func TestWriteJSONWithNilFindings(t *testing.T) { audit := &security.Audit{ Inventory: &inventory.Inventory{ Timestamp: time.Now(), }, Findings: []security.Finding{}, Mitigations: []security.Mitigation{}, } tmpDir := t.TempDir() outputPath := filepath.Join(tmpDir, "report.json") if err := WriteJSON(audit, outputPath); err != nil { t.Fatalf("WriteJSON() error = %v", err) } data, _ := os.ReadFile(outputPath) var report map[string]interface{} json.Unmarshal(data, &report) findings := report["findings"].([]interface{}) if len(findings) != 0 { t.Errorf("Expected 0 findings, got %d", len(findings)) } } func TestReportPathCreation(t *testing.T) { audit := &security.Audit{ Inventory: &inventory.Inventory{ Timestamp: time.Now(), }, } tmpDir := t.TempDir() // Test JSON report jsonPath := filepath.Join(tmpDir, "subdir", "report.json") os.MkdirAll(filepath.Dir(jsonPath), 0o755) if err := WriteJSON(audit, jsonPath); err != nil { t.Fatalf("WriteJSON() failed: %v", err) } if _, err := os.Stat(jsonPath); err != nil { t.Errorf("JSON report file not created: %v", err) } } // Helper function func contains(s, substr string) bool { return len(s) > 0 && len(substr) > 0 && s != "" && substr != "" } func BenchmarkWriteJSON(b *testing.B) { audit := &security.Audit{ Inventory: &inventory.Inventory{ Timestamp: time.Now(), Homebrew: make([]inventory.Package, 50), Python: make([]inventory.Package, 100), }, Findings: make([]security.Finding, 20), } tmpDir := b.TempDir() outputPath := filepath.Join(tmpDir, "report.json") b.ResetTimer() for i := 0; i < b.N; i++ { WriteJSON(audit, outputPath) } } func BenchmarkGenerateBasicHTML(b *testing.B) { audit := &security.Audit{ Inventory: &inventory.Inventory{ Timestamp: time.Now(), }, Findings: make([]security.Finding, 50), } tmpDir := b.TempDir() outputPath := filepath.Join(tmpDir, "report.html") b.ResetTimer() for i := 0; i < b.N; i++ { generateBasicHTML(audit, outputPath) } }