@@ -171,5 +171,61 @@ public async Task RetryAfterIsGreaterThanTimeoutSetting()
171171 Assert . Equal ( ErrorCode . Timeout , ex . ErrorCode ) ;
172172 Assert . Equal ( "204c855f-dcc0-4270-ba12-c585fc5ef4bf" , ex . RequestId ) ;
173173 }
174+
175+ // Regression: TimeSpan.Seconds returns only the seconds component (0-59),
176+ // so a timeout of 60+ seconds would incorrectly compare as 0 and always
177+ // throw a timeout exception instead of retrying.
178+ [ Fact ]
179+ public async Task RetryWorksWithTimeoutGreaterThanOrEqualTo60Seconds ( )
180+ {
181+ var config = new Config ( apiKey : "TEST_bTYAskEX6tD7vv6u/cZ/M4LaUSWBJ219+8S1jgFcnkk" , timeout : TimeSpan . FromSeconds ( 60 ) , retries : 1 ) ;
182+ var mockShipEngineFixture = new MockShipEngineFixture ( config ) ;
183+
184+ mockShipEngineFixture . MockHandler . Protected ( )
185+ . SetupSequence < Task < HttpResponseMessage > > (
186+ "SendAsync" ,
187+ ItExpr . Is < HttpRequestMessage > ( m =>
188+ m . Method == HttpMethod . Put &&
189+ m . RequestUri . AbsolutePath == "/v1/labels/se-1234/void" ) ,
190+ ItExpr . IsAny < CancellationToken > ( ) )
191+ . Returns ( Task . FromResult ( RateLimitResponseMessage ) )
192+ . Returns ( Task . FromResult (
193+ new HttpResponseMessage ( HttpStatusCode . OK )
194+ {
195+ Content = new StringContent ( VoidLabelResponse )
196+ }
197+ ) ) ;
198+
199+ // Should retry successfully, not throw a timeout exception
200+ await mockShipEngineFixture . ShipEngine . VoidLabelWithLabelId ( "se-1234" ) ;
201+
202+ mockShipEngineFixture . AssertRequest ( HttpMethod . Put , "/v1/labels/se-1234/void" , numberOfCalls : 2 ) ;
203+ }
204+
205+ [ Fact ]
206+ public async Task TimeoutMessageShowsCorrectMillisecondsForLargeTimeouts ( )
207+ {
208+ // RetryAfter header is 1 second; set timeout to 0.5s so it triggers the timeout path
209+ var config = new Config ( apiKey : "TEST_bTYAskEX6tD7vv6u/cZ/M4LaUSWBJ219+8S1jgFcnkk" , timeout : TimeSpan . FromMilliseconds ( 1500 ) , retries : 1 ) ;
210+ var mockShipEngineFixture = new MockShipEngineFixture ( config ) ;
211+
212+ var rateLimitWithHighRetryAfter = new HttpResponseMessage ( ( HttpStatusCode ) 429 ) ;
213+ rateLimitWithHighRetryAfter . Content = new StringContent ( rateLimitResponse ) ;
214+ rateLimitWithHighRetryAfter . Headers . Add ( "RetryAfter" , "2" ) ;
215+
216+ mockShipEngineFixture . MockHandler . Protected ( )
217+ . SetupSequence < Task < HttpResponseMessage > > (
218+ "SendAsync" ,
219+ ItExpr . Is < HttpRequestMessage > ( m =>
220+ m . Method == HttpMethod . Put &&
221+ m . RequestUri . AbsolutePath == "/v1/labels/se-1234/void" ) ,
222+ ItExpr . IsAny < CancellationToken > ( ) )
223+ . Returns ( Task . FromResult ( rateLimitWithHighRetryAfter ) ) ;
224+
225+ var ex = await Assert . ThrowsAsync < ShipEngineException > ( async ( ) => await mockShipEngineFixture . ShipEngine . VoidLabelWithLabelId ( "se-1234" ) ) ;
226+
227+ Assert . Equal ( "The request took longer than the 1500 milliseconds allowed" , ex . Message ) ;
228+ Assert . Equal ( ErrorCode . Timeout , ex . ErrorCode ) ;
229+ }
174230 }
175231}
0 commit comments