Skip to content

Commit cbb0d6d

Browse files
author
merge-queue-bot
committed
Merge PR #627: Add per-function unit tests for secreview/report.go
2 parents 41f9b39 + 3b1619b commit cbb0d6d

4 files changed

Lines changed: 374 additions & 14 deletions

File tree

PLAN.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,8 +198,8 @@ footer: |
198198
| 2606141902 | 🔲 | opus | [Lazy parse: Layer 0 block scanner and parse-skip](plan/2606141902_lazy-parse-layer0.md) |
199199
| 2606141903 | 🔲 | opus | [Lazy parse: BlockSpan seam for block NodeCheckers](plan/2606141903_lazy-parse-blockspan-nodecheckers.md) |
200200
| 2606141904 | 🔲 | opus | [Lazy parse: Layer 1 light inline index](plan/2606141904_lazy-parse-inline-index.md) |
201-
| 2606141910 | 🔲 | sonnet | [Extract build path helpers out of internal/rules/build](plan/2606141910_arch-fix-build-rules-dip.md) |
201+
| 2606141910 | | sonnet | [Extract build path helpers out of internal/rules/build](plan/2606141910_arch-fix-build-rules-dip.md) |
202202
| 2606141911 || haiku | [Remove deprecated engine wrappers for checker and lint](plan/2606141911_arch-fix-engine-deprecated-wrappers.md) |
203-
| 2606141912 | 🔲 | haiku | [Add per-function unit tests for secreview/report.go](plan/2606141912_arch-fix-secreview-report-tests.md) |
203+
| 2606141912 | | haiku | [Add per-function unit tests for secreview/report.go](plan/2606141912_arch-fix-secreview-report-tests.md) |
204204
| 2606142147 | 🔲 | opus | [Prototype: flat Layer-0 line classifier for line-length vs gomarklint](plan/2606142147_flat-layer0-line-classifier.md) |
205205
<?/catalog?>

internal/secreview/report_test.go

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
package secreview
2+
3+
import (
4+
"strings"
5+
"testing"
6+
"time"
7+
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
// --- orQuestion ---
13+
14+
func TestOrQuestion(t *testing.T) {
15+
assert.Equal(t, "hello", orQuestion("hello"))
16+
assert.Equal(t, "?", orQuestion(""))
17+
}
18+
19+
// --- buildReport ---
20+
21+
func TestBuildReport(t *testing.T) {
22+
r := &Report{
23+
Target: Target{Mode: "audit", Repo: "jeduden/mdsmith", Ref: "abc123", Scope: "cli"},
24+
Coverage: "All code paths reviewed.",
25+
Findings: []Finding{
26+
{ID: "S001", Title: "cmd injection", Severity: "high", Confidence: "confirmed", Surface: "cli"},
27+
{ID: "S002", Title: "info note", Severity: "info", Confidence: "likely", Surface: "lsp"},
28+
},
29+
}
30+
now := time.Date(2026, 6, 14, 0, 0, 0, 0, time.UTC)
31+
got := buildReport(r, now)
32+
assert.Contains(t, got, "# mdsmith Security Review")
33+
assert.Contains(t, got, "jeduden/mdsmith")
34+
assert.Contains(t, got, "abc123")
35+
assert.Contains(t, got, "2026-06-14")
36+
assert.Contains(t, got, "cmd injection")
37+
assert.Contains(t, got, "info note")
38+
assert.Contains(t, got, "All code paths reviewed.")
39+
// high-severity finding must appear before info-severity (sortedFindings ordering).
40+
assert.Less(t, strings.Index(got, "cmd injection"), strings.Index(got, "info note"))
41+
}
42+
43+
func TestBuildReport_Empty(t *testing.T) {
44+
r := &Report{}
45+
now := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
46+
got := buildReport(r, now)
47+
assert.Contains(t, got, "# mdsmith Security Review")
48+
assert.Contains(t, got, coveragePlaceholder)
49+
}
50+
51+
// --- writeHeader ---
52+
53+
func TestWriteHeader(t *testing.T) {
54+
var b strings.Builder
55+
target := &Target{Repo: "jeduden/mdsmith", Ref: "deadbeef", Mode: "pr", Scope: "cli + lsp"}
56+
now := time.Date(2026, 6, 14, 0, 0, 0, 0, time.UTC)
57+
writeHeader(&b, target, now)
58+
got := b.String()
59+
assert.Contains(t, got, "# mdsmith Security Review")
60+
assert.Contains(t, got, "jeduden/mdsmith")
61+
assert.Contains(t, got, "`deadbeef`")
62+
assert.Contains(t, got, "pr")
63+
assert.Contains(t, got, "cli + lsp")
64+
assert.Contains(t, got, "2026-06-14")
65+
}
66+
67+
func TestWriteHeader_EmptyTarget(t *testing.T) {
68+
var b strings.Builder
69+
writeHeader(&b, &Target{}, time.Time{})
70+
got := b.String()
71+
assert.Contains(t, got, "?")
72+
}
73+
74+
// --- writeSummary ---
75+
76+
func TestWriteSummary(t *testing.T) {
77+
findings := []Finding{
78+
{ID: "S001", Title: "critical bug", Severity: "critical", Confidence: "confirmed", Surface: "cli",
79+
Location: &Location{File: "a.go", StartLine: 10}},
80+
{ID: "S002", Title: "medium thing", Severity: "medium", Confidence: "likely", Surface: "lsp"},
81+
}
82+
var b strings.Builder
83+
writeSummary(&b, findings)
84+
got := b.String()
85+
assert.Contains(t, got, "## Summary")
86+
assert.Contains(t, got, "Critical: 1 |")
87+
assert.Contains(t, got, "Medium: 1 |")
88+
assert.Contains(t, got, "S001")
89+
assert.Contains(t, got, "critical bug")
90+
assert.Contains(t, got, "medium thing")
91+
}
92+
93+
func TestWriteSummary_Empty(t *testing.T) {
94+
var b strings.Builder
95+
writeSummary(&b, nil)
96+
got := b.String()
97+
assert.Contains(t, got, "## Summary")
98+
assert.Contains(t, got, "Critical: 0 |")
99+
}
100+
101+
// --- tableCell ---
102+
103+
func TestTableCell(t *testing.T) {
104+
assert.Equal(t, "plain", tableCell("plain"))
105+
}
106+
107+
func TestTableCell_Pipe(t *testing.T) {
108+
assert.Equal(t, `a\|b`, tableCell("a|b"))
109+
}
110+
111+
func TestTableCell_Backslash(t *testing.T) {
112+
assert.Equal(t, `a\\b`, tableCell(`a\b`))
113+
}
114+
115+
func TestTableCell_BackslashPipe(t *testing.T) {
116+
// A literal \| input must not produce a live pipe in the output.
117+
// The backslash is doubled first, then the pipe is escaped.
118+
assert.Equal(t, `a\\\|b`, tableCell(`a\|b`))
119+
}
120+
121+
func TestTableCell_Newline(t *testing.T) {
122+
assert.Equal(t, "a b", tableCell("a\nb"))
123+
assert.Equal(t, "a b", tableCell("a\rb"))
124+
}
125+
126+
// --- severityCounts ---
127+
128+
func TestSeverityCounts(t *testing.T) {
129+
findings := []Finding{
130+
{Severity: "critical"},
131+
{Severity: "critical"},
132+
{Severity: "high"},
133+
{Severity: "info"},
134+
}
135+
got := severityCounts(findings)
136+
assert.Contains(t, got, "Critical: 2 |")
137+
assert.Contains(t, got, "High: 1 |")
138+
assert.Contains(t, got, "Medium: 0 |")
139+
assert.Contains(t, got, "Low: 0 |")
140+
assert.Contains(t, got, "Info: 1")
141+
}
142+
143+
func TestSeverityCounts_Empty(t *testing.T) {
144+
got := severityCounts(nil)
145+
assert.Contains(t, got, "Critical: 0 |")
146+
assert.Contains(t, got, "Info: 0")
147+
}
148+
149+
// --- writeFindingSections ---
150+
151+
func TestWriteFindingSections(t *testing.T) {
152+
findings := []Finding{
153+
{ID: "S001", Title: "exec sink", Severity: "high", Confidence: "confirmed"},
154+
{ID: "S002", Title: "hardening note", Severity: "info", Confidence: "likely"},
155+
}
156+
var b strings.Builder
157+
writeFindingSections(&b, findings)
158+
got := b.String()
159+
assert.Contains(t, got, "## Findings")
160+
assert.Contains(t, got, "exec sink")
161+
assert.Contains(t, got, "## Hardening / Informational")
162+
assert.Contains(t, got, "hardening note")
163+
}
164+
165+
func TestWriteFindingSections_NoReal(t *testing.T) {
166+
findings := []Finding{
167+
{ID: "S001", Title: "note", Severity: "info"},
168+
}
169+
var b strings.Builder
170+
writeFindingSections(&b, findings)
171+
got := b.String()
172+
assert.NotContains(t, got, "## Findings")
173+
assert.Contains(t, got, "## Hardening / Informational")
174+
}
175+
176+
func TestWriteFindingSections_Empty(t *testing.T) {
177+
var b strings.Builder
178+
writeFindingSections(&b, nil)
179+
assert.Empty(t, b.String())
180+
}
181+
182+
// --- renderFinding ---
183+
184+
func TestRenderFinding(t *testing.T) {
185+
f := &Finding{
186+
ID: "S001",
187+
Title: "exec injection",
188+
Severity: "critical",
189+
Confidence: "confirmed",
190+
Surface: "cli",
191+
CWE: "CWE-78",
192+
Location: &Location{File: "cmd/main.go", StartLine: 10},
193+
Description: "shell args passed unsanitized",
194+
Impact: "RCE",
195+
Repro: "mdsmith fix $(malicious)",
196+
Remediation: "use argv not sh -c",
197+
}
198+
got := renderFinding(f)
199+
assert.Contains(t, got, "### S001 · exec injection")
200+
assert.Contains(t, got, "**Severity:** critical")
201+
assert.Contains(t, got, "**CWE-78**")
202+
assert.Contains(t, got, "cmd/main.go:10")
203+
assert.Contains(t, got, "**What.** shell args passed unsanitized")
204+
assert.Contains(t, got, "**Impact.** RCE")
205+
assert.Contains(t, got, "**Repro (sketch).** mdsmith fix $(malicious)")
206+
assert.Contains(t, got, "**Fix.** use argv not sh -c")
207+
}
208+
209+
func TestRenderFinding_NoCWE(t *testing.T) {
210+
f := &Finding{ID: "S001", Title: "t", Severity: "low", Confidence: "likely"}
211+
got := renderFinding(f)
212+
assert.NotContains(t, got, "CWE")
213+
}
214+
215+
func TestRenderFinding_RelatedLocations(t *testing.T) {
216+
f := &Finding{
217+
ID: "S001",
218+
Title: "t",
219+
Severity: "medium",
220+
RelatedLocations: []Location{{File: "x.go", StartLine: 5}},
221+
}
222+
got := renderFinding(f)
223+
assert.Contains(t, got, "- related: `x.go:5`")
224+
}
225+
226+
// --- writeFindingProse ---
227+
228+
func TestWriteFindingProse(t *testing.T) {
229+
var b strings.Builder
230+
f := &Finding{
231+
Description: "desc",
232+
Impact: "impact",
233+
Repro: "repro",
234+
Remediation: "fix",
235+
}
236+
writeFindingProse(&b, f)
237+
got := b.String()
238+
assert.Contains(t, got, "**What.** desc")
239+
assert.Contains(t, got, "**Impact.** impact")
240+
assert.Contains(t, got, "**Repro (sketch).** repro")
241+
assert.Contains(t, got, "**Fix.** fix")
242+
}
243+
244+
func TestWriteFindingProse_AllEmpty(t *testing.T) {
245+
var b strings.Builder
246+
writeFindingProse(&b, &Finding{})
247+
assert.Empty(t, b.String())
248+
}
249+
250+
func TestWriteFindingProse_PartialFields(t *testing.T) {
251+
var b strings.Builder
252+
writeFindingProse(&b, &Finding{Description: "only desc"})
253+
got := b.String()
254+
assert.Contains(t, got, "**What.**")
255+
assert.NotContains(t, got, "**Impact.**")
256+
assert.NotContains(t, got, "**Fix.**")
257+
}
258+
259+
// --- writeCoverage ---
260+
261+
func TestWriteCoverage(t *testing.T) {
262+
var b strings.Builder
263+
writeCoverage(&b, "Reviewed all LSP paths.")
264+
got := b.String()
265+
assert.Contains(t, got, "## Coverage")
266+
assert.Contains(t, got, "Reviewed all LSP paths.")
267+
}
268+
269+
func TestWriteCoverage_Empty(t *testing.T) {
270+
var b strings.Builder
271+
writeCoverage(&b, "")
272+
got := b.String()
273+
assert.Contains(t, got, "## Coverage")
274+
assert.Contains(t, got, coveragePlaceholder)
275+
}
276+
277+
// --- capitalize ---
278+
279+
func TestCapitalize(t *testing.T) {
280+
assert.Equal(t, "Critical", capitalize("critical"))
281+
assert.Equal(t, "High", capitalize("high"))
282+
assert.Equal(t, "Info", capitalize("info"))
283+
assert.Equal(t, "A", capitalize("a"))
284+
}
285+
286+
func TestCapitalize_Empty(t *testing.T) {
287+
assert.Equal(t, "", capitalize(""))
288+
}
289+
290+
func TestCapitalize_AlreadyUpper(t *testing.T) {
291+
assert.Equal(t, "Critical", capitalize("Critical"))
292+
}
293+
294+
// --- buildAnnotations ---
295+
296+
func TestBuildAnnotations(t *testing.T) {
297+
r := &Report{
298+
Findings: []Finding{
299+
{ID: "S001", Title: "exec sink", Severity: "critical", Confidence: "confirmed",
300+
Location: &Location{File: "cmd/main.go", StartLine: 10}},
301+
{ID: "S002", Title: "no location", Severity: "high"},
302+
},
303+
}
304+
anns := buildAnnotations(r)
305+
require.Len(t, anns, 1)
306+
assert.Equal(t, "cmd/main.go", anns[0].Path)
307+
assert.Equal(t, 10, anns[0].Line)
308+
assert.Equal(t, "RIGHT", anns[0].Side)
309+
assert.Equal(t, "critical", anns[0].Severity)
310+
assert.Equal(t, "S001", anns[0].ID)
311+
assert.Contains(t, anns[0].Body, "S001")
312+
}
313+
314+
func TestBuildAnnotations_ZeroStartLine(t *testing.T) {
315+
r := &Report{
316+
Findings: []Finding{
317+
{ID: "S001", Title: "no line", Severity: "high",
318+
Location: &Location{File: "a.go", StartLine: 0}},
319+
},
320+
}
321+
anns := buildAnnotations(r)
322+
assert.Empty(t, anns)
323+
}
324+
325+
func TestBuildAnnotations_NilLocation(t *testing.T) {
326+
r := &Report{
327+
Findings: []Finding{
328+
{ID: "S001", Title: "no loc", Severity: "medium"},
329+
},
330+
}
331+
anns := buildAnnotations(r)
332+
assert.Empty(t, anns)
333+
}
334+
335+
func TestBuildAnnotations_Empty(t *testing.T) {
336+
assert.Empty(t, buildAnnotations(&Report{}))
337+
}
338+
339+
// --- annotationBody ---
340+
341+
func TestAnnotationBody(t *testing.T) {
342+
f := &Finding{ID: "S001", Title: "exec sink", Severity: "critical",
343+
Description: "shell args not sanitized", Remediation: "use argv"}
344+
got := annotationBody(f)
345+
assert.Contains(t, got, "**[S001 · critical] exec sink**")
346+
assert.Contains(t, got, "shell args not sanitized")
347+
assert.Contains(t, got, "**Fix:** use argv")
348+
}
349+
350+
func TestAnnotationBody_NoDescription(t *testing.T) {
351+
f := &Finding{ID: "S001", Title: "t", Severity: "low"}
352+
got := annotationBody(f)
353+
assert.Equal(t, "**[S001 · low] t**\n\n**Fix:** n/a", got)
354+
}
355+
356+
func TestAnnotationBody_NoRemediation(t *testing.T) {
357+
f := &Finding{ID: "S001", Title: "t", Severity: "low", Description: "desc"}
358+
got := annotationBody(f)
359+
assert.Equal(t, "**[S001 · low] t**\n\ndesc\n\n**Fix:** n/a", got)
360+
}

0 commit comments

Comments
 (0)