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