go-ai/cmd/security/cmd_security_test.go
Snider aff8ff4b3b
Some checks failed
Security Scan / security (push) Successful in 10s
Test / test (push) Failing after 35s
refactor(ax): round 7 AX sweep — replace banned imports with core primitives, add security tests
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>
2026-03-31 14:40:57 +01:00

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