Skip to content

Commit 59e0484

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

File tree

4 files changed

+292
-15
lines changed

4 files changed

+292
-15
lines changed

Package@swift-5.10.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ let package = Package(
1818
],
1919
dependencies: [
2020
.package(url: "https://github.com/space-code/atomic", exact: "1.1.0"),
21-
.package(url: "https://github.com/space-code/typhoon", exact: "1.2.1"),
21+
.package(url: "https://github.com/space-code/typhoon", exact: "1.4.0"),
2222
.package(url: "https://github.com/WeTransfer/Mocker", exact: "3.0.1"),
2323
],
2424
targets: [

Package@swift-6.0.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ let package = Package(
1818
],
1919
dependencies: [
2020
.package(url: "https://github.com/space-code/atomic", exact: "1.1.0"),
21-
.package(url: "https://github.com/space-code/typhoon", exact: "1.2.1"),
21+
.package(url: "https://github.com/space-code/typhoon", exact: "1.4.0"),
2222
.package(url: "https://github.com/WeTransfer/Mocker", exact: "3.0.1"),
2323
],
2424
targets: [

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: 286 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import NetworkLayerInterfaces
88
import Typhoon
99
import XCTest
1010

11+
// MARK: - RequestProcessorTests
12+
1113
final 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

Comments
 (0)