Skip to content

Commit b527df7

Browse files
committed
tests: add unit tests
1 parent 3fec66a commit b527df7

File tree

2 files changed

+284
-13
lines changed

2 files changed

+284
-13
lines changed

Sources/NetworkLayer/Classes/Core/Services/RequestProcessor/RequestProcessor.swift

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -189,14 +189,10 @@ actor RequestProcessor {
189189
send: @Sendable () async throws -> T,
190190
shouldRetry: @Sendable @escaping (Error) -> Bool
191191
) async throws -> T {
192-
do {
193-
return try await send()
194-
} catch {
195-
if let retryPolicyService {
196-
return try await retryPolicyService.retry(strategy: strategy, onFailure: shouldRetry, send)
197-
} else {
198-
throw error
199-
}
192+
if let retryPolicyService {
193+
try await retryPolicyService.retry(strategy: strategy, onFailure: shouldRetry, send)
194+
} else {
195+
try await send()
200196
}
201197
}
202198

Tests/NetworkLayerTests/Classes/Tests/UnitTests/RequestProcessorTests.swift

Lines changed: 280 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)