@@ -20,49 +20,46 @@ class Finance::StripeRateLimitHandlingTest < ActionDispatch::IntegrationTest
2020
2121 # ==================== Unit Tests for Rate Limit Detection ====================
2222
23- test 'PaymentTransaction detects rate limit from HTTP 429 status code' do
24- transaction = PaymentTransaction . new
23+ test 'StripeChargeService#rate_limit_error? detects rate limit from Stripe error code' do
24+ service = prepare_stripe_charge_service
2525
2626 response = stub (
2727 success? : false ,
28- params : { 'error' => { 'http_code' => 429 } } ,
29- message : 'Rate limit exceeded'
28+ params : { 'error' => { 'code' => 'rate_limit' } }
3029 )
3130
32- assert transaction . send ( :rate_limit_error? , response ) ,
33- 'Should detect rate limit from HTTP 429 status code'
31+ assert service . send ( :rate_limit_error? , response ) ,
32+ 'Should detect rate limit from Stripe rate_limit error code'
3433 end
3534
36- test 'PaymentTransaction detects rate limit from error message ' do
37- transaction = PaymentTransaction . new
35+ test 'StripeChargeService#rate_limit_error? does not false-positive on other errors ' do
36+ service = prepare_stripe_charge_service
3837
3938 response = stub (
4039 success? : false ,
41- params : { } ,
42- message : 'Too many requests - please try again later'
40+ params : { 'error' => { 'code' => 'card_declined' } }
4341 )
4442
45- assert transaction . send ( :rate_limit_error? , response ) ,
46- 'Should detect rate limit from error message '
43+ assert_not service . send ( :rate_limit_error? , response ) ,
44+ 'Should not detect rate limit for other error codes '
4745 end
4846
49- test 'PaymentTransaction does not false-positive on other errors ' do
50- transaction = PaymentTransaction . new
47+ test 'StripeChargeService#rate_limit_error? does not detect rate limit on successful response ' do
48+ service = prepare_stripe_charge_service
5149
5250 response = stub (
53- success? : false ,
54- params : { 'error' => { 'http_code' => 402 } } ,
55- message : 'Card declined'
51+ success? : true ,
52+ params : { }
5653 )
5754
58- assert_not transaction . send ( :rate_limit_error? , response ) ,
59- 'Should not detect rate limit for other error codes '
55+ assert_not service . send ( :rate_limit_error? , response ) ,
56+ 'Should not detect rate limit on successful response '
6057 end
6158
6259 # ==================== Integration Tests for Invoice Charging ====================
6360
64- test 'Invoice charge re-raises RateLimitError when Stripe returns 429 ' do
65- Account . any_instance . expects ( :charge! ) . raises ( Finance :: Payment :: StripeRateLimitError . new )
61+ test 'Invoice charge re-raises RateLimitError when Stripe returns rate limit error ' do
62+ Account . any_instance . expects ( :charge! ) . raises ( create_rate_limit_error )
6663
6764 invoice = prepare_invoice
6865
@@ -103,23 +100,12 @@ class Finance::StripeRateLimitHandlingTest < ActionDispatch::IntegrationTest
103100
104101 # ==================== BillingWorker Retry Logic Tests ====================
105102
106- test 'BillingWorker uses exponential backoff for rate limit errors' do
107- rate_limit_error = Finance ::Payment ::StripeRateLimitError . new
108-
109- # First retry: ~15 seconds (3^1 * 5 = 15)
110- retry_delay_1 = BillingWorker . sidekiq_retry_in_block . call ( 1 , rate_limit_error )
111- assert retry_delay_1 >= 15 , "First retry should be >= 15 seconds, got #{ retry_delay_1 } "
112- assert retry_delay_1 <= 25 , "First retry should be <= 25 seconds (with jitter), got #{ retry_delay_1 } "
103+ test 'BillingWorker uses Sidekiq default exponential backoff for rate limit errors' do
104+ rate_limit_error = create_rate_limit_error
113105
114- # Second retry: ~45 seconds (3^2 * 5 = 45)
115- retry_delay_2 = BillingWorker . sidekiq_retry_in_block . call ( 2 , rate_limit_error )
116- assert retry_delay_2 >= 45 , "Second retry should be >= 45 seconds, got #{ retry_delay_2 } "
117- assert retry_delay_2 <= 55 , "Second retry should be <= 55 seconds (with jitter), got #{ retry_delay_2 } "
118-
119- # Third retry: ~135 seconds (3^3 * 5 = 135)
120- retry_delay_3 = BillingWorker . sidekiq_retry_in_block . call ( 3 , rate_limit_error )
121- assert retry_delay_3 >= 135 , "Third retry should be >= 135 seconds, got #{ retry_delay_3 } "
122- assert retry_delay_3 <= 145 , "Third retry should be <= 145 seconds (with jitter), got #{ retry_delay_3 } "
106+ # Returns nil to use Sidekiq's default exponential backoff
107+ retry_delay = BillingWorker . sidekiq_retry_in_block . call ( 1 , rate_limit_error )
108+ assert_nil retry_delay , "Should return nil to use Sidekiq's default exponential backoff"
123109 end
124110
125111 test 'BillingWorker uses standard retry delay for non-rate-limit errors' do
@@ -135,7 +121,7 @@ class Finance::StripeRateLimitHandlingTest < ActionDispatch::IntegrationTest
135121 # ==================== Job-Level Integration Tests ====================
136122
137123 test 'BillingService re-raises rate limit error and releases lock' do
138- Account . any_instance . expects ( :charge! ) . raises ( Finance :: Payment :: StripeRateLimitError . new )
124+ Account . any_instance . expects ( :charge! ) . raises ( create_rate_limit_error )
139125
140126 invoice = prepare_invoice
141127
@@ -147,13 +133,15 @@ class Finance::StripeRateLimitHandlingTest < ActionDispatch::IntegrationTest
147133 assert_instance_of Finance ::Payment ::StripeRateLimitError , error
148134
149135 # Lock should be released - we can acquire it again immediately
136+ lock_service = Synchronization ::BillingLockService . new ( @buyer . id . to_s )
150137 assert_nothing_raised do
151- Finance :: BillingService . new ( @buyer . id , provider_account_id : @provider . id , now : invoice . due_on ) . send ( :acquire_lock )
138+ lock_service . lock
152139 end
140+ lock_service . unlock
153141 end
154142
155143 test 'rate limit error flow through BillingWorker' do
156- Account . any_instance . expects ( :charge! ) . raises ( Finance :: Payment :: StripeRateLimitError . new )
144+ Account . any_instance . expects ( :charge! ) . raises ( create_rate_limit_error )
157145
158146 invoice = prepare_invoice
159147
@@ -182,7 +170,7 @@ class Finance::StripeRateLimitHandlingTest < ActionDispatch::IntegrationTest
182170 # First attempt: Invoice 1 succeeds, Invoice 2 hits rate limit
183171 call_sequence = sequence ( 'charging' )
184172 Account . any_instance . expects ( :charge! ) . with ( invoice1 . cost , invoice : invoice1 ) . returns ( true ) . in_sequence ( call_sequence )
185- Account . any_instance . expects ( :charge! ) . with ( invoice2 . cost , invoice : invoice2 ) . raises ( Finance :: Payment :: StripeRateLimitError . new ) . in_sequence ( call_sequence )
173+ Account . any_instance . expects ( :charge! ) . with ( invoice2 . cost , invoice : invoice2 ) . raises ( create_rate_limit_error ) . in_sequence ( call_sequence )
186174
187175 # Job fails with rate limit
188176 assert_raises ( Finance ::Payment ::StripeRateLimitError ) do
@@ -214,12 +202,12 @@ class Finance::StripeRateLimitHandlingTest < ActionDispatch::IntegrationTest
214202 invoice = prepare_invoice
215203
216204 # First rate limit error
217- Account . any_instance . expects ( :charge! ) . raises ( Finance :: Payment :: StripeRateLimitError . new )
205+ Account . any_instance . expects ( :charge! ) . raises ( create_rate_limit_error )
218206 assert_raises ( Finance ::Payment ::StripeRateLimitError ) { invoice . charge! }
219207 assert_equal 0 , invoice . reload . charging_retries_count
220208
221209 # Second rate limit error
222- Account . any_instance . expects ( :charge! ) . raises ( Finance :: Payment :: StripeRateLimitError . new )
210+ Account . any_instance . expects ( :charge! ) . raises ( create_rate_limit_error )
223211 assert_raises ( Finance ::Payment ::StripeRateLimitError ) { invoice . charge! }
224212 assert_equal 0 , invoice . reload . charging_retries_count
225213
@@ -231,7 +219,7 @@ class Finance::StripeRateLimitHandlingTest < ActionDispatch::IntegrationTest
231219 invoice = prepare_invoice
232220
233221 # First attempt: rate limit
234- Account . any_instance . expects ( :charge! ) . raises ( Finance :: Payment :: StripeRateLimitError . new )
222+ Account . any_instance . expects ( :charge! ) . raises ( create_rate_limit_error )
235223 assert_raises ( Finance ::Payment ::StripeRateLimitError ) { invoice . charge! }
236224
237225 # Second attempt: success
@@ -246,7 +234,7 @@ class Finance::StripeRateLimitHandlingTest < ActionDispatch::IntegrationTest
246234 invoice = prepare_invoice
247235
248236 # First attempt: rate limit
249- Account . any_instance . expects ( :charge! ) . raises ( Finance :: Payment :: StripeRateLimitError . new )
237+ Account . any_instance . expects ( :charge! ) . raises ( create_rate_limit_error )
250238 assert_raises ( Finance ::Payment ::StripeRateLimitError ) { invoice . charge! }
251239 assert_equal 0 , invoice . reload . charging_retries_count
252240
@@ -260,6 +248,32 @@ class Finance::StripeRateLimitHandlingTest < ActionDispatch::IntegrationTest
260248
261249 private
262250
251+ def prepare_stripe_charge_service ( invoice : nil )
252+ invoice ||= prepare_invoice
253+ gateway = stub ( 'gateway' )
254+ Finance ::StripeChargeService . new (
255+ gateway ,
256+ payment_method_id : 'pm_test' ,
257+ invoice : invoice ,
258+ gateway_options : { }
259+ )
260+ end
261+
262+ def create_rate_limit_error
263+ response = stub (
264+ success? : false ,
265+ params : { 'error' => { 'code' => 'rate_limit' } } ,
266+ message : 'Request rate limit exceeded'
267+ )
268+ payment_metadata = {
269+ invoice_id : 123 ,
270+ buyer_id : 456 ,
271+ payment_method_id : 'pm_test' ,
272+ gateway_options : { }
273+ }
274+ Finance ::Payment ::StripeRateLimitError . new ( response , payment_metadata )
275+ end
276+
263277 def prepare_invoice
264278 prepare_invoice_with_id ( '00000001' )
265279 end
0 commit comments