Skip to content

Commit c43c6d2

Browse files
Improve test coverage for circuit breaker package
Add four new test functions to cover previously untested paths: - TestCircuitBreakerState_String: verifies all String() representations including the defensive UNKNOWN default case (circuitBreakerState(99)) - TestFormatResetAt: directly tests the formatResetAt helper for both zero time (returns 'unknown') and non-zero time (RFC3339 + duration hint) - TestIsRateLimitText_Direct: directly tests isRateLimitText with all five match patterns plus edge cases (case insensitivity, empty string, 'rate limit' without qualifying context) - TestCircuitBreaker_RecordRateLimitWhenAlreadyOpen: tests the OPEN→OPEN path in RecordRateLimit, verifying resetAt is updated on subsequent rate-limit errors while the circuit remains OPEN Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent ca03645 commit c43c6d2

1 file changed

Lines changed: 129 additions & 0 deletions

File tree

internal/server/circuit_breaker_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,3 +433,132 @@ func TestExtractRateLimitErrorText(t *testing.T) {
433433
assert.Equal(t, "rate limit exceeded", extractRateLimitErrorText(result))
434434
})
435435
}
436+
437+
// TestCircuitBreakerState_String verifies the string representation of each circuit breaker state.
438+
func TestCircuitBreakerState_String(t *testing.T) {
439+
t.Parallel()
440+
441+
tests := []struct {
442+
state circuitBreakerState
443+
want string
444+
}{
445+
{circuitClosed, "CLOSED"},
446+
{circuitOpen, "OPEN"},
447+
{circuitHalfOpen, "HALF-OPEN"},
448+
{circuitBreakerState(99), "UNKNOWN"},
449+
}
450+
451+
for _, tt := range tests {
452+
t.Run(tt.want, func(t *testing.T) {
453+
t.Parallel()
454+
assert.Equal(t, tt.want, tt.state.String())
455+
})
456+
}
457+
}
458+
459+
// TestFormatResetAt verifies the formatResetAt helper function.
460+
func TestFormatResetAt(t *testing.T) {
461+
t.Parallel()
462+
463+
t.Run("zero time returns 'unknown'", func(t *testing.T) {
464+
t.Parallel()
465+
assert.Equal(t, "unknown", formatResetAt(time.Time{}))
466+
})
467+
468+
t.Run("non-zero time includes RFC3339 timestamp and duration hint", func(t *testing.T) {
469+
t.Parallel()
470+
resetTime := time.Date(2026, 1, 1, 12, 0, 0, 0, time.UTC)
471+
result := formatResetAt(resetTime)
472+
assert.Contains(t, result, "2026-01-01T12:00:00Z", "should contain RFC3339-formatted time")
473+
assert.Contains(t, result, "in ", "should contain duration hint")
474+
})
475+
}
476+
477+
// TestIsRateLimitText_Direct directly verifies isRateLimitText with each pattern and edge cases.
478+
func TestIsRateLimitText_Direct(t *testing.T) {
479+
t.Parallel()
480+
481+
tests := []struct {
482+
name string
483+
text string
484+
want bool
485+
}{
486+
{
487+
name: "rate limit exceeded",
488+
text: "403 API rate limit exceeded",
489+
want: true,
490+
},
491+
{
492+
name: "rate limit combined with 403 status",
493+
text: "error: rate limit triggered, status 403",
494+
want: true,
495+
},
496+
{
497+
name: "api rate limit phrase",
498+
text: "api rate limit reached",
499+
want: true,
500+
},
501+
{
502+
name: "secondary rate limit phrase",
503+
text: "secondary rate limit triggered",
504+
want: true,
505+
},
506+
{
507+
name: "too many requests phrase",
508+
text: "too many requests, please slow down",
509+
want: true,
510+
},
511+
{
512+
name: "case insensitive match",
513+
text: "RATE LIMIT EXCEEDED",
514+
want: true,
515+
},
516+
{
517+
name: "rate limit phrase without 403 or qualifier",
518+
text: "rate limit information page",
519+
want: false,
520+
},
521+
{
522+
name: "unrelated error",
523+
text: "repository not found",
524+
want: false,
525+
},
526+
{
527+
name: "empty string",
528+
text: "",
529+
want: false,
530+
},
531+
}
532+
533+
for _, tt := range tests {
534+
t.Run(tt.name, func(t *testing.T) {
535+
t.Parallel()
536+
assert.Equal(t, tt.want, isRateLimitText(tt.text))
537+
})
538+
}
539+
}
540+
541+
// TestCircuitBreaker_RecordRateLimitWhenAlreadyOpen verifies that calling RecordRateLimit
542+
// on an already-OPEN circuit keeps it OPEN and updates the reset time.
543+
func TestCircuitBreaker_RecordRateLimitWhenAlreadyOpen(t *testing.T) {
544+
t.Parallel()
545+
546+
fakeNow := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
547+
cb := newCircuitBreaker("test", 1, time.Hour)
548+
cb.nowFunc = func() time.Time { return fakeNow }
549+
550+
initialReset := fakeNow.Add(30 * time.Second)
551+
cb.RecordRateLimit(initialReset)
552+
require.Equal(t, circuitOpen, cb.State(), "should be OPEN after threshold errors")
553+
554+
// Record another rate limit while already OPEN with a later reset time.
555+
laterReset := fakeNow.Add(90 * time.Second)
556+
cb.RecordRateLimit(laterReset)
557+
assert.Equal(t, circuitOpen, cb.State(), "should remain OPEN")
558+
559+
// The reset time should be updated to the later value.
560+
cb.mu.Lock()
561+
gotReset := cb.resetAt
562+
cb.mu.Unlock()
563+
assert.Equal(t, laterReset, gotReset, "resetAt should be updated to the later reset time")
564+
}

0 commit comments

Comments
 (0)