@@ -59,9 +59,9 @@ final class RequestProcessorTests: XCTestCase {
5959 super. tearDown ( )
6060 }
6161
62- // MARK: Tests
62+ // MARK: Authentication Tests
6363
64- func test_thatRequestProcessorSignsRequest_whenRequestRequiresAuthentication ( ) async {
64+ func test_send_appliesAuthenticationInterceptor_whenRequestRequiresAuthentication ( ) async {
6565 // given
6666 requestBuilderMock. stubbedBuildResult = URLRequest . fake ( )
6767 dataRequestHandler. stubbedStartDataTask = . init( data: Data ( ) , response: . init( ) , task: . fake( ) )
@@ -79,7 +79,7 @@ final class RequestProcessorTests: XCTestCase {
7979 XCTAssertFalse ( interceptorMock. invokedRefresh)
8080 }
8181
82- func test_thatRequestProcessorDoesNotSignRequest_whenRequestDoesNotRequireAuthentication ( ) async {
82+ func test_send_skipsAuthenticationInterceptor_whenRequestDoesNotRequireAuthentication ( ) async {
8383 // given
8484 requestBuilderMock. stubbedBuildResult = URLRequest . fake ( )
8585 dataRequestHandler. stubbedStartDataTask = . init( data: Data ( ) , response: . init( ) , task: . fake( ) )
@@ -97,7 +97,7 @@ final class RequestProcessorTests: XCTestCase {
9797 XCTAssertFalse ( interceptorMock. invokedRefresh)
9898 }
9999
100- func test_thatRequestProcessorRefreshesCredential_whenCredentialIsNotValid ( ) async {
100+ func test_send_refreshesCredential_whenAuthenticationIsRequiredAndCredentialIsInvalid ( ) async {
101101 // given
102102 requestBuilderMock. stubbedBuildResult = URLRequest . fake ( )
103103 dataRequestHandler. stubbedStartDataTask = . init( data: Data ( ) , response: HTTPURLResponse ( ) , task: . fake( ) )
@@ -116,7 +116,7 @@ final class RequestProcessorTests: XCTestCase {
116116 XCTAssertTrue ( interceptorMock. invokedRefresh)
117117 }
118118
119- func test_thatRequestProcessorDoesNotRefreshesCredential_whenRequestDoesNotRequireAuthentication ( ) async {
119+ func test_send_skipsCredentialRefresh_whenRequestDoesNotRequireAuthentication ( ) async {
120120 // given
121121 requestBuilderMock. stubbedBuildResult = URLRequest . fake ( )
122122 dataRequestHandler. startDataTaskThrowError = URLError ( . unknown)
@@ -133,4 +133,279 @@ final class RequestProcessorTests: XCTestCase {
133133 XCTAssertFalse ( interceptorMock. invokedAdapt)
134134 XCTAssertFalse ( interceptorMock. invokedRefresh)
135135 }
136+
137+ // MARK: Retry Policy Tests
138+
139+ func test_send_retriesRequest_whenRequestFailsAndRetryPolicyIsConfigured( ) async {
140+ // given
141+ requestBuilderMock. stubbedBuildResult = URLRequest . fake ( )
142+ dataRequestHandler. startDataTaskThrowError = URLError ( . networkConnectionLost)
143+
144+ let request = RequestMock ( )
145+ request. stubbedRequiresAuthentication = false
146+
147+ // when
148+ do {
149+ _ = try await sut. send ( request) as Response < Int >
150+ } catch { }
151+
152+ // then
153+ XCTAssertGreaterThan (
154+ dataRequestHandler. invokedStartDataTaskCount,
155+ 1 ,
156+ " Request should have been retried multiple times "
157+ )
158+ }
159+
160+ func test_send_doesNotRetry_whenRetryPolicyIsNotConfigured( ) async {
161+ // given
162+ sut = RequestProcessor (
163+ configuration: Configuration (
164+ sessionConfiguration: . default,
165+ sessionDelegate: nil ,
166+ sessionDelegateQueue: nil ,
167+ jsonDecoder: JSONDecoder ( )
168+ ) ,
169+ requestBuilder: requestBuilderMock,
170+ dataRequestHandler: dataRequestHandler,
171+ retryPolicyService: nil ,
172+ delegate: SafeRequestProcessorDelegate ( delegate: delegateMock) ,
173+ interceptor: interceptorMock,
174+ retryEvaluator: { _ in true }
175+ )
176+
177+ requestBuilderMock. stubbedBuildResult = URLRequest . fake ( )
178+ dataRequestHandler. startDataTaskThrowError = URLError ( . networkConnectionLost)
179+
180+ let request = RequestMock ( )
181+ request. stubbedRequiresAuthentication = false
182+
183+ // when
184+ do {
185+ _ = try await sut. send ( request) as Response < Int >
186+ } catch { }
187+
188+ // then
189+ XCTAssertEqual (
190+ dataRequestHandler. invokedStartDataTaskCount,
191+ 1 ,
192+ " Request should not have been retried without retry policy "
193+ )
194+ }
195+
196+ func test_send_stopsRetrying_whenGlobalRetryEvaluatorReturnsFalse( ) async {
197+ // given
198+ sut = RequestProcessor (
199+ configuration: Configuration (
200+ sessionConfiguration: . default,
201+ sessionDelegate: nil ,
202+ sessionDelegateQueue: nil ,
203+ jsonDecoder: JSONDecoder ( )
204+ ) ,
205+ requestBuilder: requestBuilderMock,
206+ dataRequestHandler: dataRequestHandler,
207+ retryPolicyService: retryPolicyMock,
208+ delegate: SafeRequestProcessorDelegate ( delegate: delegateMock) ,
209+ interceptor: interceptorMock,
210+ retryEvaluator: { _ in false }
211+ )
212+
213+ requestBuilderMock. stubbedBuildResult = URLRequest . fake ( )
214+ dataRequestHandler. startDataTaskThrowError = URLError ( . networkConnectionLost)
215+
216+ let request = RequestMock ( )
217+ request. stubbedRequiresAuthentication = false
218+
219+ // when
220+ do {
221+ _ = try await sut. send ( request) as Response < Int >
222+ } catch { }
223+
224+ // then
225+ XCTAssertEqual (
226+ dataRequestHandler. invokedStartDataTaskCount,
227+ 1 ,
228+ " Request should not be retried when global evaluator returns false "
229+ )
230+ }
231+
232+ func test_send_stopsRetrying_whenLocalRetryEvaluatorReturnsFalse( ) async {
233+ // given
234+ requestBuilderMock. stubbedBuildResult = URLRequest . fake ( )
235+ dataRequestHandler. startDataTaskThrowError = URLError ( . networkConnectionLost)
236+
237+ let request = RequestMock ( )
238+ request. stubbedRequiresAuthentication = false
239+
240+ // when
241+ do {
242+ _ = try await sut. send (
243+ request,
244+ shouldRetry: { _ in false }
245+ ) as Response < Int >
246+ } catch { }
247+
248+ // then
249+ XCTAssertEqual (
250+ dataRequestHandler. invokedStartDataTaskCount,
251+ 1 ,
252+ " Request should not be retried when local evaluator returns false "
253+ )
254+ }
255+
256+ func test_send_retriesRequest_whenBothEvaluatorsReturnTrue( ) async {
257+ // given
258+ requestBuilderMock. stubbedBuildResult = URLRequest . fake ( )
259+ dataRequestHandler. startDataTaskThrowError = URLError ( . networkConnectionLost)
260+
261+ let request = RequestMock ( )
262+ request. stubbedRequiresAuthentication = false
263+
264+ // when
265+ do {
266+ _ = try await sut. send (
267+ request,
268+ shouldRetry: { _ in true }
269+ ) as Response < Int >
270+ } catch { }
271+
272+ // then
273+ XCTAssertGreaterThan (
274+ dataRequestHandler. invokedStartDataTaskCount,
275+ 1 ,
276+ " Request should be retried when both evaluators return true "
277+ )
278+ }
279+
280+ func test_send_retriesWithCustomStrategy_whenStrategyIsProvided( ) async {
281+ // given
282+ let customRetryCount = 3
283+ let customStrategy = RetryPolicyStrategy . constant ( retry: customRetryCount, duration: . seconds( . zero) )
284+
285+ requestBuilderMock. stubbedBuildResult = URLRequest . fake ( )
286+ dataRequestHandler. startDataTaskThrowError = URLError ( . networkConnectionLost)
287+
288+ let request = RequestMock ( )
289+ request. stubbedRequiresAuthentication = false
290+
291+ // when
292+ do {
293+ _ = try await sut. send (
294+ request,
295+ strategy: customStrategy
296+ ) as Response < Int >
297+ } catch { }
298+
299+ // then
300+ XCTAssertGreaterThan (
301+ dataRequestHandler. invokedStartDataTaskCount,
302+ 1 ,
303+ " Request should be retried with custom strategy "
304+ )
305+ XCTAssertLessThanOrEqual (
306+ dataRequestHandler. invokedStartDataTaskCount,
307+ customRetryCount + 1 ,
308+ " Should not exceed custom retry count plus initial attempt "
309+ )
310+ }
311+
312+ func test_send_throwsError_whenAllRetriesExhausted( ) async {
313+ // given
314+ requestBuilderMock. stubbedBuildResult = URLRequest . fake ( )
315+ dataRequestHandler. startDataTaskThrowError = URLError ( . networkConnectionLost)
316+
317+ let request = RequestMock ( )
318+ request. stubbedRequiresAuthentication = false
319+
320+ // when
321+ var thrownError : Error ?
322+ do {
323+ _ = try await sut. send ( request) as Response < Int >
324+ } catch {
325+ thrownError = error
326+ }
327+
328+ // then
329+ XCTAssertNotNil ( thrownError, " Should throw error when all retries are exhausted " )
330+ XCTAssertGreaterThan (
331+ dataRequestHandler. invokedStartDataTaskCount,
332+ 1 ,
333+ " Should have attempted retries before throwing error "
334+ )
335+ }
336+
337+ func test_send_invokesRequestBuilderOnce_whenRequestSucceedsOnFirstAttempt( ) async {
338+ // given
339+ requestBuilderMock. stubbedBuildResult = URLRequest . fake ( )
340+ dataRequestHandler. stubbedStartDataTask = . init( data: Data ( ) , response: HTTPURLResponse ( ) , task: . fake( ) )
341+
342+ let request = RequestMock ( )
343+ request. stubbedRequiresAuthentication = false
344+
345+ // when
346+ do {
347+ _ = try await sut. send ( request) as Response < Int >
348+ } catch { }
349+
350+ // then
351+ XCTAssertEqual (
352+ dataRequestHandler. invokedStartDataTaskCount,
353+ 1 ,
354+ " Should only attempt request once when successful "
355+ )
356+ }
357+
358+ func test_send_retainsRequestParameters_acrossRetryAttempts( ) async {
359+ // given
360+ requestBuilderMock. stubbedBuildResult = URLRequest . fake ( )
361+ dataRequestHandler. startDataTaskThrowError = URLError ( . networkConnectionLost)
362+
363+ let request = RequestMock ( )
364+ request. stubbedRequiresAuthentication = false
365+
366+ // when
367+ do {
368+ _ = try await sut. send ( request) as Response < Int >
369+ } catch { }
370+
371+ // then
372+ XCTAssertGreaterThan (
373+ dataRequestHandler. invokedStartDataTaskParametersList. count,
374+ 1 ,
375+ " Should have multiple retry attempts recorded "
376+ )
377+
378+ let firstDelegate = dataRequestHandler. invokedStartDataTaskParametersList. first? . delegate
379+ let lastDelegate = dataRequestHandler. invokedStartDataTaskParametersList. last? . delegate
380+ XCTAssertTrue (
381+ ( firstDelegate == nil && lastDelegate == nil ) || ( firstDelegate != nil && lastDelegate != nil ) ,
382+ " Delegate should be consistent across retries "
383+ )
384+ }
385+
386+ final class Box < T> : @unchecked Sendable {
387+ var value : T ?
388+ }
389+
390+ func test_send_evaluatesErrorType_beforeRetrying( ) async {
391+ // given
392+ let errorBox = Box < Error > ( )
393+ requestBuilderMock. stubbedBuildResult = URLRequest . fake ( )
394+ let specificError = URLError ( . networkConnectionLost)
395+ dataRequestHandler. startDataTaskThrowError = specificError
396+
397+ // when
398+ do {
399+ _ = try await sut. send (
400+ RequestMock ( ) ,
401+ shouldRetry: { error in
402+ errorBox. value = error
403+ return false
404+ }
405+ ) as Response < Int >
406+ } catch { }
407+
408+ // then
409+ XCTAssertEqual ( ( errorBox. value as? URLError ) ? . code, specificError. code)
410+ }
136411}
0 commit comments