diff --git a/backend/services/verification/service.go b/backend/services/verification/service.go index ebfdd25..5ed7bfe 100644 --- a/backend/services/verification/service.go +++ b/backend/services/verification/service.go @@ -70,7 +70,7 @@ func (s *Service) Issue(request IssueRequest, remoteIP string) (Challenge, error allowed, release, limitErr := s.allow(request, remoteIP) if limitErr != nil { s.record(EventInput{Purpose: request.Purpose, Action: models.VerificationActionIssue, Outcome: models.VerificationOutcomeFailure, Account: request.Account, Email: request.Email, RemoteIP: remoteIP}) - return Challenge{}, ErrServiceDown + return Challenge{}, fmt.Errorf("%w: %w", ErrServiceDown, limitErr) } if !allowed { s.record(EventInput{Purpose: request.Purpose, Action: models.VerificationActionIssue, Outcome: models.VerificationOutcomeBlocked, Account: request.Account, Email: request.Email, RemoteIP: remoteIP}) diff --git a/backend/services/verification/service_test.go b/backend/services/verification/service_test.go index deff19d..2030686 100644 --- a/backend/services/verification/service_test.go +++ b/backend/services/verification/service_test.go @@ -44,6 +44,33 @@ func TestServiceRequiresDelivery(t *testing.T) { } } +func TestServicePreservesRateLimitFailureWithoutSending(t *testing.T) { + store, err := NewStore(4) + if err != nil { + t.Fatal(err) + } + sent := false + service, err := NewService(store, func(_ Challenge, _ string) error { + sent = true + return nil + }, nil) + if err != nil { + t.Fatal(err) + } + service.SetRateLimitStore(&PersistentRateLimiter{}) + _, err = service.Issue(IssueRequest{ + Purpose: PurposeExternalEmailBind, + Account: "user", + Email: "user@example.com", + }, "192.0.2.10") + if !errors.Is(err, ErrServiceDown) || !errors.Is(err, ErrRateLimitStoreUnavailable) { + t.Fatalf("error chain lost: %v", err) + } + if sent { + t.Fatal("delivery ran after rate limit storage failed") + } +} + func TestServiceAppliesIndependentAccountEmailAndIPLimits(t *testing.T) { store, err := NewStore(64) if err != nil {