Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 0 additions & 12 deletions internal/config/guard_policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -777,18 +777,6 @@ func ParseGuardPolicyJSON(policyJSON string) (*GuardPolicy, error) {
return policy, nil
}

func validateGuardPolicies(cfg *Config) error {
logGuardPolicy.Printf("Validating guard policies: count=%d", len(cfg.Guards))
for name, guardCfg := range cfg.Guards {
if guardCfg != nil && guardCfg.Policy != nil {
if err := ValidateGuardPolicy(guardCfg.Policy); err != nil {
return fmt.Errorf("invalid policy for guard '%s': %w", name, err)
}
}
}
return nil
}

// NormalizeScopeKind returns a copy of the policy map with the scope_kind field
// normalized to lowercase trimmed string form. Other fields are preserved as-is.
func NormalizeScopeKind(policy map[string]interface{}) map[string]interface{} {
Expand Down
14 changes: 14 additions & 0 deletions internal/config/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -634,3 +634,17 @@ func validateOpenTelemetryConfig(cfg *TracingConfig, enforceHTTPS bool) error {
logValidation.Print("OpenTelemetry config validation passed")
return nil
}

// validateGuardPolicies validates all per-server guard policies in the config.
// It iterates over cfg.Guards and calls ValidateGuardPolicy for each non-nil policy.
func validateGuardPolicies(cfg *Config) error {
logValidation.Printf("Validating guard policies: count=%d", len(cfg.Guards))
for name, guardCfg := range cfg.Guards {
if guardCfg != nil && guardCfg.Policy != nil {
if err := ValidateGuardPolicy(guardCfg.Policy); err != nil {
return fmt.Errorf("invalid policy for guard '%s': %w", name, err)
}
}
}
return nil
}
17 changes: 17 additions & 0 deletions internal/httputil/httputil.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ package httputil
import (
"encoding/json"
"net/http"
"strconv"
"strings"
"time"
)

// WriteJSONResponse sets the Content-Type header, writes the status code, and encodes
Expand All @@ -18,3 +21,17 @@ func WriteJSONResponse(w http.ResponseWriter, statusCode int, body interface{})
}
w.Write(data)
}

// ParseRateLimitResetHeader parses the Unix-timestamp value of the
// X-RateLimit-Reset HTTP header into a time.Time.
// Returns zero time when the header value is absent or malformed.
func ParseRateLimitResetHeader(value string) time.Time {
if value == "" {
return time.Time{}
}
unix, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
if err != nil {
return time.Time{}
}
return time.Unix(unix, 0)
}
60 changes: 60 additions & 0 deletions internal/httputil/httputil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import (
"encoding/json"
"net/http"
"net/http/httptest"
"strconv"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -151,3 +153,61 @@ func TestWriteJSONResponse(t *testing.T) {
assert.Empty(t, rec.Body.String())
})
}

// TestParseRateLimitResetHeader verifies the shared Unix-timestamp header parser.
func TestParseRateLimitResetHeader(t *testing.T) {
t.Parallel()

now := time.Now()
future := now.Add(60 * time.Second)

tests := []struct {
name string
value string
wantZero bool
wantTime time.Time
}{
{
name: "empty",
value: "",
wantZero: true,
},
{
name: "invalid",
value: "not-a-number",
wantZero: true,
},
{
name: "valid unix timestamp",
value: "1000000000",
wantZero: false,
wantTime: time.Unix(1000000000, 0),
},
{
name: "future timestamp",
value: strconv.FormatInt(future.Unix(), 10),
wantZero: false,
},
{
name: "value with surrounding whitespace",
value: " 1000000000 ",
wantZero: false,
wantTime: time.Unix(1000000000, 0),
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
got := ParseRateLimitResetHeader(tt.value)
if tt.wantZero {
assert.True(t, got.IsZero(), "expected zero time")
} else {
assert.False(t, got.IsZero(), "expected non-zero time")
if !tt.wantTime.IsZero() {
assert.Equal(t, tt.wantTime.Unix(), got.Unix())
}
}
})
}
}
9 changes: 2 additions & 7 deletions internal/proxy/graphql.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,24 +190,19 @@ func extractOwnerRepo(variables map[string]interface{}, query string) (string, s
// searchQueryArgPattern extracts the literal query string from search(query:"...", ...)
var searchQueryArgPattern = regexp.MustCompile(`(?i)\bsearch\s*\(\s*query\s*:\s*"([^"]+)"`)

// truncateForLog truncates s to at most maxRunes runes, for safe debug logging.
func truncateForLog(s string, maxRunes int) string {
return strutil.TruncateRunes(s, maxRunes)
}

// extractSearchQuery returns the search query argument from a GraphQL search
// query. It checks variables ($query) first, then inline query text.
func extractSearchQuery(query string, variables map[string]interface{}) string {
// Check variables for $query
if variables != nil {
if v, ok := variables["query"].(string); ok && v != "" {
logGraphQL.Printf("extractSearchQuery: found in variables: %q", truncateForLog(v, 80))
logGraphQL.Printf("extractSearchQuery: found in variables: %q", strutil.TruncateRunes(v, 80))
return v
}
}
// Parse inline: search(query:"repo:owner/name is:issue", ...)
if m := searchQueryArgPattern.FindStringSubmatch(query); m != nil {
logGraphQL.Printf("extractSearchQuery: found inline: %q", truncateForLog(m[1], 80))
logGraphQL.Printf("extractSearchQuery: found inline: %q", strutil.TruncateRunes(m[1], 80))
return m[1]
}
logGraphQL.Print("extractSearchQuery: no search query found")
Expand Down
134 changes: 0 additions & 134 deletions internal/proxy/graphql_test.go

This file was deleted.

15 changes: 1 addition & 14 deletions internal/proxy/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ func injectRetryAfterIfRateLimited(w http.ResponseWriter, resp *http.Response) {
return
}

resetAt := parseRateLimitReset(resetHeader)
resetAt := httputil.ParseRateLimitResetHeader(resetHeader)
retryAfter := computeRetryAfter(resetAt)

w.Header().Set("Retry-After", strconv.Itoa(retryAfter))
Expand All @@ -457,19 +457,6 @@ func injectRetryAfterIfRateLimited(w http.ResponseWriter, resp *http.Response) {
resp.StatusCode, remaining, resetHeader, retryAfter)
}

// parseRateLimitReset parses the X-RateLimit-Reset Unix-timestamp header.
// Returns zero time when absent or malformed.
func parseRateLimitReset(value string) time.Time {
if value == "" {
return time.Time{}
}
unix, err := strconv.ParseInt(value, 10, 64)
if err != nil {
return time.Time{}
}
return time.Unix(unix, 0)
}

// computeRetryAfter returns the number of seconds to wait before retrying.
// When resetAt is in the future the delay is clamped to [1, 3600] seconds.
// When resetAt is zero or in the past a default of 60 seconds is returned.
Expand Down
23 changes: 0 additions & 23 deletions internal/proxy/rate_limit_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,29 +85,6 @@ func TestInjectRetryAfterIfRateLimited(t *testing.T) {
})
}

// TestParseRateLimitReset verifies the Unix-timestamp header parser.
func TestParseRateLimitReset(t *testing.T) {
t.Parallel()

t.Run("empty string returns zero", func(t *testing.T) {
t.Parallel()
assert.True(t, parseRateLimitReset("").IsZero())
})

t.Run("invalid string returns zero", func(t *testing.T) {
t.Parallel()
assert.True(t, parseRateLimitReset("not-a-number").IsZero())
})

t.Run("valid unix timestamp parses correctly", func(t *testing.T) {
t.Parallel()
ts := time.Now().Add(60 * time.Second)
got := parseRateLimitReset(strconv.FormatInt(ts.Unix(), 10))
assert.False(t, got.IsZero())
assert.Equal(t, ts.Unix(), got.Unix())
})
}

// TestComputeRetryAfter verifies the retry-after calculation.
func TestComputeRetryAfter(t *testing.T) {
t.Parallel()
Expand Down
14 changes: 0 additions & 14 deletions internal/server/circuit_breaker.go
Original file line number Diff line number Diff line change
Expand Up @@ -315,17 +315,3 @@ func parseRateLimitResetFromText(text string) time.Time {
}
return time.Now().Add(time.Duration(secs) * time.Second)
}

// parseRateLimitResetHeader parses the Unix-timestamp value of the
// X-RateLimit-Reset HTTP header into a time.Time.
// Returns zero time when the header is absent or malformed.
func parseRateLimitResetHeader(value string) time.Time {
if value == "" {
return time.Time{}
}
unix, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
if err != nil {
return time.Time{}
}
return time.Unix(unix, 0)
}
Loading
Loading