27a9be38bc
1. Refactor err2 to use scoped loadErr variable (MINOR - sonnet-review-bot) The else-if branches are mutually exclusive, so the error variable should be scoped inside the block, not declared outside with err2. 2. Sanitize DisplayName before embedding in Markdown (MINOR - security-review-bot) Remote persona metadata is untrusted. Added sanitizeMarkdownText() to escape Markdown special characters and strip control characters. Applied to both the header title and the footer attribution. 3. Document YAML DoS mitigations (MINOR - security-review-bot) Added comprehensive comment in remote_persona.go explaining existing defenses: file size limit, file count cap, depth limit, node count cap, and alias cycle detection. These collectively mitigate billion-laughs and stack exhaustion attacks.
285 lines
7.5 KiB
Go
285 lines
7.5 KiB
Go
package review
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestFormatMarkdown_EmptyFindings(t *testing.T) {
|
|
result := &ReviewResult{
|
|
Verdict: "APPROVE",
|
|
Summary: "All good, no issues.",
|
|
Findings: []Finding{},
|
|
Recommendation: "Merge this PR.",
|
|
}
|
|
|
|
got := FormatMarkdown(result, "Sonnet")
|
|
|
|
if !strings.Contains(got, "## Summary") {
|
|
t.Error("expected Summary header")
|
|
}
|
|
if !strings.Contains(got, "All good, no issues.") {
|
|
t.Error("expected summary text")
|
|
}
|
|
if strings.Contains(got, "## Findings") {
|
|
t.Error("should not contain Findings header when empty")
|
|
}
|
|
if !strings.Contains(got, "**APPROVE**") {
|
|
t.Error("expected verdict in recommendation")
|
|
}
|
|
if !strings.Contains(got, "Review by Sonnet") {
|
|
t.Error("expected reviewer name")
|
|
}
|
|
}
|
|
|
|
func TestFormatMarkdown_MultipleFindings(t *testing.T) {
|
|
result := &ReviewResult{
|
|
Verdict: "REQUEST_CHANGES",
|
|
Summary: "Several issues found.",
|
|
Findings: []Finding{
|
|
{Severity: "MAJOR", File: "main.go", Line: 42, Finding: "Nil pointer dereference"},
|
|
{Severity: "MINOR", File: "util.go", Line: 7, Finding: "Unused variable"},
|
|
{Severity: "NIT", File: "doc.go", Line: 1, Finding: "Typo in comment"},
|
|
},
|
|
Recommendation: "Fix the nil pointer issue before merging.",
|
|
}
|
|
|
|
got := FormatMarkdown(result, "GPT")
|
|
|
|
if !strings.Contains(got, "## Findings") {
|
|
t.Error("expected Findings header")
|
|
}
|
|
if !strings.Contains(got, "| 1 | [MAJOR] | `main.go` | 42 | Nil pointer dereference |") {
|
|
t.Error("expected first finding row")
|
|
}
|
|
if !strings.Contains(got, "| 2 | [MINOR] | `util.go` | 7 | Unused variable |") {
|
|
t.Error("expected second finding row")
|
|
}
|
|
if !strings.Contains(got, "| 3 | [NIT] | `doc.go` | 1 | Typo in comment |") {
|
|
t.Error("expected third finding row")
|
|
}
|
|
if !strings.Contains(got, "**REQUEST_CHANGES**") {
|
|
t.Error("expected verdict in recommendation")
|
|
}
|
|
}
|
|
|
|
func TestFormatMarkdown_NoReviewerName(t *testing.T) {
|
|
result := &ReviewResult{
|
|
Verdict: "APPROVE",
|
|
Summary: "Fine.",
|
|
Findings: []Finding{},
|
|
Recommendation: "Go ahead.",
|
|
}
|
|
|
|
got := FormatMarkdown(result, "")
|
|
if strings.Contains(got, "Review by") {
|
|
t.Error("should not contain reviewer line when name is empty")
|
|
}
|
|
}
|
|
|
|
func TestFormatMarkdown_SpecialChars(t *testing.T) {
|
|
result := &ReviewResult{
|
|
Verdict: "REQUEST_CHANGES",
|
|
Summary: "Issues with `fmt.Sprintf` usage.",
|
|
Findings: []Finding{
|
|
{Severity: "MAJOR", File: "render.go", Line: 15, Finding: "Use `%v` instead of `%s` for interface{}"},
|
|
},
|
|
Recommendation: "Fix the format verb.",
|
|
}
|
|
|
|
got := FormatMarkdown(result, "Test")
|
|
|
|
// Should contain the backtick content without breaking the table
|
|
if !strings.Contains(got, "`render.go`") {
|
|
t.Error("expected file in backticks")
|
|
}
|
|
if !strings.Contains(got, "Use `%v` instead of `%s` for interface{}") {
|
|
t.Error("expected finding text with backticks preserved")
|
|
}
|
|
}
|
|
|
|
func TestGiteaEvent(t *testing.T) {
|
|
tests := []struct {
|
|
verdict string
|
|
expected string
|
|
}{
|
|
{"APPROVE", "APPROVED"},
|
|
{"REQUEST_CHANGES", "REQUEST_CHANGES"},
|
|
{"UNKNOWN", "COMMENT"},
|
|
{"", "COMMENT"},
|
|
}
|
|
|
|
for _, tc := range tests {
|
|
got := GiteaEvent(tc.verdict)
|
|
if got != tc.expected {
|
|
t.Errorf("GiteaEvent(%q) = %q, want %q", tc.verdict, got, tc.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFormatMarkdown_Sentinel(t *testing.T) {
|
|
result := &ReviewResult{
|
|
Verdict: "APPROVE",
|
|
Summary: "All good.",
|
|
Recommendation: "Merge it.",
|
|
}
|
|
output := FormatMarkdown(result, "security")
|
|
if !strings.Contains(output, "<!-- review-bot:security -->") {
|
|
t.Error("expected sentinel comment in output")
|
|
}
|
|
|
|
// Empty reviewer name should NOT have sentinel
|
|
output2 := FormatMarkdown(result, "")
|
|
if strings.Contains(output2, "<!-- review-bot") {
|
|
t.Error("should not contain sentinel when reviewer name is empty")
|
|
}
|
|
}
|
|
|
|
func TestFormatMarkdown_RoleTitle(t *testing.T) {
|
|
result := &ReviewResult{
|
|
Verdict: "APPROVE",
|
|
Summary: "All good.",
|
|
Recommendation: "Merge it.",
|
|
}
|
|
|
|
// With reviewer name: should have title header
|
|
output := FormatMarkdown(result, "security")
|
|
if !strings.Contains(output, "# Security Review\n") {
|
|
t.Error("expected '# Security Review' header when reviewer name is set")
|
|
}
|
|
|
|
output2 := FormatMarkdown(result, "gpt")
|
|
if !strings.Contains(output2, "# Gpt Review\n") {
|
|
t.Error("expected '# Gpt Review' header")
|
|
}
|
|
|
|
// Without reviewer name: no title header
|
|
output3 := FormatMarkdown(result, "")
|
|
if strings.Contains(output3, "# ") && strings.Contains(output3, " Review\n") {
|
|
t.Error("should not contain role title header when reviewer name is empty")
|
|
}
|
|
}
|
|
|
|
func TestFormatMarkdownWithDisplay(t *testing.T) {
|
|
result := &ReviewResult{
|
|
Verdict: "APPROVE",
|
|
Summary: "Test summary",
|
|
Findings: nil,
|
|
Recommendation: "Test recommendation",
|
|
}
|
|
|
|
t.Run("with display name", func(t *testing.T) {
|
|
body := FormatMarkdownWithDisplay(result, "Security Specialist", "security")
|
|
|
|
// Header should use display name
|
|
if !strings.Contains(body, "# Security Specialist Review") {
|
|
t.Error("header should use display name")
|
|
}
|
|
|
|
// Sentinel should use sentinel name
|
|
if !strings.Contains(body, "<!-- review-bot:security -->") {
|
|
t.Error("sentinel should use sentinel name")
|
|
}
|
|
|
|
// Footer "Review by" should use display name
|
|
if !strings.Contains(body, "*Review by Security Specialist*") {
|
|
t.Error("footer should use display name")
|
|
}
|
|
})
|
|
|
|
t.Run("without display name", func(t *testing.T) {
|
|
body := FormatMarkdownWithDisplay(result, "", "reviewer")
|
|
|
|
// Should fall back to sentinel name for header
|
|
if !strings.Contains(body, "# Reviewer Review") {
|
|
t.Error("header should fall back to sentinel name")
|
|
}
|
|
|
|
if !strings.Contains(body, "<!-- review-bot:reviewer -->") {
|
|
t.Error("sentinel should use sentinel name")
|
|
}
|
|
})
|
|
|
|
t.Run("empty both names", func(t *testing.T) {
|
|
body := FormatMarkdownWithDisplay(result, "", "")
|
|
|
|
// Should not have header
|
|
if strings.Contains(body, "# ") && strings.Contains(body, " Review") {
|
|
t.Error("should not have header when both names empty")
|
|
}
|
|
|
|
// Should not have sentinel
|
|
if strings.Contains(body, "<!-- review-bot:") {
|
|
t.Error("should not have sentinel when sentinel name empty")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestSanitizeMarkdownText(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input string
|
|
want string
|
|
}{
|
|
{
|
|
name: "plain text unchanged",
|
|
input: "Security Specialist",
|
|
want: "Security Specialist",
|
|
},
|
|
{
|
|
name: "escapes asterisks",
|
|
input: "**bold** attack",
|
|
want: `\*\*bold\*\* attack`,
|
|
},
|
|
{
|
|
name: "escapes brackets for links",
|
|
input: "[click me](http://evil.com)",
|
|
want: `\[click me\]\(http://evil.com\)`,
|
|
},
|
|
{
|
|
name: "escapes backticks",
|
|
input: "`code` injection",
|
|
want: "\\`code\\` injection",
|
|
},
|
|
{
|
|
name: "escapes angle brackets",
|
|
input: "<script>alert(1)</script>",
|
|
want: `\<script\>alert\(1\)\</script\>`,
|
|
},
|
|
{
|
|
name: "escapes hash for headers",
|
|
input: "# Fake Header",
|
|
want: `\# Fake Header`,
|
|
},
|
|
{
|
|
name: "escapes pipe for tables",
|
|
input: "col1 | col2",
|
|
want: `col1 \| col2`,
|
|
},
|
|
{
|
|
name: "removes control characters",
|
|
input: "hello\x00world\x1f",
|
|
want: "helloworld",
|
|
},
|
|
{
|
|
name: "preserves tabs and newlines",
|
|
input: "line1\n\tindented",
|
|
want: "line1\n\tindented",
|
|
},
|
|
{
|
|
name: "escapes tilde for strikethrough",
|
|
input: "~~strikethrough~~",
|
|
want: `\~\~strikethrough\~\~`,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
got := sanitizeMarkdownText(tt.input)
|
|
if got != tt.want {
|
|
t.Errorf("sanitizeMarkdownText(%q) = %q, want %q", tt.input, got, tt.want)
|
|
}
|
|
})
|
|
}
|
|
}
|