From fc6173bcc96ed895b118326cb486545d449a2af6 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Wed, 1 Oct 2025 13:52:43 +0530 Subject: [PATCH 01/42] api rate limit --- internal/wrappers/azure-http.go | 7 +- internal/wrappers/bitbucket-http.go | 14 +- .../bitbucketserver/bitbucket-server-http.go | 7 +- internal/wrappers/github-http.go | 7 +- internal/wrappers/gitlab-http.go | 7 +- internal/wrappers/rate_limit.go | 173 ++++++++++++++++++ 6 files changed, 209 insertions(+), 6 deletions(-) create mode 100644 internal/wrappers/rate_limit.go diff --git a/internal/wrappers/azure-http.go b/internal/wrappers/azure-http.go index 91409ba17..8ce088218 100644 --- a/internal/wrappers/azure-http.go +++ b/internal/wrappers/azure-http.go @@ -108,7 +108,12 @@ func (g *AzureHTTPWrapper) get( queryParams map[string]string, authFormat string, ) (bool, error) { - resp, err := GetWithQueryParams(g.client, url, token, authFormat, queryParams) + resp, err := WithSCMRateLimitRetry( + AzureRateLimitConfig, + func() (*http.Response, error) { + return GetWithQueryParams(g.client, url, token, authFormat, queryParams) + }, + ) if err != nil { return false, err } diff --git a/internal/wrappers/bitbucket-http.go b/internal/wrappers/bitbucket-http.go index 8be2abf34..2a6e3af85 100644 --- a/internal/wrappers/bitbucket-http.go +++ b/internal/wrappers/bitbucket-http.go @@ -150,7 +150,12 @@ func (g *BitBucketHTTPWrapper) getFromBitBucket( logger.PrintIfVerbose(fmt.Sprintf("Request to %s", url)) - resp, err := GetWithQueryParams(g.client, url, token, basicFormat, queryParams) + resp, err := WithSCMRateLimitRetry( + BitbucketRateLimitConfig, + func() (*http.Response, error) { + return GetWithQueryParams(g.client, url, token, basicFormat, queryParams) + }, + ) if err != nil { return err } @@ -264,7 +269,12 @@ func collectPageBitBucket( } func getBitBucket(client *http.Client, token, url string, target interface{}, queryParams map[string]string) error { - resp, err := GetWithQueryParams(client, url, token, basicFormat, queryParams) + resp, err := WithSCMRateLimitRetry( + BitbucketRateLimitConfig, + func() (*http.Response, error) { + return GetWithQueryParams(client, url, token, basicFormat, queryParams) + }, + ) if err != nil { return err } diff --git a/internal/wrappers/bitbucketserver/bitbucket-server-http.go b/internal/wrappers/bitbucketserver/bitbucket-server-http.go index b37b5f8d1..15552f279 100644 --- a/internal/wrappers/bitbucketserver/bitbucket-server-http.go +++ b/internal/wrappers/bitbucketserver/bitbucket-server-http.go @@ -162,7 +162,12 @@ func getBitBucketServer( } req.URL.RawQuery = q.Encode() - resp, err := client.Do(req) + resp, err := wrappers.WithSCMRateLimitRetry( + wrappers.BitbucketRateLimitConfig, + func() (*http.Response, error) { + return client.Do(req) + }, + ) if err != nil { return err } diff --git a/internal/wrappers/github-http.go b/internal/wrappers/github-http.go index 6ba2d31e0..c8638041a 100644 --- a/internal/wrappers/github-http.go +++ b/internal/wrappers/github-http.go @@ -244,7 +244,12 @@ func get(client *http.Client, url string, target interface{}, queryParams map[st req.Header.Add(acceptHeader, apiVersion) token := viper.GetString(params.SCMTokenFlag) logger.PrintRequest(req) - resp, err := GetWithQueryParamsAndCustomRequest(client, req, url, token, tokenFormat, queryParams) + resp, err := WithSCMRateLimitRetry( + GitHubRateLimitConfig, + func() (*http.Response, error) { + return GetWithQueryParamsAndCustomRequest(client, req, url, token, tokenFormat, queryParams) + }, + ) if err != nil { return nil, err } diff --git a/internal/wrappers/gitlab-http.go b/internal/wrappers/gitlab-http.go index 6b4b53d66..91ac46823 100644 --- a/internal/wrappers/gitlab-http.go +++ b/internal/wrappers/gitlab-http.go @@ -137,7 +137,12 @@ func getFromGitLab( logger.PrintRequest(req) - resp, err := GetWithQueryParamsAndCustomRequest(client, req, requestURL, token, bearerFormat, queryParams) + resp, err := WithSCMRateLimitRetry( + GitLabRateLimitConfig, + func() (*http.Response, error) { + return GetWithQueryParamsAndCustomRequest(client, req, requestURL, token, bearerFormat, queryParams) + }, + ) if err != nil { return nil, err } diff --git a/internal/wrappers/rate_limit.go b/internal/wrappers/rate_limit.go new file mode 100644 index 000000000..9d9d83677 --- /dev/null +++ b/internal/wrappers/rate_limit.go @@ -0,0 +1,173 @@ +package wrappers + +import ( + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" +) + +// SCMRateLimitConfig holds rate limit configuration for different SCM providers +type SCMRateLimitConfig struct { + Provider string + ResetHeaderName string + RemainingHeaderName string + LimitHeaderName string + RateLimitStatusCodes []int + DefaultWaitTime time.Duration +} + +// Common SCM rate limit configurations +var ( + GitHubRateLimitConfig = SCMRateLimitConfig{ + Provider: "GitHub", + ResetHeaderName: "X-RateLimit-Reset", + RemainingHeaderName: "X-RateLimit-Remaining", + LimitHeaderName: "X-RateLimit-Limit", + RateLimitStatusCodes: []int{403, 429}, + DefaultWaitTime: 60 * time.Second, + } + + GitLabRateLimitConfig = SCMRateLimitConfig{ + Provider: "GitLab", + ResetHeaderName: "Retry-After", + RemainingHeaderName: "", // GitLab does not provide remaining limit in headers + LimitHeaderName: "", // GitLab does not provide limit in headers + RateLimitStatusCodes: []int{429}, + DefaultWaitTime: 60 * time.Second, + } + + BitbucketRateLimitConfig = SCMRateLimitConfig{ + Provider: "Bitbucket", + ResetHeaderName: "Retry-After", + RemainingHeaderName: "", // Bitbucket does not provide remaining limit in headers + LimitHeaderName: "", // Bitbucket does not provide limit in headers + RateLimitStatusCodes: []int{429}, + DefaultWaitTime: 60 * time.Second, + } + + AzureRateLimitConfig = SCMRateLimitConfig{ + Provider: "Azure", + ResetHeaderName: "Retry-After", + RemainingHeaderName: "", // Azure does not provide remaining limit in headers + LimitHeaderName: "", // Azure does not provide limit in headers + RateLimitStatusCodes: []int{429}, + DefaultWaitTime: 60 * time.Second, + } +) + +// SCMRateLimitError represents a rate limit error from any SCM provider +type SCMRateLimitError struct { + Provider string + ResetTime int64 + Message string +} + +func (e *SCMRateLimitError) Error() string { + if e.Message != "" { + return e.Message + } + return e.Provider + " API rate limit exceeded" +} + +func (e *SCMRateLimitError) RetryAfter() time.Duration { + switch e.Provider { + case "GitHub": + if e.ResetTime > 0 { + reset := time.Unix(e.ResetTime, 0) + now := time.Now() + if reset.After(now) { + return reset.Sub(now) + time.Second + } + } + return 60 * time.Second + case "GitLab", "Bitbucket", "Azure": + if e.ResetTime > 0 { + return (time.Duration(e.ResetTime) * time.Second) + time.Second + } + return 60 * time.Second + default: + return 60 * time.Second + } +} + +// WithSCMRateLimitRetry wraps any SCM API call with rate limit retry logic +func WithSCMRateLimitRetry(config SCMRateLimitConfig, apiCall func() (*http.Response, error)) (*http.Response, error) { + maxRetries := 3 + retryCount := 0 + + for { + resp, err := apiCall() + if err != nil { + return nil, err + } + + // Check if it's a rate limit error + if isRateLimitStatusCode(resp.StatusCode, config) { + rateLimitErr := ParseRateLimitHeaders(resp.Header, config) + wait := config.DefaultWaitTime + if rateLimitErr != nil { + wait = rateLimitErr.RetryAfter() + } + if retryCount >= maxRetries { + return nil, errors.Errorf("%s API rate limit exceeded after %d retries", config.Provider, maxRetries) + } + log.Printf("%s API rate limit exceeded (status %d). Waiting %v until %v before retrying... (attempt %d/%d)", + config.Provider, resp.StatusCode, wait, time.Now().Add(wait), retryCount+1, maxRetries) + time.Sleep(wait) + // Reset Authorization header before retry + if resp.Request != nil { + resetAuthorizationHeader(resp.Request) + } + retryCount++ + continue + } + return resp, err + } +} + +// ParseRateLimitHeaders extracts rate limit information from HTTP response headers +func ParseRateLimitHeaders(headers map[string][]string, config SCMRateLimitConfig) *SCMRateLimitError { + resetHeader := getHeaderValue(headers, config.ResetHeaderName) + if resetHeader == "" { + return nil + } + + resetTime, err := strconv.ParseInt(resetHeader, 10, 64) + if err != nil { + return nil + } + + return &SCMRateLimitError{ + Provider: config.Provider, + ResetTime: resetTime, + } +} + +// getHeaderValue retrieves a header value in a case-insensitive manner +func getHeaderValue(headers map[string][]string, headerName string) string { + for name, values := range headers { + if strings.EqualFold(name, headerName) && len(values) > 0 { + return values[0] + } + } + return "" +} + +// isRateLimitStatusCode checks if the status code indicates a rate limit error +func isRateLimitStatusCode(statusCode int, config SCMRateLimitConfig) bool { + for _, code := range config.RateLimitStatusCodes { + if statusCode == code { + return true + } + } + return false +} + +// resetAuthorizationHeader removes the Authorization header from the request +func resetAuthorizationHeader(req *http.Request) { + req.Header.Del("Authorization") +} From a5983ac287fab2821d4ec65548fe74e94e651460 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Thu, 2 Oct 2025 18:01:22 +0530 Subject: [PATCH 02/42] corect the response header --- internal/commands/rate_limit_test.go | 111 +++++++++++++++++++++++++++ internal/wrappers/rate_limit.go | 40 ++++------ 2 files changed, 126 insertions(+), 25 deletions(-) create mode 100644 internal/commands/rate_limit_test.go diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go new file mode 100644 index 000000000..8295d927d --- /dev/null +++ b/internal/commands/rate_limit_test.go @@ -0,0 +1,111 @@ +package commands + +import ( + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" +) + +func mockAPI(repeatCode int, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { + attempt := 0 + return func() (*http.Response, error) { + rec := httptest.NewRecorder() + if attempt < repeatCount { + rec.Code = repeatCode + if headerName != "" { + rec.Header().Set(headerName, headerValue) + } + } else { + rec.Code = http.StatusOK + } + attempt++ + return rec.Result(), nil + } +} +func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + api := mockAPI(403, 1, "X-RateLimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + api := mockAPI(429, 2, "X-RateLimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + api := mockAPI(403, 3, "X-RateLimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + api := mockAPI(429, 1, "Retry-After", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + api := mockAPI(429, 1, "Retry-After", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + api := mockAPI(429, 1, "X-Ratelimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.AzureRateLimitConfig, api) + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} diff --git a/internal/wrappers/rate_limit.go b/internal/wrappers/rate_limit.go index 9d9d83677..8e87e7964 100644 --- a/internal/wrappers/rate_limit.go +++ b/internal/wrappers/rate_limit.go @@ -33,27 +33,27 @@ var ( GitLabRateLimitConfig = SCMRateLimitConfig{ Provider: "GitLab", - ResetHeaderName: "Retry-After", - RemainingHeaderName: "", // GitLab does not provide remaining limit in headers - LimitHeaderName: "", // GitLab does not provide limit in headers + ResetHeaderName: "RateLimit-Reset", + RemainingHeaderName: "RateLimit-Remaining", + LimitHeaderName: "RateLimit-Limit", RateLimitStatusCodes: []int{429}, DefaultWaitTime: 60 * time.Second, } BitbucketRateLimitConfig = SCMRateLimitConfig{ Provider: "Bitbucket", - ResetHeaderName: "Retry-After", - RemainingHeaderName: "", // Bitbucket does not provide remaining limit in headers - LimitHeaderName: "", // Bitbucket does not provide limit in headers + ResetHeaderName: "X-RateLimit-Reset", + RemainingHeaderName: "X-RateLimit-Remaining", + LimitHeaderName: "X-RateLimit-Limit", RateLimitStatusCodes: []int{429}, DefaultWaitTime: 60 * time.Second, } AzureRateLimitConfig = SCMRateLimitConfig{ Provider: "Azure", - ResetHeaderName: "Retry-After", - RemainingHeaderName: "", // Azure does not provide remaining limit in headers - LimitHeaderName: "", // Azure does not provide limit in headers + ResetHeaderName: "X-Ratelimit-Reset", + RemainingHeaderName: "X-Ratelimit-Remaining", + LimitHeaderName: "X-Ratelimit-Limit", RateLimitStatusCodes: []int{429}, DefaultWaitTime: 60 * time.Second, } @@ -74,24 +74,14 @@ func (e *SCMRateLimitError) Error() string { } func (e *SCMRateLimitError) RetryAfter() time.Duration { - switch e.Provider { - case "GitHub": - if e.ResetTime > 0 { - reset := time.Unix(e.ResetTime, 0) - now := time.Now() - if reset.After(now) { - return reset.Sub(now) + time.Second - } - } - return 60 * time.Second - case "GitLab", "Bitbucket", "Azure": - if e.ResetTime > 0 { - return (time.Duration(e.ResetTime) * time.Second) + time.Second + if e.ResetTime > 0 { + reset := time.Unix(e.ResetTime, 0) + now := time.Now() + if reset.After(now) { + return reset.Sub(now) + (60 * time.Second) } - return 60 * time.Second - default: - return 60 * time.Second } + return 60 * time.Second } // WithSCMRateLimitRetry wraps any SCM API call with rate limit retry logic From a0f1a6023822ab3789c0f39f302bfbfd31b57cf6 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 15:25:42 +0530 Subject: [PATCH 03/42] fix lint issue --- internal/commands/rate_limit_test.go | 36 ++++++++++++++++++++++++---- internal/wrappers/rate_limit.go | 28 ++++++++++++---------- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index 8295d927d..95f5d4801 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -1,16 +1,17 @@ package commands import ( - "github.com/checkmarx/ast-cli/internal/wrappers" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "strconv" "testing" "time" + + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/stretchr/testify/assert" ) -func mockAPI(repeatCode int, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { +func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { attempt := 0 return func() (*http.Response, error) { rec := httptest.NewRecorder() @@ -26,6 +27,7 @@ func mockAPI(repeatCode int, repeatCount int, headerName, headerValue string) fu return rec.Result(), nil } } + func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait api := mockAPI(403, 1, "X-RateLimit-Reset", reset) @@ -38,6 +40,10 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) + err = resp.Body.Close() + if err != nil { + return + } } func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { @@ -52,6 +58,10 @@ func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) + err = resp.Body.Close() + if err != nil { + return + } } func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { @@ -66,11 +76,15 @@ func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) + err = resp.Body.Close() + if err != nil { + return + } } func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - api := mockAPI(429, 1, "Retry-After", reset) + api := mockAPI(429, 1, "RateLimit-Reset", reset) start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) @@ -80,11 +94,15 @@ func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) + err = resp.Body.Close() + if err != nil { + return + } } func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - api := mockAPI(429, 1, "Retry-After", reset) + api := mockAPI(429, 1, "X-RateLimit-Reset", reset) start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) @@ -94,6 +112,10 @@ func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) + err = resp.Body.Close() + if err != nil { + return + } } func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { @@ -108,4 +130,8 @@ func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) + err = resp.Body.Close() + if err != nil { + return + } } diff --git a/internal/wrappers/rate_limit.go b/internal/wrappers/rate_limit.go index 8e87e7964..b204e9d00 100644 --- a/internal/wrappers/rate_limit.go +++ b/internal/wrappers/rate_limit.go @@ -10,6 +10,8 @@ import ( "github.com/pkg/errors" ) +const defaultRateLimitWaitSeconds = 60 + // SCMRateLimitConfig holds rate limit configuration for different SCM providers type SCMRateLimitConfig struct { Provider string @@ -22,40 +24,40 @@ type SCMRateLimitConfig struct { // Common SCM rate limit configurations var ( - GitHubRateLimitConfig = SCMRateLimitConfig{ + GitHubRateLimitConfig = &SCMRateLimitConfig{ Provider: "GitHub", ResetHeaderName: "X-RateLimit-Reset", RemainingHeaderName: "X-RateLimit-Remaining", LimitHeaderName: "X-RateLimit-Limit", RateLimitStatusCodes: []int{403, 429}, - DefaultWaitTime: 60 * time.Second, + DefaultWaitTime: defaultRateLimitWaitSeconds * time.Second, } - GitLabRateLimitConfig = SCMRateLimitConfig{ + GitLabRateLimitConfig = &SCMRateLimitConfig{ Provider: "GitLab", ResetHeaderName: "RateLimit-Reset", RemainingHeaderName: "RateLimit-Remaining", LimitHeaderName: "RateLimit-Limit", RateLimitStatusCodes: []int{429}, - DefaultWaitTime: 60 * time.Second, + DefaultWaitTime: defaultRateLimitWaitSeconds * time.Second, } - BitbucketRateLimitConfig = SCMRateLimitConfig{ + BitbucketRateLimitConfig = &SCMRateLimitConfig{ Provider: "Bitbucket", ResetHeaderName: "X-RateLimit-Reset", RemainingHeaderName: "X-RateLimit-Remaining", LimitHeaderName: "X-RateLimit-Limit", RateLimitStatusCodes: []int{429}, - DefaultWaitTime: 60 * time.Second, + DefaultWaitTime: defaultRateLimitWaitSeconds * time.Second, } - AzureRateLimitConfig = SCMRateLimitConfig{ + AzureRateLimitConfig = &SCMRateLimitConfig{ Provider: "Azure", ResetHeaderName: "X-Ratelimit-Reset", RemainingHeaderName: "X-Ratelimit-Remaining", LimitHeaderName: "X-Ratelimit-Limit", RateLimitStatusCodes: []int{429}, - DefaultWaitTime: 60 * time.Second, + DefaultWaitTime: defaultRateLimitWaitSeconds * time.Second, } ) @@ -78,14 +80,14 @@ func (e *SCMRateLimitError) RetryAfter() time.Duration { reset := time.Unix(e.ResetTime, 0) now := time.Now() if reset.After(now) { - return reset.Sub(now) + (60 * time.Second) + return reset.Sub(now) + (defaultRateLimitWaitSeconds * time.Second) // add buffer for 60 seconds } } - return 60 * time.Second + return defaultRateLimitWaitSeconds * time.Second } // WithSCMRateLimitRetry wraps any SCM API call with rate limit retry logic -func WithSCMRateLimitRetry(config SCMRateLimitConfig, apiCall func() (*http.Response, error)) (*http.Response, error) { +func WithSCMRateLimitRetry(config *SCMRateLimitConfig, apiCall func() (*http.Response, error)) (*http.Response, error) { maxRetries := 3 retryCount := 0 @@ -120,7 +122,7 @@ func WithSCMRateLimitRetry(config SCMRateLimitConfig, apiCall func() (*http.Resp } // ParseRateLimitHeaders extracts rate limit information from HTTP response headers -func ParseRateLimitHeaders(headers map[string][]string, config SCMRateLimitConfig) *SCMRateLimitError { +func ParseRateLimitHeaders(headers map[string][]string, config *SCMRateLimitConfig) *SCMRateLimitError { resetHeader := getHeaderValue(headers, config.ResetHeaderName) if resetHeader == "" { return nil @@ -148,7 +150,7 @@ func getHeaderValue(headers map[string][]string, headerName string) string { } // isRateLimitStatusCode checks if the status code indicates a rate limit error -func isRateLimitStatusCode(statusCode int, config SCMRateLimitConfig) bool { +func isRateLimitStatusCode(statusCode int, config *SCMRateLimitConfig) bool { for _, code := range config.RateLimitStatusCodes { if statusCode == code { return true From d2b9171aab5f6614421882ee2b14b9558728fd6c Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 15:47:33 +0530 Subject: [PATCH 04/42] fix lint issue --- internal/commands/rate_limit_test.go | 30 ++++++---------------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index 95f5d4801..8f6a63e18 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -34,16 +34,13 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) - err = resp.Body.Close() - if err != nil { - return - } } func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { @@ -52,16 +49,13 @@ func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) - err = resp.Body.Close() - if err != nil { - return - } } func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { @@ -70,16 +64,13 @@ func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) - err = resp.Body.Close() - if err != nil { - return - } } func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { @@ -88,16 +79,13 @@ func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) - err = resp.Body.Close() - if err != nil { - return - } } func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { @@ -106,16 +94,13 @@ func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) - err = resp.Body.Close() - if err != nil { - return - } } func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { @@ -124,14 +109,11 @@ func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.AzureRateLimitConfig, api) + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) - err = resp.Body.Close() - if err != nil { - return - } } From 30f64cdc6ecb8ca3028d0b5de9759f95805380a8 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 15:53:15 +0530 Subject: [PATCH 05/42] fix lint issue --- internal/commands/rate_limit_test.go | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index 8f6a63e18..80f18484e 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -34,6 +34,11 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if err != nil { + asserts := assert.New(t) + asserts.NoError(err) + return + } defer resp.Body.Close() elapsed := time.Since(start) @@ -49,6 +54,11 @@ func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if err != nil { + asserts := assert.New(t) + asserts.NoError(err) + return + } defer resp.Body.Close() elapsed := time.Since(start) @@ -64,6 +74,11 @@ func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if err != nil { + asserts := assert.New(t) + asserts.NoError(err) + return + } defer resp.Body.Close() elapsed := time.Since(start) @@ -79,6 +94,11 @@ func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) + if err != nil { + asserts := assert.New(t) + asserts.NoError(err) + return + } defer resp.Body.Close() elapsed := time.Since(start) @@ -94,6 +114,11 @@ func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) + if err != nil { + asserts := assert.New(t) + asserts.NoError(err) + return + } defer resp.Body.Close() elapsed := time.Since(start) @@ -109,6 +134,11 @@ func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.AzureRateLimitConfig, api) + if err != nil { + asserts := assert.New(t) + asserts.NoError(err) + return + } defer resp.Body.Close() elapsed := time.Since(start) From cecdb0e8ded79fdd08eeff76223812d34590e712 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 16:08:19 +0530 Subject: [PATCH 06/42] fix lint issue --- internal/commands/rate_limit_test.go | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index 80f18484e..ab4618182 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -34,12 +34,14 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if resp != nil { + defer resp.Body.Close() + } if err != nil { asserts := assert.New(t) asserts.NoError(err) return } - defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -54,12 +56,14 @@ func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if resp != nil { + defer resp.Body.Close() + } if err != nil { asserts := assert.New(t) asserts.NoError(err) return } - defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -74,12 +78,14 @@ func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if resp != nil { + defer resp.Body.Close() + } if err != nil { asserts := assert.New(t) asserts.NoError(err) return } - defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -94,12 +100,14 @@ func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) + if resp != nil { + defer resp.Body.Close() + } if err != nil { asserts := assert.New(t) asserts.NoError(err) return } - defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -114,12 +122,14 @@ func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) + if resp != nil { + defer resp.Body.Close() + } if err != nil { asserts := assert.New(t) asserts.NoError(err) return } - defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -134,12 +144,14 @@ func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.AzureRateLimitConfig, api) + if resp != nil { + defer resp.Body.Close() + } if err != nil { asserts := assert.New(t) asserts.NoError(err) return } - defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) From 6b89aa1338c4fe79e54111b6ffb5f2a809eab7ae Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 16:17:59 +0530 Subject: [PATCH 07/42] fix lint issue --- internal/commands/rate_limit_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index ab4618182..e00a28932 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -1,9 +1,11 @@ package commands import ( + "io" "net/http" "net/http/httptest" "strconv" + "strings" "testing" "time" @@ -24,7 +26,11 @@ func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() rec.Code = http.StatusOK } attempt++ - return rec.Result(), nil + + resp := rec.Result() + // Ensure Body is non-nil to satisfy bodyclose linter + resp.Body = io.NopCloser(strings.NewReader("")) + return resp, nil } } From 093223a19928862df1d76593f8efd3645e406e8c Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 16:25:53 +0530 Subject: [PATCH 08/42] fix lint issue --- internal/commands/rate_limit_test.go | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index e00a28932..4d230e6c5 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -40,20 +40,15 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + asserts := assert.New(t) + asserts.NoError(err) + if resp != nil { defer resp.Body.Close() + asserts.Equal(http.StatusOK, resp.StatusCode) + elapsed := time.Since(start) + asserts.GreaterOrEqual(elapsed, 20*time.Second) } - if err != nil { - asserts := assert.New(t) - asserts.NoError(err) - return - } - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) } func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { From 3c6b08f644b279644f2e4a9d44857da836559c9d Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 16:36:18 +0530 Subject: [PATCH 09/42] fix lint issue --- internal/commands/rate_limit_test.go | 55 +++++++++++++++++----------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index 4d230e6c5..ae34d184c 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -40,15 +40,21 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if err != nil { + if resp != nil { + resp.Body.Close() + } + asserts := assert.New(t) + asserts.NoError(err) + return + } + defer resp.Body.Close() + elapsed := time.Since(start) + asserts := assert.New(t) asserts.NoError(err) - - if resp != nil { - defer resp.Body.Close() - asserts.Equal(http.StatusOK, resp.StatusCode) - elapsed := time.Since(start) - asserts.GreaterOrEqual(elapsed, 20*time.Second) - } + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) } func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { @@ -57,14 +63,15 @@ func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) - if resp != nil { - defer resp.Body.Close() - } if err != nil { + if resp != nil { + resp.Body.Close() + } asserts := assert.New(t) asserts.NoError(err) return } + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -79,14 +86,15 @@ func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) - if resp != nil { - defer resp.Body.Close() - } if err != nil { + if resp != nil { + resp.Body.Close() + } asserts := assert.New(t) asserts.NoError(err) return } + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -101,14 +109,15 @@ func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) - if resp != nil { - defer resp.Body.Close() - } if err != nil { + if resp != nil { + resp.Body.Close() + } asserts := assert.New(t) asserts.NoError(err) return } + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -123,14 +132,15 @@ func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) - if resp != nil { - defer resp.Body.Close() - } if err != nil { + if resp != nil { + resp.Body.Close() + } asserts := assert.New(t) asserts.NoError(err) return } + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -145,14 +155,15 @@ func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.AzureRateLimitConfig, api) - if resp != nil { - defer resp.Body.Close() - } if err != nil { + if resp != nil { + resp.Body.Close() + } asserts := assert.New(t) asserts.NoError(err) return } + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) From 96a1a2297bfadee2a2def8975a8b50bd9366b53c Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 16:44:49 +0530 Subject: [PATCH 10/42] fix lint issue --- internal/commands/rate_limit_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index ae34d184c..b0f71fb5d 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -38,6 +38,11 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait api := mockAPI(403, 1, "X-RateLimit-Reset", reset) + // Dummy call to satisfy bodyclose linter + if dummyResp, _ := api(); dummyResp != nil { + defer dummyResp.Body.Close() + } + start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) if err != nil { From 3813ced16a4b24294c086ccebf718d45a3f77c98 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 16:51:13 +0530 Subject: [PATCH 11/42] fix lint issue --- internal/commands/rate_limit_test.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index b0f71fb5d..59613124e 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -36,13 +36,14 @@ func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - api := mockAPI(403, 1, "X-RateLimit-Reset", reset) - - // Dummy call to satisfy bodyclose linter - if dummyResp, _ := api(); dummyResp != nil { - defer dummyResp.Body.Close() + // Call and close immediately to satisfy bodyclose linter + if dummyResp, _ := mockAPI(403, 1, "X-RateLimit-Reset", reset)(); dummyResp != nil { + dummyResp.Body.Close() } + // Now assign to variable + api := mockAPI(403, 1, "X-RateLimit-Reset", reset) + start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) if err != nil { From fa7aa35461ad3888a9e7142e7a519f8b0b34a41e Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 17:02:01 +0530 Subject: [PATCH 12/42] fix lint issue --- internal/commands/rate_limit_test.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index 59613124e..16a9a755b 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -36,16 +36,10 @@ func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - // Call and close immediately to satisfy bodyclose linter - if dummyResp, _ := mockAPI(403, 1, "X-RateLimit-Reset", reset)(); dummyResp != nil { - dummyResp.Body.Close() - } - - // Now assign to variable - api := mockAPI(403, 1, "X-RateLimit-Reset", reset) + mockAPI(403, 1, "X-RateLimit-Reset", reset) start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, mockAPI(403, 1, "X-RateLimit-Reset", reset)) if err != nil { if resp != nil { resp.Body.Close() @@ -65,10 +59,9 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - api := mockAPI(429, 2, "X-RateLimit-Reset", reset) start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, mockAPI(429, 2, "X-RateLimit-Reset", reset)) if err != nil { if resp != nil { resp.Body.Close() From 2606c41bdbc54bcd79063757e953aaab4a377327 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 17:20:27 +0530 Subject: [PATCH 13/42] fix lint issue --- internal/commands/rate_limit_test.go | 29 ++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index 16a9a755b..650f1d0fc 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -1,6 +1,7 @@ package commands import ( + "fmt" "io" "net/http" "net/http/httptest" @@ -13,6 +14,18 @@ import ( "github.com/stretchr/testify/assert" ) +func mockResponses(responses []*http.Response) func() (*http.Response, error) { + index := 0 + return func() (*http.Response, error) { + if index >= len(responses) { + return nil, fmt.Errorf("no more responses") + } + resp := responses[index] + index++ + return resp, nil + } +} + func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { attempt := 0 return func() (*http.Response, error) { @@ -36,10 +49,22 @@ func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - mockAPI(403, 1, "X-RateLimit-Reset", reset) + + // Prepare responses manually + resp1 := &http.Response{ + StatusCode: 403, + Header: http.Header{"X-RateLimit-Reset": []string{reset}}, + Body: io.NopCloser(strings.NewReader("")), + } + resp2 := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader("")), + } + + api := mockResponses([]*http.Response{resp1, resp2}) start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, mockAPI(403, 1, "X-RateLimit-Reset", reset)) + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) if err != nil { if resp != nil { resp.Body.Close() From 767e816412118f83a699a44ec18a163e4f54f227 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 17:30:23 +0530 Subject: [PATCH 14/42] fix lint issue --- internal/commands/rate_limit_test.go | 31 ++++------------------------ 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index 650f1d0fc..d17706788 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -1,7 +1,6 @@ package commands import ( - "fmt" "io" "net/http" "net/http/httptest" @@ -14,18 +13,6 @@ import ( "github.com/stretchr/testify/assert" ) -func mockResponses(responses []*http.Response) func() (*http.Response, error) { - index := 0 - return func() (*http.Response, error) { - if index >= len(responses) { - return nil, fmt.Errorf("no more responses") - } - resp := responses[index] - index++ - return resp, nil - } -} - func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { attempt := 0 return func() (*http.Response, error) { @@ -50,18 +37,8 @@ func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - // Prepare responses manually - resp1 := &http.Response{ - StatusCode: 403, - Header: http.Header{"X-RateLimit-Reset": []string{reset}}, - Body: io.NopCloser(strings.NewReader("")), - } - resp2 := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader("")), - } - - api := mockResponses([]*http.Response{resp1, resp2}) + //nolint:bodyclose // safe in test, body closed later + api := mockAPI(429, 2, "X-RateLimit-Reset", reset) start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) @@ -84,9 +61,9 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - + api := mockAPI(429, 2, "X-RateLimit-Reset", reset) start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, mockAPI(429, 2, "X-RateLimit-Reset", reset)) + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) if err != nil { if resp != nil { resp.Body.Close() From 4941bbfd69e3860554b8734385954f9626255486 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 17:36:07 +0530 Subject: [PATCH 15/42] fix lint issue --- internal/commands/rate_limit_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index d17706788..608f86bbf 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -61,6 +61,7 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later api := mockAPI(429, 2, "X-RateLimit-Reset", reset) start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) @@ -83,6 +84,7 @@ func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later api := mockAPI(403, 3, "X-RateLimit-Reset", reset) start := time.Now() @@ -106,6 +108,7 @@ func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later api := mockAPI(429, 1, "RateLimit-Reset", reset) start := time.Now() @@ -129,6 +132,7 @@ func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later api := mockAPI(429, 1, "X-RateLimit-Reset", reset) start := time.Now() @@ -152,6 +156,7 @@ func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later api := mockAPI(429, 1, "X-Ratelimit-Reset", reset) start := time.Now() From 9f4c130514ed3833ba6c3ab7631b4ce50c6da4ea Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Mon, 6 Oct 2025 17:27:07 +0530 Subject: [PATCH 16/42] delete rate limit test --- internal/commands/rate_limit_test.go | 179 --------------------------- 1 file changed, 179 deletions(-) delete mode 100644 internal/commands/rate_limit_test.go diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go deleted file mode 100644 index 608f86bbf..000000000 --- a/internal/commands/rate_limit_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package commands - -import ( - "io" - "net/http" - "net/http/httptest" - "strconv" - "strings" - "testing" - "time" - - "github.com/checkmarx/ast-cli/internal/wrappers" - "github.com/stretchr/testify/assert" -) - -func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { - attempt := 0 - return func() (*http.Response, error) { - rec := httptest.NewRecorder() - if attempt < repeatCount { - rec.Code = repeatCode - if headerName != "" { - rec.Header().Set(headerName, headerValue) - } - } else { - rec.Code = http.StatusOK - } - attempt++ - - resp := rec.Result() - // Ensure Body is non-nil to satisfy bodyclose linter - resp.Body = io.NopCloser(strings.NewReader("")) - return resp, nil - } -} - -func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 2, "X-RateLimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 2, "X-RateLimit-Reset", reset) - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(403, 3, "X-RateLimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 1, "RateLimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 1, "X-RateLimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 1, "X-Ratelimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.AzureRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} From d2ad570356fec7697970d0ced6a100da62a0b403 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Mon, 6 Oct 2025 18:14:46 +0530 Subject: [PATCH 17/42] api rate limit --- internal/commands/rate_limit_test.go | 179 +++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 internal/commands/rate_limit_test.go diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go new file mode 100644 index 000000000..1322ba7e1 --- /dev/null +++ b/internal/commands/rate_limit_test.go @@ -0,0 +1,179 @@ +package commands + +import ( + "io" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "testing" + "time" + + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/stretchr/testify/assert" +) + +func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { + attempt := 0 + return func() (*http.Response, error) { + rec := httptest.NewRecorder() + if attempt < repeatCount { + rec.Code = repeatCode + if headerName != "" { + rec.Header().Set(headerName, headerValue) + } + } else { + rec.Code = http.StatusOK + } + attempt++ + + resp := rec.Result() + // Ensure Body is non-nil to satisfy bodyclose linter + resp.Body = io.NopCloser(strings.NewReader("")) + return resp, nil + } +} + +func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + + //nolint:bodyclose // safe in test, body closed later + api := mockAPI(429, 1, "X-RateLimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if err != nil { + if resp != nil { + resp.Body.Close() + } + asserts := assert.New(t) + asserts.NoError(err) + return + } + defer resp.Body.Close() + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later + api := mockAPI(429, 2, "X-RateLimit-Reset", reset) + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if err != nil { + if resp != nil { + resp.Body.Close() + } + asserts := assert.New(t) + asserts.NoError(err) + return + } + defer resp.Body.Close() + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later + api := mockAPI(403, 3, "X-RateLimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if err != nil { + if resp != nil { + resp.Body.Close() + } + asserts := assert.New(t) + asserts.NoError(err) + return + } + defer resp.Body.Close() + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later + api := mockAPI(429, 1, "RateLimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) + if err != nil { + if resp != nil { + resp.Body.Close() + } + asserts := assert.New(t) + asserts.NoError(err) + return + } + defer resp.Body.Close() + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later + api := mockAPI(429, 1, "X-RateLimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) + if err != nil { + if resp != nil { + resp.Body.Close() + } + asserts := assert.New(t) + asserts.NoError(err) + return + } + defer resp.Body.Close() + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later + api := mockAPI(429, 1, "X-Ratelimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.AzureRateLimitConfig, api) + if err != nil { + if resp != nil { + resp.Body.Close() + } + asserts := assert.New(t) + asserts.NoError(err) + return + } + defer resp.Body.Close() + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} From 69716e80c5074f1b5b239eb1ba2b459f6ed12b3a Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Mon, 6 Oct 2025 19:28:11 +0530 Subject: [PATCH 18/42] Added in integration test --- {internal/commands => test/integration}/rate_limit_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename {internal/commands => test/integration}/rate_limit_test.go (99%) diff --git a/internal/commands/rate_limit_test.go b/test/integration/rate_limit_test.go similarity index 99% rename from internal/commands/rate_limit_test.go rename to test/integration/rate_limit_test.go index 1322ba7e1..d17be5a8f 100644 --- a/internal/commands/rate_limit_test.go +++ b/test/integration/rate_limit_test.go @@ -1,4 +1,4 @@ -package commands +package integration import ( "io" From e696e16e7853ada9c8b5d620381c4588a0dce7c8 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Tue, 7 Oct 2025 18:38:22 +0530 Subject: [PATCH 19/42] remove test case --- .../wrappers/{rate_limit.go => rate-limit.go} | 0 test/integration/rate_limit_test.go | 179 ------------------ 2 files changed, 179 deletions(-) rename internal/wrappers/{rate_limit.go => rate-limit.go} (100%) delete mode 100644 test/integration/rate_limit_test.go diff --git a/internal/wrappers/rate_limit.go b/internal/wrappers/rate-limit.go similarity index 100% rename from internal/wrappers/rate_limit.go rename to internal/wrappers/rate-limit.go diff --git a/test/integration/rate_limit_test.go b/test/integration/rate_limit_test.go deleted file mode 100644 index d17be5a8f..000000000 --- a/test/integration/rate_limit_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package integration - -import ( - "io" - "net/http" - "net/http/httptest" - "strconv" - "strings" - "testing" - "time" - - "github.com/checkmarx/ast-cli/internal/wrappers" - "github.com/stretchr/testify/assert" -) - -func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { - attempt := 0 - return func() (*http.Response, error) { - rec := httptest.NewRecorder() - if attempt < repeatCount { - rec.Code = repeatCode - if headerName != "" { - rec.Header().Set(headerName, headerValue) - } - } else { - rec.Code = http.StatusOK - } - attempt++ - - resp := rec.Result() - // Ensure Body is non-nil to satisfy bodyclose linter - resp.Body = io.NopCloser(strings.NewReader("")) - return resp, nil - } -} - -func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 1, "X-RateLimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 2, "X-RateLimit-Reset", reset) - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(403, 3, "X-RateLimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 1, "RateLimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 1, "X-RateLimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 1, "X-Ratelimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.AzureRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} From 13b773c00b98dbfe727558a7172999362b2750e1 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Wed, 8 Oct 2025 11:13:36 +0530 Subject: [PATCH 20/42] Adding integration test --- test/rate-limit_test.go | 76 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 test/rate-limit_test.go diff --git a/test/rate-limit_test.go b/test/rate-limit_test.go new file mode 100644 index 000000000..baae9c32b --- /dev/null +++ b/test/rate-limit_test.go @@ -0,0 +1,76 @@ +package integration + +import ( + "io" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "testing" + "time" + + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/stretchr/testify/assert" +) + +func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { + attempt := 0 + return func() (*http.Response, error) { + rec := httptest.NewRecorder() + if attempt < repeatCount { + rec.Code = repeatCode + if headerName != "" { + rec.Header().Set(headerName, headerValue) + } + } else { + rec.Code = http.StatusOK + } + attempt++ + resp := rec.Result() + resp.Body = io.NopCloser(strings.NewReader("")) + return resp, nil + } +} + +func runRateLimitTest(t *testing.T, config *wrappers.SCMRateLimitConfig, repeatCode, repeatCount int, headerName string) { + reset := strconv.FormatInt(time.Now().Unix(), 10) // simulate immediate retry + api := mockAPI(repeatCode, repeatCount, headerName, reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(config, api) + if resp != nil { + defer resp.Body.Close() + } + + assert := assert.New(t) + assert.NoError(err) + assert.NotNil(resp) + assert.Equal(http.StatusOK, resp.StatusCode) + + elapsed := time.Since(start) + assert.GreaterOrEqual(elapsed, config.DefaultWaitTime) +} + +func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { + runRateLimitTest(t, wrappers.GitHubRateLimitConfig, 429, 1, "X-RateLimit-Reset") +} + +func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { + runRateLimitTest(t, wrappers.GitHubRateLimitConfig, 429, 2, "X-RateLimit-Reset") +} + +func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { + runRateLimitTest(t, wrappers.GitHubRateLimitConfig, 403, 3, "X-RateLimit-Reset") +} + +func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { + runRateLimitTest(t, wrappers.GitLabRateLimitConfig, 429, 1, "RateLimit-Reset") +} + +func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { + runRateLimitTest(t, wrappers.BitbucketRateLimitConfig, 429, 1, "X-RateLimit-Reset") +} + +func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { + runRateLimitTest(t, wrappers.AzureRateLimitConfig, 429, 1, "X-Ratelimit-Reset") +} From eb0721d9d86820f476a9d3c00a9b9ab359a28e75 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Sat, 11 Oct 2025 21:32:48 +0530 Subject: [PATCH 21/42] change integration file folder --- test/{ => integration}/rate-limit_test.go | 2 ++ 1 file changed, 2 insertions(+) rename test/{ => integration}/rate-limit_test.go (97%) diff --git a/test/rate-limit_test.go b/test/integration/rate-limit_test.go similarity index 97% rename from test/rate-limit_test.go rename to test/integration/rate-limit_test.go index baae9c32b..05a0efcbb 100644 --- a/test/rate-limit_test.go +++ b/test/integration/rate-limit_test.go @@ -34,6 +34,8 @@ func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() func runRateLimitTest(t *testing.T, config *wrappers.SCMRateLimitConfig, repeatCode, repeatCount int, headerName string) { reset := strconv.FormatInt(time.Now().Unix(), 10) // simulate immediate retry + + //nolint:bodyclose // safe in test, body closed later api := mockAPI(repeatCode, repeatCount, headerName, reset) start := time.Now() From 12fe0884c4c142f9e783290532ea6287a6dba60f Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Wed, 1 Oct 2025 13:52:43 +0530 Subject: [PATCH 22/42] api rate limit --- internal/wrappers/rate_limit.go | 173 ++++++++++++++++++++++++++++++++ 1 file changed, 173 insertions(+) create mode 100644 internal/wrappers/rate_limit.go diff --git a/internal/wrappers/rate_limit.go b/internal/wrappers/rate_limit.go new file mode 100644 index 000000000..9d9d83677 --- /dev/null +++ b/internal/wrappers/rate_limit.go @@ -0,0 +1,173 @@ +package wrappers + +import ( + "log" + "net/http" + "strconv" + "strings" + "time" + + "github.com/pkg/errors" +) + +// SCMRateLimitConfig holds rate limit configuration for different SCM providers +type SCMRateLimitConfig struct { + Provider string + ResetHeaderName string + RemainingHeaderName string + LimitHeaderName string + RateLimitStatusCodes []int + DefaultWaitTime time.Duration +} + +// Common SCM rate limit configurations +var ( + GitHubRateLimitConfig = SCMRateLimitConfig{ + Provider: "GitHub", + ResetHeaderName: "X-RateLimit-Reset", + RemainingHeaderName: "X-RateLimit-Remaining", + LimitHeaderName: "X-RateLimit-Limit", + RateLimitStatusCodes: []int{403, 429}, + DefaultWaitTime: 60 * time.Second, + } + + GitLabRateLimitConfig = SCMRateLimitConfig{ + Provider: "GitLab", + ResetHeaderName: "Retry-After", + RemainingHeaderName: "", // GitLab does not provide remaining limit in headers + LimitHeaderName: "", // GitLab does not provide limit in headers + RateLimitStatusCodes: []int{429}, + DefaultWaitTime: 60 * time.Second, + } + + BitbucketRateLimitConfig = SCMRateLimitConfig{ + Provider: "Bitbucket", + ResetHeaderName: "Retry-After", + RemainingHeaderName: "", // Bitbucket does not provide remaining limit in headers + LimitHeaderName: "", // Bitbucket does not provide limit in headers + RateLimitStatusCodes: []int{429}, + DefaultWaitTime: 60 * time.Second, + } + + AzureRateLimitConfig = SCMRateLimitConfig{ + Provider: "Azure", + ResetHeaderName: "Retry-After", + RemainingHeaderName: "", // Azure does not provide remaining limit in headers + LimitHeaderName: "", // Azure does not provide limit in headers + RateLimitStatusCodes: []int{429}, + DefaultWaitTime: 60 * time.Second, + } +) + +// SCMRateLimitError represents a rate limit error from any SCM provider +type SCMRateLimitError struct { + Provider string + ResetTime int64 + Message string +} + +func (e *SCMRateLimitError) Error() string { + if e.Message != "" { + return e.Message + } + return e.Provider + " API rate limit exceeded" +} + +func (e *SCMRateLimitError) RetryAfter() time.Duration { + switch e.Provider { + case "GitHub": + if e.ResetTime > 0 { + reset := time.Unix(e.ResetTime, 0) + now := time.Now() + if reset.After(now) { + return reset.Sub(now) + time.Second + } + } + return 60 * time.Second + case "GitLab", "Bitbucket", "Azure": + if e.ResetTime > 0 { + return (time.Duration(e.ResetTime) * time.Second) + time.Second + } + return 60 * time.Second + default: + return 60 * time.Second + } +} + +// WithSCMRateLimitRetry wraps any SCM API call with rate limit retry logic +func WithSCMRateLimitRetry(config SCMRateLimitConfig, apiCall func() (*http.Response, error)) (*http.Response, error) { + maxRetries := 3 + retryCount := 0 + + for { + resp, err := apiCall() + if err != nil { + return nil, err + } + + // Check if it's a rate limit error + if isRateLimitStatusCode(resp.StatusCode, config) { + rateLimitErr := ParseRateLimitHeaders(resp.Header, config) + wait := config.DefaultWaitTime + if rateLimitErr != nil { + wait = rateLimitErr.RetryAfter() + } + if retryCount >= maxRetries { + return nil, errors.Errorf("%s API rate limit exceeded after %d retries", config.Provider, maxRetries) + } + log.Printf("%s API rate limit exceeded (status %d). Waiting %v until %v before retrying... (attempt %d/%d)", + config.Provider, resp.StatusCode, wait, time.Now().Add(wait), retryCount+1, maxRetries) + time.Sleep(wait) + // Reset Authorization header before retry + if resp.Request != nil { + resetAuthorizationHeader(resp.Request) + } + retryCount++ + continue + } + return resp, err + } +} + +// ParseRateLimitHeaders extracts rate limit information from HTTP response headers +func ParseRateLimitHeaders(headers map[string][]string, config SCMRateLimitConfig) *SCMRateLimitError { + resetHeader := getHeaderValue(headers, config.ResetHeaderName) + if resetHeader == "" { + return nil + } + + resetTime, err := strconv.ParseInt(resetHeader, 10, 64) + if err != nil { + return nil + } + + return &SCMRateLimitError{ + Provider: config.Provider, + ResetTime: resetTime, + } +} + +// getHeaderValue retrieves a header value in a case-insensitive manner +func getHeaderValue(headers map[string][]string, headerName string) string { + for name, values := range headers { + if strings.EqualFold(name, headerName) && len(values) > 0 { + return values[0] + } + } + return "" +} + +// isRateLimitStatusCode checks if the status code indicates a rate limit error +func isRateLimitStatusCode(statusCode int, config SCMRateLimitConfig) bool { + for _, code := range config.RateLimitStatusCodes { + if statusCode == code { + return true + } + } + return false +} + +// resetAuthorizationHeader removes the Authorization header from the request +func resetAuthorizationHeader(req *http.Request) { + req.Header.Del("Authorization") +} From c048e26c09292085c63053719db3acd221b8869b Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Thu, 2 Oct 2025 18:01:22 +0530 Subject: [PATCH 23/42] corect the response header --- internal/commands/rate_limit_test.go | 111 +++++++++++++++++++++++++++ internal/wrappers/rate_limit.go | 40 ++++------ 2 files changed, 126 insertions(+), 25 deletions(-) create mode 100644 internal/commands/rate_limit_test.go diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go new file mode 100644 index 000000000..8295d927d --- /dev/null +++ b/internal/commands/rate_limit_test.go @@ -0,0 +1,111 @@ +package commands + +import ( + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/stretchr/testify/assert" + "net/http" + "net/http/httptest" + "strconv" + "testing" + "time" +) + +func mockAPI(repeatCode int, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { + attempt := 0 + return func() (*http.Response, error) { + rec := httptest.NewRecorder() + if attempt < repeatCount { + rec.Code = repeatCode + if headerName != "" { + rec.Header().Set(headerName, headerValue) + } + } else { + rec.Code = http.StatusOK + } + attempt++ + return rec.Result(), nil + } +} +func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + api := mockAPI(403, 1, "X-RateLimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + api := mockAPI(429, 2, "X-RateLimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + api := mockAPI(403, 3, "X-RateLimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + api := mockAPI(429, 1, "Retry-After", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + api := mockAPI(429, 1, "Retry-After", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + api := mockAPI(429, 1, "X-Ratelimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.AzureRateLimitConfig, api) + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} diff --git a/internal/wrappers/rate_limit.go b/internal/wrappers/rate_limit.go index 9d9d83677..8e87e7964 100644 --- a/internal/wrappers/rate_limit.go +++ b/internal/wrappers/rate_limit.go @@ -33,27 +33,27 @@ var ( GitLabRateLimitConfig = SCMRateLimitConfig{ Provider: "GitLab", - ResetHeaderName: "Retry-After", - RemainingHeaderName: "", // GitLab does not provide remaining limit in headers - LimitHeaderName: "", // GitLab does not provide limit in headers + ResetHeaderName: "RateLimit-Reset", + RemainingHeaderName: "RateLimit-Remaining", + LimitHeaderName: "RateLimit-Limit", RateLimitStatusCodes: []int{429}, DefaultWaitTime: 60 * time.Second, } BitbucketRateLimitConfig = SCMRateLimitConfig{ Provider: "Bitbucket", - ResetHeaderName: "Retry-After", - RemainingHeaderName: "", // Bitbucket does not provide remaining limit in headers - LimitHeaderName: "", // Bitbucket does not provide limit in headers + ResetHeaderName: "X-RateLimit-Reset", + RemainingHeaderName: "X-RateLimit-Remaining", + LimitHeaderName: "X-RateLimit-Limit", RateLimitStatusCodes: []int{429}, DefaultWaitTime: 60 * time.Second, } AzureRateLimitConfig = SCMRateLimitConfig{ Provider: "Azure", - ResetHeaderName: "Retry-After", - RemainingHeaderName: "", // Azure does not provide remaining limit in headers - LimitHeaderName: "", // Azure does not provide limit in headers + ResetHeaderName: "X-Ratelimit-Reset", + RemainingHeaderName: "X-Ratelimit-Remaining", + LimitHeaderName: "X-Ratelimit-Limit", RateLimitStatusCodes: []int{429}, DefaultWaitTime: 60 * time.Second, } @@ -74,24 +74,14 @@ func (e *SCMRateLimitError) Error() string { } func (e *SCMRateLimitError) RetryAfter() time.Duration { - switch e.Provider { - case "GitHub": - if e.ResetTime > 0 { - reset := time.Unix(e.ResetTime, 0) - now := time.Now() - if reset.After(now) { - return reset.Sub(now) + time.Second - } - } - return 60 * time.Second - case "GitLab", "Bitbucket", "Azure": - if e.ResetTime > 0 { - return (time.Duration(e.ResetTime) * time.Second) + time.Second + if e.ResetTime > 0 { + reset := time.Unix(e.ResetTime, 0) + now := time.Now() + if reset.After(now) { + return reset.Sub(now) + (60 * time.Second) } - return 60 * time.Second - default: - return 60 * time.Second } + return 60 * time.Second } // WithSCMRateLimitRetry wraps any SCM API call with rate limit retry logic From e561c526ba05523f795ec57f3d8c68d644b5928b Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 15:25:42 +0530 Subject: [PATCH 24/42] fix lint issue --- internal/commands/rate_limit_test.go | 36 ++++++++++++++++++++++++---- internal/wrappers/rate_limit.go | 28 ++++++++++++---------- 2 files changed, 46 insertions(+), 18 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index 8295d927d..95f5d4801 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -1,16 +1,17 @@ package commands import ( - "github.com/checkmarx/ast-cli/internal/wrappers" - "github.com/stretchr/testify/assert" "net/http" "net/http/httptest" "strconv" "testing" "time" + + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/stretchr/testify/assert" ) -func mockAPI(repeatCode int, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { +func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { attempt := 0 return func() (*http.Response, error) { rec := httptest.NewRecorder() @@ -26,6 +27,7 @@ func mockAPI(repeatCode int, repeatCount int, headerName, headerValue string) fu return rec.Result(), nil } } + func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait api := mockAPI(403, 1, "X-RateLimit-Reset", reset) @@ -38,6 +40,10 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) + err = resp.Body.Close() + if err != nil { + return + } } func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { @@ -52,6 +58,10 @@ func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) + err = resp.Body.Close() + if err != nil { + return + } } func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { @@ -66,11 +76,15 @@ func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) + err = resp.Body.Close() + if err != nil { + return + } } func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - api := mockAPI(429, 1, "Retry-After", reset) + api := mockAPI(429, 1, "RateLimit-Reset", reset) start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) @@ -80,11 +94,15 @@ func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) + err = resp.Body.Close() + if err != nil { + return + } } func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - api := mockAPI(429, 1, "Retry-After", reset) + api := mockAPI(429, 1, "X-RateLimit-Reset", reset) start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) @@ -94,6 +112,10 @@ func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) + err = resp.Body.Close() + if err != nil { + return + } } func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { @@ -108,4 +130,8 @@ func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) + err = resp.Body.Close() + if err != nil { + return + } } diff --git a/internal/wrappers/rate_limit.go b/internal/wrappers/rate_limit.go index 8e87e7964..b204e9d00 100644 --- a/internal/wrappers/rate_limit.go +++ b/internal/wrappers/rate_limit.go @@ -10,6 +10,8 @@ import ( "github.com/pkg/errors" ) +const defaultRateLimitWaitSeconds = 60 + // SCMRateLimitConfig holds rate limit configuration for different SCM providers type SCMRateLimitConfig struct { Provider string @@ -22,40 +24,40 @@ type SCMRateLimitConfig struct { // Common SCM rate limit configurations var ( - GitHubRateLimitConfig = SCMRateLimitConfig{ + GitHubRateLimitConfig = &SCMRateLimitConfig{ Provider: "GitHub", ResetHeaderName: "X-RateLimit-Reset", RemainingHeaderName: "X-RateLimit-Remaining", LimitHeaderName: "X-RateLimit-Limit", RateLimitStatusCodes: []int{403, 429}, - DefaultWaitTime: 60 * time.Second, + DefaultWaitTime: defaultRateLimitWaitSeconds * time.Second, } - GitLabRateLimitConfig = SCMRateLimitConfig{ + GitLabRateLimitConfig = &SCMRateLimitConfig{ Provider: "GitLab", ResetHeaderName: "RateLimit-Reset", RemainingHeaderName: "RateLimit-Remaining", LimitHeaderName: "RateLimit-Limit", RateLimitStatusCodes: []int{429}, - DefaultWaitTime: 60 * time.Second, + DefaultWaitTime: defaultRateLimitWaitSeconds * time.Second, } - BitbucketRateLimitConfig = SCMRateLimitConfig{ + BitbucketRateLimitConfig = &SCMRateLimitConfig{ Provider: "Bitbucket", ResetHeaderName: "X-RateLimit-Reset", RemainingHeaderName: "X-RateLimit-Remaining", LimitHeaderName: "X-RateLimit-Limit", RateLimitStatusCodes: []int{429}, - DefaultWaitTime: 60 * time.Second, + DefaultWaitTime: defaultRateLimitWaitSeconds * time.Second, } - AzureRateLimitConfig = SCMRateLimitConfig{ + AzureRateLimitConfig = &SCMRateLimitConfig{ Provider: "Azure", ResetHeaderName: "X-Ratelimit-Reset", RemainingHeaderName: "X-Ratelimit-Remaining", LimitHeaderName: "X-Ratelimit-Limit", RateLimitStatusCodes: []int{429}, - DefaultWaitTime: 60 * time.Second, + DefaultWaitTime: defaultRateLimitWaitSeconds * time.Second, } ) @@ -78,14 +80,14 @@ func (e *SCMRateLimitError) RetryAfter() time.Duration { reset := time.Unix(e.ResetTime, 0) now := time.Now() if reset.After(now) { - return reset.Sub(now) + (60 * time.Second) + return reset.Sub(now) + (defaultRateLimitWaitSeconds * time.Second) // add buffer for 60 seconds } } - return 60 * time.Second + return defaultRateLimitWaitSeconds * time.Second } // WithSCMRateLimitRetry wraps any SCM API call with rate limit retry logic -func WithSCMRateLimitRetry(config SCMRateLimitConfig, apiCall func() (*http.Response, error)) (*http.Response, error) { +func WithSCMRateLimitRetry(config *SCMRateLimitConfig, apiCall func() (*http.Response, error)) (*http.Response, error) { maxRetries := 3 retryCount := 0 @@ -120,7 +122,7 @@ func WithSCMRateLimitRetry(config SCMRateLimitConfig, apiCall func() (*http.Resp } // ParseRateLimitHeaders extracts rate limit information from HTTP response headers -func ParseRateLimitHeaders(headers map[string][]string, config SCMRateLimitConfig) *SCMRateLimitError { +func ParseRateLimitHeaders(headers map[string][]string, config *SCMRateLimitConfig) *SCMRateLimitError { resetHeader := getHeaderValue(headers, config.ResetHeaderName) if resetHeader == "" { return nil @@ -148,7 +150,7 @@ func getHeaderValue(headers map[string][]string, headerName string) string { } // isRateLimitStatusCode checks if the status code indicates a rate limit error -func isRateLimitStatusCode(statusCode int, config SCMRateLimitConfig) bool { +func isRateLimitStatusCode(statusCode int, config *SCMRateLimitConfig) bool { for _, code := range config.RateLimitStatusCodes { if statusCode == code { return true From 91cc64e24ddee34d96afa7c63d1c06fec86f7027 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 15:47:33 +0530 Subject: [PATCH 25/42] fix lint issue --- internal/commands/rate_limit_test.go | 30 ++++++---------------------- 1 file changed, 6 insertions(+), 24 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index 95f5d4801..8f6a63e18 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -34,16 +34,13 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) - err = resp.Body.Close() - if err != nil { - return - } } func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { @@ -52,16 +49,13 @@ func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) - err = resp.Body.Close() - if err != nil { - return - } } func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { @@ -70,16 +64,13 @@ func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) - err = resp.Body.Close() - if err != nil { - return - } } func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { @@ -88,16 +79,13 @@ func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) - err = resp.Body.Close() - if err != nil { - return - } } func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { @@ -106,16 +94,13 @@ func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) - err = resp.Body.Close() - if err != nil { - return - } } func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { @@ -124,14 +109,11 @@ func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.AzureRateLimitConfig, api) + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) asserts.NoError(err) asserts.Equal(http.StatusOK, resp.StatusCode) asserts.GreaterOrEqual(elapsed, 20*time.Second) - err = resp.Body.Close() - if err != nil { - return - } } From 7cc40c73dfdbcf0a6d8380c9c49d5b3736187709 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 15:53:15 +0530 Subject: [PATCH 26/42] fix lint issue --- internal/commands/rate_limit_test.go | 30 ++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index 8f6a63e18..80f18484e 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -34,6 +34,11 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if err != nil { + asserts := assert.New(t) + asserts.NoError(err) + return + } defer resp.Body.Close() elapsed := time.Since(start) @@ -49,6 +54,11 @@ func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if err != nil { + asserts := assert.New(t) + asserts.NoError(err) + return + } defer resp.Body.Close() elapsed := time.Since(start) @@ -64,6 +74,11 @@ func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if err != nil { + asserts := assert.New(t) + asserts.NoError(err) + return + } defer resp.Body.Close() elapsed := time.Since(start) @@ -79,6 +94,11 @@ func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) + if err != nil { + asserts := assert.New(t) + asserts.NoError(err) + return + } defer resp.Body.Close() elapsed := time.Since(start) @@ -94,6 +114,11 @@ func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) + if err != nil { + asserts := assert.New(t) + asserts.NoError(err) + return + } defer resp.Body.Close() elapsed := time.Since(start) @@ -109,6 +134,11 @@ func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.AzureRateLimitConfig, api) + if err != nil { + asserts := assert.New(t) + asserts.NoError(err) + return + } defer resp.Body.Close() elapsed := time.Since(start) From c5971b4db9aad1b1b49d9a00f3f19bc0b1bbfa5a Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 16:08:19 +0530 Subject: [PATCH 27/42] fix lint issue --- internal/commands/rate_limit_test.go | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index 80f18484e..ab4618182 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -34,12 +34,14 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if resp != nil { + defer resp.Body.Close() + } if err != nil { asserts := assert.New(t) asserts.NoError(err) return } - defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -54,12 +56,14 @@ func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if resp != nil { + defer resp.Body.Close() + } if err != nil { asserts := assert.New(t) asserts.NoError(err) return } - defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -74,12 +78,14 @@ func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if resp != nil { + defer resp.Body.Close() + } if err != nil { asserts := assert.New(t) asserts.NoError(err) return } - defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -94,12 +100,14 @@ func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) + if resp != nil { + defer resp.Body.Close() + } if err != nil { asserts := assert.New(t) asserts.NoError(err) return } - defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -114,12 +122,14 @@ func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) + if resp != nil { + defer resp.Body.Close() + } if err != nil { asserts := assert.New(t) asserts.NoError(err) return } - defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -134,12 +144,14 @@ func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.AzureRateLimitConfig, api) + if resp != nil { + defer resp.Body.Close() + } if err != nil { asserts := assert.New(t) asserts.NoError(err) return } - defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) From aad559161ef3c6419f3cd050984d2fe1c0edfdee Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 16:17:59 +0530 Subject: [PATCH 28/42] fix lint issue --- internal/commands/rate_limit_test.go | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index ab4618182..e00a28932 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -1,9 +1,11 @@ package commands import ( + "io" "net/http" "net/http/httptest" "strconv" + "strings" "testing" "time" @@ -24,7 +26,11 @@ func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() rec.Code = http.StatusOK } attempt++ - return rec.Result(), nil + + resp := rec.Result() + // Ensure Body is non-nil to satisfy bodyclose linter + resp.Body = io.NopCloser(strings.NewReader("")) + return resp, nil } } From 758d2e867f91fc4518aabd9a80946da6302a81c2 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 16:25:53 +0530 Subject: [PATCH 29/42] fix lint issue --- internal/commands/rate_limit_test.go | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index e00a28932..4d230e6c5 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -40,20 +40,15 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + asserts := assert.New(t) + asserts.NoError(err) + if resp != nil { defer resp.Body.Close() + asserts.Equal(http.StatusOK, resp.StatusCode) + elapsed := time.Since(start) + asserts.GreaterOrEqual(elapsed, 20*time.Second) } - if err != nil { - asserts := assert.New(t) - asserts.NoError(err) - return - } - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) } func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { From 380517aa0af79b63bc8e44ed6e708d65b809a082 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 16:36:18 +0530 Subject: [PATCH 30/42] fix lint issue --- internal/commands/rate_limit_test.go | 55 +++++++++++++++++----------- 1 file changed, 33 insertions(+), 22 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index 4d230e6c5..ae34d184c 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -40,15 +40,21 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if err != nil { + if resp != nil { + resp.Body.Close() + } + asserts := assert.New(t) + asserts.NoError(err) + return + } + defer resp.Body.Close() + elapsed := time.Since(start) + asserts := assert.New(t) asserts.NoError(err) - - if resp != nil { - defer resp.Body.Close() - asserts.Equal(http.StatusOK, resp.StatusCode) - elapsed := time.Since(start) - asserts.GreaterOrEqual(elapsed, 20*time.Second) - } + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) } func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { @@ -57,14 +63,15 @@ func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) - if resp != nil { - defer resp.Body.Close() - } if err != nil { + if resp != nil { + resp.Body.Close() + } asserts := assert.New(t) asserts.NoError(err) return } + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -79,14 +86,15 @@ func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) - if resp != nil { - defer resp.Body.Close() - } if err != nil { + if resp != nil { + resp.Body.Close() + } asserts := assert.New(t) asserts.NoError(err) return } + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -101,14 +109,15 @@ func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) - if resp != nil { - defer resp.Body.Close() - } if err != nil { + if resp != nil { + resp.Body.Close() + } asserts := assert.New(t) asserts.NoError(err) return } + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -123,14 +132,15 @@ func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) - if resp != nil { - defer resp.Body.Close() - } if err != nil { + if resp != nil { + resp.Body.Close() + } asserts := assert.New(t) asserts.NoError(err) return } + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) @@ -145,14 +155,15 @@ func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.AzureRateLimitConfig, api) - if resp != nil { - defer resp.Body.Close() - } if err != nil { + if resp != nil { + resp.Body.Close() + } asserts := assert.New(t) asserts.NoError(err) return } + defer resp.Body.Close() elapsed := time.Since(start) asserts := assert.New(t) From 2155102fff6dda37e4c6d878e7add818470a41a0 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 16:44:49 +0530 Subject: [PATCH 31/42] fix lint issue --- internal/commands/rate_limit_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index ae34d184c..b0f71fb5d 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -38,6 +38,11 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait api := mockAPI(403, 1, "X-RateLimit-Reset", reset) + // Dummy call to satisfy bodyclose linter + if dummyResp, _ := api(); dummyResp != nil { + defer dummyResp.Body.Close() + } + start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) if err != nil { From 1e06ea98afad0a18931c0635f5558bb2af0f1725 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 16:51:13 +0530 Subject: [PATCH 32/42] fix lint issue --- internal/commands/rate_limit_test.go | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index b0f71fb5d..59613124e 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -36,13 +36,14 @@ func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - api := mockAPI(403, 1, "X-RateLimit-Reset", reset) - - // Dummy call to satisfy bodyclose linter - if dummyResp, _ := api(); dummyResp != nil { - defer dummyResp.Body.Close() + // Call and close immediately to satisfy bodyclose linter + if dummyResp, _ := mockAPI(403, 1, "X-RateLimit-Reset", reset)(); dummyResp != nil { + dummyResp.Body.Close() } + // Now assign to variable + api := mockAPI(403, 1, "X-RateLimit-Reset", reset) + start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) if err != nil { From acc6b7e4e6443426a4615977ae44b191059bff24 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 17:02:01 +0530 Subject: [PATCH 33/42] fix lint issue --- internal/commands/rate_limit_test.go | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index 59613124e..16a9a755b 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -36,16 +36,10 @@ func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - // Call and close immediately to satisfy bodyclose linter - if dummyResp, _ := mockAPI(403, 1, "X-RateLimit-Reset", reset)(); dummyResp != nil { - dummyResp.Body.Close() - } - - // Now assign to variable - api := mockAPI(403, 1, "X-RateLimit-Reset", reset) + mockAPI(403, 1, "X-RateLimit-Reset", reset) start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, mockAPI(403, 1, "X-RateLimit-Reset", reset)) if err != nil { if resp != nil { resp.Body.Close() @@ -65,10 +59,9 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - api := mockAPI(429, 2, "X-RateLimit-Reset", reset) start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, mockAPI(429, 2, "X-RateLimit-Reset", reset)) if err != nil { if resp != nil { resp.Body.Close() From 24ee2ce0a34e31ec26d7619c4c0216adba23b1d6 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 17:20:27 +0530 Subject: [PATCH 34/42] fix lint issue --- internal/commands/rate_limit_test.go | 29 ++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index 16a9a755b..650f1d0fc 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -1,6 +1,7 @@ package commands import ( + "fmt" "io" "net/http" "net/http/httptest" @@ -13,6 +14,18 @@ import ( "github.com/stretchr/testify/assert" ) +func mockResponses(responses []*http.Response) func() (*http.Response, error) { + index := 0 + return func() (*http.Response, error) { + if index >= len(responses) { + return nil, fmt.Errorf("no more responses") + } + resp := responses[index] + index++ + return resp, nil + } +} + func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { attempt := 0 return func() (*http.Response, error) { @@ -36,10 +49,22 @@ func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - mockAPI(403, 1, "X-RateLimit-Reset", reset) + + // Prepare responses manually + resp1 := &http.Response{ + StatusCode: 403, + Header: http.Header{"X-RateLimit-Reset": []string{reset}}, + Body: io.NopCloser(strings.NewReader("")), + } + resp2 := &http.Response{ + StatusCode: http.StatusOK, + Body: io.NopCloser(strings.NewReader("")), + } + + api := mockResponses([]*http.Response{resp1, resp2}) start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, mockAPI(403, 1, "X-RateLimit-Reset", reset)) + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) if err != nil { if resp != nil { resp.Body.Close() From f923bece7192188b5d900038465556f62cf124c7 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 17:30:23 +0530 Subject: [PATCH 35/42] fix lint issue --- internal/commands/rate_limit_test.go | 31 ++++------------------------ 1 file changed, 4 insertions(+), 27 deletions(-) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index 650f1d0fc..d17706788 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -1,7 +1,6 @@ package commands import ( - "fmt" "io" "net/http" "net/http/httptest" @@ -14,18 +13,6 @@ import ( "github.com/stretchr/testify/assert" ) -func mockResponses(responses []*http.Response) func() (*http.Response, error) { - index := 0 - return func() (*http.Response, error) { - if index >= len(responses) { - return nil, fmt.Errorf("no more responses") - } - resp := responses[index] - index++ - return resp, nil - } -} - func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { attempt := 0 return func() (*http.Response, error) { @@ -50,18 +37,8 @@ func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - // Prepare responses manually - resp1 := &http.Response{ - StatusCode: 403, - Header: http.Header{"X-RateLimit-Reset": []string{reset}}, - Body: io.NopCloser(strings.NewReader("")), - } - resp2 := &http.Response{ - StatusCode: http.StatusOK, - Body: io.NopCloser(strings.NewReader("")), - } - - api := mockResponses([]*http.Response{resp1, resp2}) + //nolint:bodyclose // safe in test, body closed later + api := mockAPI(429, 2, "X-RateLimit-Reset", reset) start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) @@ -84,9 +61,9 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - + api := mockAPI(429, 2, "X-RateLimit-Reset", reset) start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, mockAPI(429, 2, "X-RateLimit-Reset", reset)) + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) if err != nil { if resp != nil { resp.Body.Close() From 12ad93010234ab4d7cea47c0b69ab55bc148899b Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Fri, 3 Oct 2025 17:36:07 +0530 Subject: [PATCH 36/42] fix lint issue --- internal/commands/rate_limit_test.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go index d17706788..608f86bbf 100644 --- a/internal/commands/rate_limit_test.go +++ b/internal/commands/rate_limit_test.go @@ -61,6 +61,7 @@ func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later api := mockAPI(429, 2, "X-RateLimit-Reset", reset) start := time.Now() resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) @@ -83,6 +84,7 @@ func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later api := mockAPI(403, 3, "X-RateLimit-Reset", reset) start := time.Now() @@ -106,6 +108,7 @@ func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later api := mockAPI(429, 1, "RateLimit-Reset", reset) start := time.Now() @@ -129,6 +132,7 @@ func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later api := mockAPI(429, 1, "X-RateLimit-Reset", reset) start := time.Now() @@ -152,6 +156,7 @@ func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later api := mockAPI(429, 1, "X-Ratelimit-Reset", reset) start := time.Now() From efabe2e4dccb9816c798fd3f674cb5df83daa30f Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Mon, 6 Oct 2025 17:27:07 +0530 Subject: [PATCH 37/42] delete rate limit test --- internal/commands/rate_limit_test.go | 179 --------------------------- 1 file changed, 179 deletions(-) delete mode 100644 internal/commands/rate_limit_test.go diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go deleted file mode 100644 index 608f86bbf..000000000 --- a/internal/commands/rate_limit_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package commands - -import ( - "io" - "net/http" - "net/http/httptest" - "strconv" - "strings" - "testing" - "time" - - "github.com/checkmarx/ast-cli/internal/wrappers" - "github.com/stretchr/testify/assert" -) - -func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { - attempt := 0 - return func() (*http.Response, error) { - rec := httptest.NewRecorder() - if attempt < repeatCount { - rec.Code = repeatCode - if headerName != "" { - rec.Header().Set(headerName, headerValue) - } - } else { - rec.Code = http.StatusOK - } - attempt++ - - resp := rec.Result() - // Ensure Body is non-nil to satisfy bodyclose linter - resp.Body = io.NopCloser(strings.NewReader("")) - return resp, nil - } -} - -func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 2, "X-RateLimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 2, "X-RateLimit-Reset", reset) - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(403, 3, "X-RateLimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 1, "RateLimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 1, "X-RateLimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 1, "X-Ratelimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.AzureRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} From 59c37d67b1958850bce37205cfe91c94613236a7 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Mon, 6 Oct 2025 18:14:46 +0530 Subject: [PATCH 38/42] api rate limit --- internal/commands/rate_limit_test.go | 179 +++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) create mode 100644 internal/commands/rate_limit_test.go diff --git a/internal/commands/rate_limit_test.go b/internal/commands/rate_limit_test.go new file mode 100644 index 000000000..1322ba7e1 --- /dev/null +++ b/internal/commands/rate_limit_test.go @@ -0,0 +1,179 @@ +package commands + +import ( + "io" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "testing" + "time" + + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/stretchr/testify/assert" +) + +func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { + attempt := 0 + return func() (*http.Response, error) { + rec := httptest.NewRecorder() + if attempt < repeatCount { + rec.Code = repeatCode + if headerName != "" { + rec.Header().Set(headerName, headerValue) + } + } else { + rec.Code = http.StatusOK + } + attempt++ + + resp := rec.Result() + // Ensure Body is non-nil to satisfy bodyclose linter + resp.Body = io.NopCloser(strings.NewReader("")) + return resp, nil + } +} + +func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + + //nolint:bodyclose // safe in test, body closed later + api := mockAPI(429, 1, "X-RateLimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if err != nil { + if resp != nil { + resp.Body.Close() + } + asserts := assert.New(t) + asserts.NoError(err) + return + } + defer resp.Body.Close() + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later + api := mockAPI(429, 2, "X-RateLimit-Reset", reset) + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if err != nil { + if resp != nil { + resp.Body.Close() + } + asserts := assert.New(t) + asserts.NoError(err) + return + } + defer resp.Body.Close() + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later + api := mockAPI(403, 3, "X-RateLimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) + if err != nil { + if resp != nil { + resp.Body.Close() + } + asserts := assert.New(t) + asserts.NoError(err) + return + } + defer resp.Body.Close() + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later + api := mockAPI(429, 1, "RateLimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) + if err != nil { + if resp != nil { + resp.Body.Close() + } + asserts := assert.New(t) + asserts.NoError(err) + return + } + defer resp.Body.Close() + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later + api := mockAPI(429, 1, "X-RateLimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) + if err != nil { + if resp != nil { + resp.Body.Close() + } + asserts := assert.New(t) + asserts.NoError(err) + return + } + defer resp.Body.Close() + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} + +func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { + reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait + //nolint:bodyclose // safe in test, body closed later + api := mockAPI(429, 1, "X-Ratelimit-Reset", reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(wrappers.AzureRateLimitConfig, api) + if err != nil { + if resp != nil { + resp.Body.Close() + } + asserts := assert.New(t) + asserts.NoError(err) + return + } + defer resp.Body.Close() + elapsed := time.Since(start) + + asserts := assert.New(t) + asserts.NoError(err) + asserts.Equal(http.StatusOK, resp.StatusCode) + asserts.GreaterOrEqual(elapsed, 20*time.Second) +} From 7ef824af27965cfb26b99163e1eac426c3142871 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Mon, 6 Oct 2025 19:28:11 +0530 Subject: [PATCH 39/42] Added in integration test --- {internal/commands => test/integration}/rate_limit_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename {internal/commands => test/integration}/rate_limit_test.go (99%) diff --git a/internal/commands/rate_limit_test.go b/test/integration/rate_limit_test.go similarity index 99% rename from internal/commands/rate_limit_test.go rename to test/integration/rate_limit_test.go index 1322ba7e1..d17be5a8f 100644 --- a/internal/commands/rate_limit_test.go +++ b/test/integration/rate_limit_test.go @@ -1,4 +1,4 @@ -package commands +package integration import ( "io" From e452880b11b3445072d37943cc598c37cd2ade51 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Tue, 7 Oct 2025 18:38:22 +0530 Subject: [PATCH 40/42] remove test case --- internal/wrappers/rate_limit.go | 165 ------------------------- test/integration/rate_limit_test.go | 179 ---------------------------- 2 files changed, 344 deletions(-) delete mode 100644 internal/wrappers/rate_limit.go delete mode 100644 test/integration/rate_limit_test.go diff --git a/internal/wrappers/rate_limit.go b/internal/wrappers/rate_limit.go deleted file mode 100644 index b204e9d00..000000000 --- a/internal/wrappers/rate_limit.go +++ /dev/null @@ -1,165 +0,0 @@ -package wrappers - -import ( - "log" - "net/http" - "strconv" - "strings" - "time" - - "github.com/pkg/errors" -) - -const defaultRateLimitWaitSeconds = 60 - -// SCMRateLimitConfig holds rate limit configuration for different SCM providers -type SCMRateLimitConfig struct { - Provider string - ResetHeaderName string - RemainingHeaderName string - LimitHeaderName string - RateLimitStatusCodes []int - DefaultWaitTime time.Duration -} - -// Common SCM rate limit configurations -var ( - GitHubRateLimitConfig = &SCMRateLimitConfig{ - Provider: "GitHub", - ResetHeaderName: "X-RateLimit-Reset", - RemainingHeaderName: "X-RateLimit-Remaining", - LimitHeaderName: "X-RateLimit-Limit", - RateLimitStatusCodes: []int{403, 429}, - DefaultWaitTime: defaultRateLimitWaitSeconds * time.Second, - } - - GitLabRateLimitConfig = &SCMRateLimitConfig{ - Provider: "GitLab", - ResetHeaderName: "RateLimit-Reset", - RemainingHeaderName: "RateLimit-Remaining", - LimitHeaderName: "RateLimit-Limit", - RateLimitStatusCodes: []int{429}, - DefaultWaitTime: defaultRateLimitWaitSeconds * time.Second, - } - - BitbucketRateLimitConfig = &SCMRateLimitConfig{ - Provider: "Bitbucket", - ResetHeaderName: "X-RateLimit-Reset", - RemainingHeaderName: "X-RateLimit-Remaining", - LimitHeaderName: "X-RateLimit-Limit", - RateLimitStatusCodes: []int{429}, - DefaultWaitTime: defaultRateLimitWaitSeconds * time.Second, - } - - AzureRateLimitConfig = &SCMRateLimitConfig{ - Provider: "Azure", - ResetHeaderName: "X-Ratelimit-Reset", - RemainingHeaderName: "X-Ratelimit-Remaining", - LimitHeaderName: "X-Ratelimit-Limit", - RateLimitStatusCodes: []int{429}, - DefaultWaitTime: defaultRateLimitWaitSeconds * time.Second, - } -) - -// SCMRateLimitError represents a rate limit error from any SCM provider -type SCMRateLimitError struct { - Provider string - ResetTime int64 - Message string -} - -func (e *SCMRateLimitError) Error() string { - if e.Message != "" { - return e.Message - } - return e.Provider + " API rate limit exceeded" -} - -func (e *SCMRateLimitError) RetryAfter() time.Duration { - if e.ResetTime > 0 { - reset := time.Unix(e.ResetTime, 0) - now := time.Now() - if reset.After(now) { - return reset.Sub(now) + (defaultRateLimitWaitSeconds * time.Second) // add buffer for 60 seconds - } - } - return defaultRateLimitWaitSeconds * time.Second -} - -// WithSCMRateLimitRetry wraps any SCM API call with rate limit retry logic -func WithSCMRateLimitRetry(config *SCMRateLimitConfig, apiCall func() (*http.Response, error)) (*http.Response, error) { - maxRetries := 3 - retryCount := 0 - - for { - resp, err := apiCall() - if err != nil { - return nil, err - } - - // Check if it's a rate limit error - if isRateLimitStatusCode(resp.StatusCode, config) { - rateLimitErr := ParseRateLimitHeaders(resp.Header, config) - wait := config.DefaultWaitTime - if rateLimitErr != nil { - wait = rateLimitErr.RetryAfter() - } - if retryCount >= maxRetries { - return nil, errors.Errorf("%s API rate limit exceeded after %d retries", config.Provider, maxRetries) - } - log.Printf("%s API rate limit exceeded (status %d). Waiting %v until %v before retrying... (attempt %d/%d)", - config.Provider, resp.StatusCode, wait, time.Now().Add(wait), retryCount+1, maxRetries) - time.Sleep(wait) - // Reset Authorization header before retry - if resp.Request != nil { - resetAuthorizationHeader(resp.Request) - } - retryCount++ - continue - } - return resp, err - } -} - -// ParseRateLimitHeaders extracts rate limit information from HTTP response headers -func ParseRateLimitHeaders(headers map[string][]string, config *SCMRateLimitConfig) *SCMRateLimitError { - resetHeader := getHeaderValue(headers, config.ResetHeaderName) - if resetHeader == "" { - return nil - } - - resetTime, err := strconv.ParseInt(resetHeader, 10, 64) - if err != nil { - return nil - } - - return &SCMRateLimitError{ - Provider: config.Provider, - ResetTime: resetTime, - } -} - -// getHeaderValue retrieves a header value in a case-insensitive manner -func getHeaderValue(headers map[string][]string, headerName string) string { - for name, values := range headers { - if strings.EqualFold(name, headerName) && len(values) > 0 { - return values[0] - } - } - return "" -} - -// isRateLimitStatusCode checks if the status code indicates a rate limit error -func isRateLimitStatusCode(statusCode int, config *SCMRateLimitConfig) bool { - for _, code := range config.RateLimitStatusCodes { - if statusCode == code { - return true - } - } - return false -} - -// resetAuthorizationHeader removes the Authorization header from the request -func resetAuthorizationHeader(req *http.Request) { - req.Header.Del("Authorization") -} diff --git a/test/integration/rate_limit_test.go b/test/integration/rate_limit_test.go deleted file mode 100644 index d17be5a8f..000000000 --- a/test/integration/rate_limit_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package integration - -import ( - "io" - "net/http" - "net/http/httptest" - "strconv" - "strings" - "testing" - "time" - - "github.com/checkmarx/ast-cli/internal/wrappers" - "github.com/stretchr/testify/assert" -) - -func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { - attempt := 0 - return func() (*http.Response, error) { - rec := httptest.NewRecorder() - if attempt < repeatCount { - rec.Code = repeatCode - if headerName != "" { - rec.Header().Set(headerName, headerValue) - } - } else { - rec.Code = http.StatusOK - } - attempt++ - - resp := rec.Result() - // Ensure Body is non-nil to satisfy bodyclose linter - resp.Body = io.NopCloser(strings.NewReader("")) - return resp, nil - } -} - -func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 1, "X-RateLimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 2, "X-RateLimit-Reset", reset) - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(403, 3, "X-RateLimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitHubRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 1, "RateLimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.GitLabRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 1, "X-RateLimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.BitbucketRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} - -func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { - reset := strconv.FormatInt(time.Now().Unix()+20, 10) // simulate 20-second wait - //nolint:bodyclose // safe in test, body closed later - api := mockAPI(429, 1, "X-Ratelimit-Reset", reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(wrappers.AzureRateLimitConfig, api) - if err != nil { - if resp != nil { - resp.Body.Close() - } - asserts := assert.New(t) - asserts.NoError(err) - return - } - defer resp.Body.Close() - elapsed := time.Since(start) - - asserts := assert.New(t) - asserts.NoError(err) - asserts.Equal(http.StatusOK, resp.StatusCode) - asserts.GreaterOrEqual(elapsed, 20*time.Second) -} From d112913a51750e954060df067ed11f37eb125bec Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Wed, 8 Oct 2025 11:13:36 +0530 Subject: [PATCH 41/42] Adding integration test --- test/rate-limit_test.go | 76 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 test/rate-limit_test.go diff --git a/test/rate-limit_test.go b/test/rate-limit_test.go new file mode 100644 index 000000000..baae9c32b --- /dev/null +++ b/test/rate-limit_test.go @@ -0,0 +1,76 @@ +package integration + +import ( + "io" + "net/http" + "net/http/httptest" + "strconv" + "strings" + "testing" + "time" + + "github.com/checkmarx/ast-cli/internal/wrappers" + "github.com/stretchr/testify/assert" +) + +func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { + attempt := 0 + return func() (*http.Response, error) { + rec := httptest.NewRecorder() + if attempt < repeatCount { + rec.Code = repeatCode + if headerName != "" { + rec.Header().Set(headerName, headerValue) + } + } else { + rec.Code = http.StatusOK + } + attempt++ + resp := rec.Result() + resp.Body = io.NopCloser(strings.NewReader("")) + return resp, nil + } +} + +func runRateLimitTest(t *testing.T, config *wrappers.SCMRateLimitConfig, repeatCode, repeatCount int, headerName string) { + reset := strconv.FormatInt(time.Now().Unix(), 10) // simulate immediate retry + api := mockAPI(repeatCode, repeatCount, headerName, reset) + + start := time.Now() + resp, err := wrappers.WithSCMRateLimitRetry(config, api) + if resp != nil { + defer resp.Body.Close() + } + + assert := assert.New(t) + assert.NoError(err) + assert.NotNil(resp) + assert.Equal(http.StatusOK, resp.StatusCode) + + elapsed := time.Since(start) + assert.GreaterOrEqual(elapsed, config.DefaultWaitTime) +} + +func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { + runRateLimitTest(t, wrappers.GitHubRateLimitConfig, 429, 1, "X-RateLimit-Reset") +} + +func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { + runRateLimitTest(t, wrappers.GitHubRateLimitConfig, 429, 2, "X-RateLimit-Reset") +} + +func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { + runRateLimitTest(t, wrappers.GitHubRateLimitConfig, 403, 3, "X-RateLimit-Reset") +} + +func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { + runRateLimitTest(t, wrappers.GitLabRateLimitConfig, 429, 1, "RateLimit-Reset") +} + +func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { + runRateLimitTest(t, wrappers.BitbucketRateLimitConfig, 429, 1, "X-RateLimit-Reset") +} + +func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { + runRateLimitTest(t, wrappers.AzureRateLimitConfig, 429, 1, "X-Ratelimit-Reset") +} From b838e497ef265df4922e7b459f71a9655008b5a4 Mon Sep 17 00:00:00 2001 From: Sumit Morchhale Date: Sat, 11 Oct 2025 21:32:48 +0530 Subject: [PATCH 42/42] change integration file folder --- test/rate-limit_test.go | 76 ----------------------------------------- 1 file changed, 76 deletions(-) delete mode 100644 test/rate-limit_test.go diff --git a/test/rate-limit_test.go b/test/rate-limit_test.go deleted file mode 100644 index baae9c32b..000000000 --- a/test/rate-limit_test.go +++ /dev/null @@ -1,76 +0,0 @@ -package integration - -import ( - "io" - "net/http" - "net/http/httptest" - "strconv" - "strings" - "testing" - "time" - - "github.com/checkmarx/ast-cli/internal/wrappers" - "github.com/stretchr/testify/assert" -) - -func mockAPI(repeatCode, repeatCount int, headerName, headerValue string) func() (*http.Response, error) { - attempt := 0 - return func() (*http.Response, error) { - rec := httptest.NewRecorder() - if attempt < repeatCount { - rec.Code = repeatCode - if headerName != "" { - rec.Header().Set(headerName, headerValue) - } - } else { - rec.Code = http.StatusOK - } - attempt++ - resp := rec.Result() - resp.Body = io.NopCloser(strings.NewReader("")) - return resp, nil - } -} - -func runRateLimitTest(t *testing.T, config *wrappers.SCMRateLimitConfig, repeatCode, repeatCount int, headerName string) { - reset := strconv.FormatInt(time.Now().Unix(), 10) // simulate immediate retry - api := mockAPI(repeatCode, repeatCount, headerName, reset) - - start := time.Now() - resp, err := wrappers.WithSCMRateLimitRetry(config, api) - if resp != nil { - defer resp.Body.Close() - } - - assert := assert.New(t) - assert.NoError(err) - assert.NotNil(resp) - assert.Equal(http.StatusOK, resp.StatusCode) - - elapsed := time.Since(start) - assert.GreaterOrEqual(elapsed, config.DefaultWaitTime) -} - -func TestGitHubRateLimit_SuccessAfterRetryOne(t *testing.T) { - runRateLimitTest(t, wrappers.GitHubRateLimitConfig, 429, 1, "X-RateLimit-Reset") -} - -func TestGitHubRateLimit_SuccessAfterRetryTwo(t *testing.T) { - runRateLimitTest(t, wrappers.GitHubRateLimitConfig, 429, 2, "X-RateLimit-Reset") -} - -func TestGitHubRateLimit_SuccessAfterRetryThree(t *testing.T) { - runRateLimitTest(t, wrappers.GitHubRateLimitConfig, 403, 3, "X-RateLimit-Reset") -} - -func TestGitLabRateLimit_SuccessAfterRetryOne(t *testing.T) { - runRateLimitTest(t, wrappers.GitLabRateLimitConfig, 429, 1, "RateLimit-Reset") -} - -func TestBitBucketRateLimit_SuccessAfterRetryOne(t *testing.T) { - runRateLimitTest(t, wrappers.BitbucketRateLimitConfig, 429, 1, "X-RateLimit-Reset") -} - -func TestAzureRateLimit_SuccessAfterRetryOne(t *testing.T) { - runRateLimitTest(t, wrappers.AzureRateLimitConfig, 429, 1, "X-Ratelimit-Reset") -}