@@ -11,9 +11,14 @@ import (
1111 "fmt"
1212 "strings"
1313 "testing"
14+ "time"
1415
1516 "github.com/google/go-github/v75/github"
1617 "golang.org/x/oauth2"
18+ "google.golang.org/grpc/codes"
19+ "google.golang.org/grpc/status"
20+
21+ "github.com/chainguard-dev/terraform-infra-common/pkg/workqueue"
1722)
1823
1924// mockTokenSourceFunc returns a mock token source
@@ -245,3 +250,83 @@ func TestReconciler_GetStateManager(t *testing.T) {
245250func contains (s , substr string ) bool {
246251 return strings .Contains (s , substr )
247252}
253+
254+ func TestReconciler_RateLimitHandling (t * testing.T ) {
255+ tests := []struct {
256+ name string
257+ reconcilerError error
258+ wantRequeue bool
259+ wantRequeueAfter time.Duration
260+ wantErrContains string
261+ }{{
262+ name : "gRPC ResourceExhausted error triggers requeue" ,
263+ reconcilerError : status .Error (codes .ResourceExhausted , "resource exhausted" ),
264+ wantRequeue : true ,
265+ wantRequeueAfter : time .Minute ,
266+ }, {
267+ name : "wrapped gRPC ResourceExhausted error triggers requeue" ,
268+ reconcilerError : fmt .Errorf ("failed to call API: %w" , status .Error (codes .ResourceExhausted , "quota exceeded" )),
269+ wantRequeue : true ,
270+ wantRequeueAfter : time .Minute ,
271+ }, {
272+ name : "GitHub RateLimitError triggers requeue" ,
273+ reconcilerError : & github.RateLimitError {Rate : github.Rate {Reset : github.Timestamp {Time : time .Now ().Add (5 * time .Minute )}}},
274+ wantRequeue : true ,
275+ wantRequeueAfter : 5 * time .Minute ,
276+ }, {
277+ name : "GitHub AbuseRateLimitError with RetryAfter triggers requeue" ,
278+ reconcilerError : & github.AbuseRateLimitError {RetryAfter : ptrDuration (2 * time .Minute )},
279+ wantRequeue : true ,
280+ wantRequeueAfter : 2 * time .Minute ,
281+ }, {
282+ name : "GitHub AbuseRateLimitError without RetryAfter uses default" ,
283+ reconcilerError : & github.AbuseRateLimitError {},
284+ wantRequeue : true ,
285+ wantRequeueAfter : time .Minute ,
286+ }, {
287+ name : "non-rate-limit error does not trigger requeue" ,
288+ reconcilerError : errors .New ("regular error" ),
289+ wantRequeue : false ,
290+ wantErrContains : "regular error" ,
291+ }}
292+
293+ for _ , tt := range tests {
294+ t .Run (tt .name , func (t * testing.T ) {
295+ ctx := context .Background ()
296+ cc := NewClientCache (mockTokenSourceFunc )
297+
298+ r := NewReconciler (cc , WithReconciler (func (_ context.Context , _ * Resource , _ * github.Client ) error {
299+ return tt .reconcilerError
300+ }))
301+
302+ err := r .Reconcile (ctx , "https://github.com/owner/repo/issues/123" )
303+
304+ if tt .wantRequeue {
305+ delay , ok := workqueue .GetRequeueDelay (err )
306+ if ! ok {
307+ t .Errorf ("Expected requeue error, got = %v" , err )
308+ }
309+ // Jitter adds 0% to +10% to the base delay
310+ minDelay := tt .wantRequeueAfter
311+ maxDelay := tt .wantRequeueAfter + (tt .wantRequeueAfter / 10 )
312+ if delay < minDelay || delay > maxDelay {
313+ t .Errorf ("Requeue delay = %v, want between %v and %v" , delay , minDelay , maxDelay )
314+ }
315+ } else {
316+ if err == nil {
317+ t .Error ("Expected error, got nil" )
318+ }
319+ if _ , ok := workqueue .GetRequeueDelay (err ); ok {
320+ t .Errorf ("Did not expect requeue error, got = %v" , err )
321+ }
322+ if tt .wantErrContains != "" && ! contains (err .Error (), tt .wantErrContains ) {
323+ t .Errorf ("Error = %v, want error containing %v" , err , tt .wantErrContains )
324+ }
325+ }
326+ })
327+ }
328+ }
329+
330+ func ptrDuration (d time.Duration ) * time.Duration {
331+ return & d
332+ }
0 commit comments