diff --git a/internal/controller/distribution/handler_test.go b/internal/controller/distribution/handler_test.go index f82b0d7..28a2a6a 100644 --- a/internal/controller/distribution/handler_test.go +++ b/internal/controller/distribution/handler_test.go @@ -364,9 +364,13 @@ func TestHandlerExtractMapsErrorsToProblemResponsesWithoutSensitiveLeakage(t *te name: "extraction unavailable", requestBody: `{"count":1}`, contentType: "application/json", - extractErr: controllerExtraction.ErrUnavailable, - wantStatus: http.StatusServiceUnavailable, - wantCode: "SERVICE_UNAVAILABLE", + extractErr: errors.Join( + controllerExtraction.ErrUnavailable, + domainExtraction.ErrStoreUnavailable, + errors.New("redis dial failed: TOKEN"), + ), + wantStatus: http.StatusServiceUnavailable, + wantCode: "SERVICE_UNAVAILABLE", }, } @@ -410,7 +414,8 @@ func TestHandlerExtractMapsErrorsToProblemResponsesWithoutSensitiveLeakage(t *te if problem.Status != test.wantStatus || problem.Code != test.wantCode { t.Fatalf("problem = %+v", problem) } - if body := response.Body.String(); strings.Contains(body, `"password"`) || strings.Contains(body, "secret") { + if body := response.Body.String(); strings.Contains(body, `"password"`) || + strings.Contains(body, "secret") || strings.Contains(body, "redis") || strings.Contains(body, "TOKEN") { t.Fatalf("error body leaked sensitive data: %s", body) } }) diff --git a/internal/controller/extraction/service.go b/internal/controller/extraction/service.go index 0df7cb9..0d3ad05 100644 --- a/internal/controller/extraction/service.go +++ b/internal/controller/extraction/service.go @@ -140,6 +140,9 @@ func (s *Service) Extract(ctx context.Context, request Request) (Response, error Upstreams: append([]string(nil), request.Filters.Upstreams...), }) if err != nil { + if errors.Is(err, domain.ErrStoreUnavailable) { + return response, errors.Join(ErrUnavailable, err) + } return response, err } diff --git a/internal/controller/extraction/service_test.go b/internal/controller/extraction/service_test.go index 45c758f..5f919c2 100644 --- a/internal/controller/extraction/service_test.go +++ b/internal/controller/extraction/service_test.go @@ -145,6 +145,27 @@ func TestServiceUsesSourceIdentityForEphemeralIdempotency(t *testing.T) { } } +func TestServiceClassifiesStoreUnavailableAndPreservesCause(t *testing.T) { + t.Parallel() + storeErr := errors.Join(domain.ErrStoreUnavailable, errors.New("redis dial failed: TOKEN")) + service, err := NewService(&recordingStore{err: storeErr}, Policy{ + MaxCountPerRequest: 1, + DefaultFulfillment: domain.Partial, + }, allowAllAdmission{}, time.Now) + if err != nil { + t.Fatalf("NewService(): %v", err) + } + + _, err = service.Extract(context.Background(), Request{ + RequestID: "req-1", + ClientID: "client-1", + Count: 1, + }) + if !errors.Is(err, ErrUnavailable) || !errors.Is(err, domain.ErrStoreUnavailable) { + t.Fatalf("Extract() error = %v, want unavailable classification with store cause", err) + } +} + func TestServiceIdempotentReplayKeepsOriginalExtractionTime(t *testing.T) { firstTime := time.Date(2026, 7, 28, 12, 0, 0, 0, time.UTC) secondTime := firstTime.Add(time.Minute) diff --git a/internal/domain/extraction/extraction.go b/internal/domain/extraction/extraction.go index f58caab..3fe7f70 100644 --- a/internal/domain/extraction/extraction.go +++ b/internal/domain/extraction/extraction.go @@ -24,6 +24,7 @@ var ( ErrInsufficientProxies = errors.New("insufficient proxies") ErrIdempotencyConflict = errors.New("idempotency key was reused with a different extraction request") ErrInvalidCommand = errors.New("invalid extraction command") + ErrStoreUnavailable = errors.New("extraction store unavailable") ) type Candidate struct {