feat(gitea): harden GetPullRequestDiff against unbounded diff size
CI / test (pull_request) Successful in 23s
CI / review (anthropic--claude-4.6-sonnet, sonnet, SONNET_REVIEW_TOKEN) (pull_request) Successful in 31s
CI / review (gpt-5, security, ., rodin/security-patterns, SECURITY_REVIEW.md, SECURITY_REVIEW_TOKEN) (pull_request) Successful in 1m16s
CI / review (gpt-5, gpt, GPT_REVIEW_TOKEN) (pull_request) Successful in 1m38s

Add a configurable MaxDiffSize field to Client that limits how much
data GetPullRequestDiff will read into memory. The default is 10 MB
(DefaultMaxDiffSize). When the diff exceeds the limit, ErrDiffTooLarge
is returned, allowing callers to skip position translation gracefully.

Implementation uses io.LimitReader to read maxBytes+1, detecting
overflow without buffering the entire response. Setting MaxDiffSize
to -1 disables the limit entirely.

Closes #92
This commit is contained in:
claw
2026-05-13 04:57:30 -07:00
parent d722035629
commit 235828ec42
2 changed files with 251 additions and 1 deletions
+143
View File
@@ -0,0 +1,143 @@
package gitea
import (
"context"
"errors"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestGetPullRequestDiff_ExceedsMaxSize(t *testing.T) {
// Create a diff that exceeds a small limit
largeDiff := strings.Repeat("+ added line\n", 1000) // ~13 KB
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(largeDiff))
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
client.MaxDiffSize = 100 // 100 bytes limit
client.RetryBackoff = []time.Duration{} // no delay in tests
_, err := client.GetPullRequestDiff(context.Background(), "owner", "repo", 1)
if err == nil {
t.Fatal("expected error for oversized diff, got nil")
}
if !errors.Is(err, ErrDiffTooLarge) {
t.Errorf("expected ErrDiffTooLarge, got: %v", err)
}
}
func TestGetPullRequestDiff_WithinMaxSize(t *testing.T) {
smallDiff := "diff --git a/f.go b/f.go\n--- a/f.go\n+++ b/f.go\n@@ -1 +1 @@\n-old\n+new\n"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(smallDiff))
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
client.MaxDiffSize = 1024 // 1 KB limit — more than enough
client.RetryBackoff = []time.Duration{}
got, err := client.GetPullRequestDiff(context.Background(), "owner", "repo", 1)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got != smallDiff {
t.Errorf("expected diff %q, got %q", smallDiff, got)
}
}
func TestGetPullRequestDiff_ExactlyAtLimit(t *testing.T) {
// A diff that is exactly at the limit should succeed
exactDiff := strings.Repeat("x", 50)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(exactDiff))
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
client.MaxDiffSize = 50 // exactly the size of the diff
client.RetryBackoff = []time.Duration{}
got, err := client.GetPullRequestDiff(context.Background(), "owner", "repo", 1)
if err != nil {
t.Fatalf("unexpected error for diff at exact limit: %v", err)
}
if got != exactDiff {
t.Errorf("expected diff to match, got length %d", len(got))
}
}
func TestGetPullRequestDiff_OneByteOverLimit(t *testing.T) {
// A diff that is one byte over the limit should fail
overDiff := strings.Repeat("x", 51)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(overDiff))
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
client.MaxDiffSize = 50
client.RetryBackoff = []time.Duration{}
_, err := client.GetPullRequestDiff(context.Background(), "owner", "repo", 1)
if err == nil {
t.Fatal("expected error for diff one byte over limit")
}
if !errors.Is(err, ErrDiffTooLarge) {
t.Errorf("expected ErrDiffTooLarge, got: %v", err)
}
}
func TestGetPullRequestDiff_DisabledLimit(t *testing.T) {
// When MaxDiffSize is -1, no limit is enforced
largeDiff := strings.Repeat("x", 10000)
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(largeDiff))
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
client.MaxDiffSize = -1 // disabled
client.RetryBackoff = []time.Duration{}
got, err := client.GetPullRequestDiff(context.Background(), "owner", "repo", 1)
if err != nil {
t.Fatalf("unexpected error with disabled limit: %v", err)
}
if got != largeDiff {
t.Errorf("expected full diff with disabled limit, got length %d", len(got))
}
}
func TestGetPullRequestDiff_DefaultLimit(t *testing.T) {
// With zero MaxDiffSize (default), should use DefaultMaxDiffSize.
// A small diff should succeed without setting MaxDiffSize.
smallDiff := "diff content"
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(smallDiff))
}))
defer server.Close()
client := NewClient(server.URL, "test-token")
// MaxDiffSize is zero (default) — should use DefaultMaxDiffSize (10 MB)
client.RetryBackoff = []time.Duration{}
got, err := client.GetPullRequestDiff(context.Background(), "owner", "repo", 1)
if err != nil {
t.Fatalf("unexpected error with default limit: %v", err)
}
if got != smallDiff {
t.Errorf("expected diff %q, got %q", smallDiff, got)
}
}