Replace fmt/strings/path/filepath with core.Sprintf, core.Lower, core.Upper, core.Split, core.SplitN, core.Trim, core.Join, core.Contains, core.NewBuilder, core.Path across ai/, cmd/metrics/, and cmd/security/. Add Bad/Ugly test coverage for the untested security package (AlertSummary, filterBySeverity, buildTargetRepo, buildJobIssueBody). os/exec kept for gh CLI invocations (no core equivalent in dep graph). encoding/json kept for MarshalIndent (no core.JSONMarshalIndent yet). Co-Authored-By: Virgil <virgil@lethean.io>
273 lines
8 KiB
Go
273 lines
8 KiB
Go
package security
|
|
|
|
import (
|
|
"testing"
|
|
|
|
core "dappco.re/go/core"
|
|
)
|
|
|
|
// TestAlertSummary_Add_Good verifies that known severity levels increment the correct counter.
|
|
func TestAlertSummary_Add_Good(t *testing.T) {
|
|
summary := &AlertSummary{}
|
|
|
|
summary.Add("critical")
|
|
summary.Add("high")
|
|
summary.Add("medium")
|
|
summary.Add("low")
|
|
|
|
if summary.Critical != 1 {
|
|
t.Errorf("Critical: want 1, got %d", summary.Critical)
|
|
}
|
|
if summary.High != 1 {
|
|
t.Errorf("High: want 1, got %d", summary.High)
|
|
}
|
|
if summary.Medium != 1 {
|
|
t.Errorf("Medium: want 1, got %d", summary.Medium)
|
|
}
|
|
if summary.Low != 1 {
|
|
t.Errorf("Low: want 1, got %d", summary.Low)
|
|
}
|
|
if summary.Total != 4 {
|
|
t.Errorf("Total: want 4, got %d", summary.Total)
|
|
}
|
|
}
|
|
|
|
// TestAlertSummary_Add_Bad verifies that an unrecognised severity goes to the Unknown bucket.
|
|
func TestAlertSummary_Add_Bad(t *testing.T) {
|
|
summary := &AlertSummary{}
|
|
|
|
summary.Add("informational")
|
|
summary.Add("WEIRD")
|
|
|
|
if summary.Unknown != 2 {
|
|
t.Errorf("Unknown: want 2, got %d", summary.Unknown)
|
|
}
|
|
if summary.Total != 2 {
|
|
t.Errorf("Total: want 2, got %d", summary.Total)
|
|
}
|
|
if summary.Critical != 0 || summary.High != 0 {
|
|
t.Errorf("Critical/High should remain 0 for unknown severities")
|
|
}
|
|
}
|
|
|
|
// TestAlertSummary_Add_Ugly verifies case-insensitive matching — "CRITICAL" and "Critical" both hit Critical.
|
|
func TestAlertSummary_Add_Ugly(t *testing.T) {
|
|
summary := &AlertSummary{}
|
|
|
|
summary.Add("CRITICAL")
|
|
summary.Add("Critical")
|
|
summary.Add("HIGH")
|
|
summary.Add("Medium")
|
|
summary.Add("LOW")
|
|
|
|
if summary.Critical != 2 {
|
|
t.Errorf("Critical: want 2, got %d", summary.Critical)
|
|
}
|
|
if summary.High != 1 {
|
|
t.Errorf("High: want 1, got %d", summary.High)
|
|
}
|
|
if summary.Medium != 1 {
|
|
t.Errorf("Medium: want 1, got %d", summary.Medium)
|
|
}
|
|
if summary.Low != 1 {
|
|
t.Errorf("Low: want 1, got %d", summary.Low)
|
|
}
|
|
if summary.Total != 5 {
|
|
t.Errorf("Total: want 5, got %d", summary.Total)
|
|
}
|
|
}
|
|
|
|
// TestAlertSummaryString_Good verifies that String() renders all non-zero counts.
|
|
func TestAlertSummaryString_Good(t *testing.T) {
|
|
summary := &AlertSummary{Critical: 1, High: 2, Total: 3}
|
|
result := summary.String()
|
|
|
|
if !core.Contains(result, "1 critical") {
|
|
t.Errorf("expected '1 critical' in %q", result)
|
|
}
|
|
if !core.Contains(result, "2 high") {
|
|
t.Errorf("expected '2 high' in %q", result)
|
|
}
|
|
if !core.Contains(result, "|") {
|
|
t.Errorf("expected separator '|' between segments in %q", result)
|
|
}
|
|
}
|
|
|
|
// TestAlertSummaryString_Bad verifies that an empty AlertSummary returns the "No alerts" message.
|
|
func TestAlertSummaryString_Bad(t *testing.T) {
|
|
summary := &AlertSummary{}
|
|
result := summary.String()
|
|
|
|
if !core.Contains(result, "No alerts") {
|
|
t.Errorf("expected 'No alerts' for empty summary, got %q", result)
|
|
}
|
|
}
|
|
|
|
// TestAlertSummaryString_Ugly verifies that zero-count severity levels are omitted from the string.
|
|
func TestAlertSummaryString_Ugly(t *testing.T) {
|
|
// Only medium is set — critical/high/low/unknown should not appear.
|
|
summary := &AlertSummary{Medium: 3, Total: 3}
|
|
result := summary.String()
|
|
|
|
if core.Contains(result, "critical") {
|
|
t.Errorf("'critical' should be absent when count is 0, got %q", result)
|
|
}
|
|
if core.Contains(result, "high") {
|
|
t.Errorf("'high' should be absent when count is 0, got %q", result)
|
|
}
|
|
if !core.Contains(result, "3 medium") {
|
|
t.Errorf("expected '3 medium' in %q", result)
|
|
}
|
|
}
|
|
|
|
// TestFilterBySeverity_Good verifies that matching entries pass the filter.
|
|
func TestFilterBySeverity_Good(t *testing.T) {
|
|
cases := []struct {
|
|
severity string
|
|
filter string
|
|
}{
|
|
{"critical", "critical,high"},
|
|
{"high", "critical,high"},
|
|
{"medium", "medium"},
|
|
{"low", "low,medium,high"},
|
|
// Empty filter passes everything.
|
|
{"critical", ""},
|
|
{"anything", ""},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
if !filterBySeverity(tc.severity, tc.filter) {
|
|
t.Errorf("filterBySeverity(%q, %q): want true, got false", tc.severity, tc.filter)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFilterBySeverity_Bad verifies that non-matching severities are excluded.
|
|
func TestFilterBySeverity_Bad(t *testing.T) {
|
|
cases := []struct {
|
|
severity string
|
|
filter string
|
|
}{
|
|
{"low", "critical,high"},
|
|
{"medium", "critical"},
|
|
{"informational", "critical,high,medium,low"},
|
|
}
|
|
|
|
for _, tc := range cases {
|
|
if filterBySeverity(tc.severity, tc.filter) {
|
|
t.Errorf("filterBySeverity(%q, %q): want false, got true", tc.severity, tc.filter)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFilterBySeverity_Ugly verifies case-insensitive matching and whitespace tolerance.
|
|
func TestFilterBySeverity_Ugly(t *testing.T) {
|
|
// Severity and filter are uppercased — must still match.
|
|
if !filterBySeverity("CRITICAL", "CRITICAL,HIGH") {
|
|
t.Error("filterBySeverity should be case-insensitive for severity")
|
|
}
|
|
if !filterBySeverity("critical", "CRITICAL") {
|
|
t.Error("filterBySeverity should be case-insensitive for filter")
|
|
}
|
|
// Filter entries may have leading/trailing whitespace.
|
|
if !filterBySeverity("high", " high , critical ") {
|
|
t.Error("filterBySeverity should trim whitespace from filter entries")
|
|
}
|
|
}
|
|
|
|
// TestBuildTargetRepo_Good verifies that a valid owner/repo string parses correctly.
|
|
func TestBuildTargetRepo_Good(t *testing.T) {
|
|
repo, fullName := buildTargetRepo("wailsapp/wails")
|
|
if repo == nil {
|
|
t.Fatal("expected non-nil repo")
|
|
}
|
|
if repo.Name != "wails" {
|
|
t.Errorf("Name: want 'wails', got %q", repo.Name)
|
|
}
|
|
if fullName != "wailsapp/wails" {
|
|
t.Errorf("fullName: want 'wailsapp/wails', got %q", fullName)
|
|
}
|
|
}
|
|
|
|
// TestBuildTargetRepo_Bad verifies that malformed targets return nil.
|
|
func TestBuildTargetRepo_Bad(t *testing.T) {
|
|
badTargets := []string{
|
|
"", // empty
|
|
"noslash", // missing separator
|
|
"/repo", // empty owner
|
|
"owner/", // empty repo
|
|
"//double", // double slash
|
|
}
|
|
|
|
for _, target := range badTargets {
|
|
repo, fullName := buildTargetRepo(target)
|
|
if repo != nil || fullName != "" {
|
|
t.Errorf("buildTargetRepo(%q): expected (nil, ''), got (%v, %q)", target, repo, fullName)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBuildTargetRepo_Ugly verifies targets with extra path segments — only the first two parts matter.
|
|
func TestBuildTargetRepo_Ugly(t *testing.T) {
|
|
// SplitN(n=2) means "owner/repo/extra" → ["owner", "repo/extra"]
|
|
// repo name becomes "repo/extra" which is non-empty, so it parses.
|
|
repo, fullName := buildTargetRepo("owner/repo/extra")
|
|
if repo == nil {
|
|
t.Fatal("expected non-nil repo for 3-segment path")
|
|
}
|
|
if repo.Name != "repo/extra" {
|
|
t.Errorf("Name: want 'repo/extra', got %q", repo.Name)
|
|
}
|
|
if fullName != "owner/repo/extra" {
|
|
t.Errorf("fullName: want 'owner/repo/extra', got %q", fullName)
|
|
}
|
|
}
|
|
|
|
// TestBuildJobIssueBody_Good verifies the issue body contains all required sections.
|
|
func TestBuildJobIssueBody_Good(t *testing.T) {
|
|
summary := &AlertSummary{Critical: 1, High: 2, Total: 3}
|
|
findings := []string{"- [CRITICAL] tool: description (file.go:10)"}
|
|
|
|
body := buildJobIssueBody("wailsapp/wails", summary, findings)
|
|
|
|
requiredSections := []string{
|
|
"## Security Scan: wailsapp/wails",
|
|
"### Findings",
|
|
"### Checklist",
|
|
"### Instructions",
|
|
"wailsapp/wails",
|
|
}
|
|
|
|
for _, section := range requiredSections {
|
|
if !core.Contains(body, section) {
|
|
t.Errorf("body missing section %q", section)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBuildJobIssueBody_Bad verifies that more than 50 findings are truncated with a count line.
|
|
func TestBuildJobIssueBody_Bad(t *testing.T) {
|
|
summary := &AlertSummary{Total: 60}
|
|
findings := make([]string, 60)
|
|
for i := range 60 {
|
|
findings[i] = "- [HIGH] finding"
|
|
}
|
|
|
|
body := buildJobIssueBody("org/repo", summary, findings)
|
|
|
|
if !core.Contains(body, "... and 10 more") {
|
|
t.Errorf("expected truncation message '... and 10 more' in body, got:\n%s", body)
|
|
}
|
|
}
|
|
|
|
// TestBuildJobIssueBody_Ugly verifies that empty findings still produces a valid body with all sections.
|
|
func TestBuildJobIssueBody_Ugly(t *testing.T) {
|
|
summary := &AlertSummary{}
|
|
body := buildJobIssueBody("org/empty-repo", summary, nil)
|
|
|
|
for _, section := range []string{"### Findings", "### Checklist", "### Instructions"} {
|
|
if !core.Contains(body, section) {
|
|
t.Errorf("empty findings body missing section %q", section)
|
|
}
|
|
}
|
|
}
|