From 89691bcaf850f7e32b98d1846fbb22098ac833a3 Mon Sep 17 00:00:00 2001 From: Connor Ricks Date: Wed, 26 Jul 2023 08:16:26 -0400 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=9A=A8=20=20Adds=20assertions=20that?= =?UTF-8?q?=20request=20is=20actually=20fired=20on=20retries.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HTTPRequestTests.swift | 58 +++++++++++++++---- 1 file changed, 48 insertions(+), 10 deletions(-) diff --git a/Tests/HTTPNetworkingTests/HTTPRequestTests.swift b/Tests/HTTPNetworkingTests/HTTPRequestTests.swift index a8d6f2d..5e1deca 100644 --- a/Tests/HTTPNetworkingTests/HTTPRequestTests.swift +++ b/Tests/HTTPNetworkingTests/HTTPRequestTests.swift @@ -573,6 +573,9 @@ class HTTPRequestTests: XCTestCase { func test_client_withRetrier_runsRetrierOnRequestAndSuccessfullyRetries() async throws { let url = createMockUrl() let expectedError = URLError(.cannotConnectToHost) + let requestFiredExpectation = expectation(description: "Expected request to be fired twice.") + requestFiredExpectation.expectedFulfillmentCount = 2 + requestFiredExpectation.assertForOverFulfill = true let retryExpectation = expectation(description: "Expected retry to be called.") var shouldRequestFail = true @@ -585,6 +588,8 @@ class HTTPRequestTests: XCTestCase { } else { return .success((self.data, self.createResponse(for: url, with: 200))) } + }, onRecieveRequest: { request in + requestFiredExpectation.fulfill() }) ]), retriers: [ @@ -601,17 +606,21 @@ class HTTPRequestTests: XCTestCase { .request(for: .get, to: url, expecting: [String].self) .run() - await fulfillment(of: [retryExpectation]) + await fulfillment(of: [retryExpectation, requestFiredExpectation]) } func test_client_withRetrier_runsRetrierOnRequestAndConcedesToFailure() async throws { let url = createMockUrl() let expectedError = URLError(.cannotConnectToHost) + let requestFiredExpectation = expectation(description: "Expected request to be fired once.") + requestFiredExpectation.assertForOverFulfill = true let retryExpectation = expectation(description: "Expected retry to be called.") let client = HTTPClient( dispatcher: .mock(responses: [ - url: .failure(expectedError) + url: .failure(expectedError, onRecieveRequest: { _ in + requestFiredExpectation.fulfill() + }), ]), retriers: [ Retrier { request, session, error in @@ -631,12 +640,15 @@ class HTTPRequestTests: XCTestCase { XCTAssertEqual((error as? URLError)?.code, expectedError.code) } - await fulfillment(of: [retryExpectation]) + await fulfillment(of: [retryExpectation, requestFiredExpectation]) } func test_client_withMultipleRetriersSuccessfulAndConceding_runsFirstRetrierOnRequestAndNotSecond() async throws { let url = createMockUrl() let expectedError = URLError(.cannotConnectToHost) + let requestFiredExpectation = expectation(description: "Expected request to be fired twice.") + requestFiredExpectation.expectedFulfillmentCount = 2 + requestFiredExpectation.assertForOverFulfill = true let retryExpectation = expectation(description: "Expected retry to be called.") var shouldRequestFail = true @@ -648,6 +660,8 @@ class HTTPRequestTests: XCTestCase { } else { return .success((self.data, self.createResponse(for: url, with: 200))) } + }, onRecieveRequest: { _ in + requestFiredExpectation.fulfill() }) ]), retriers: [ @@ -668,12 +682,15 @@ class HTTPRequestTests: XCTestCase { .request(for: .get, to: url, expecting: [String].self) .run() - await fulfillment(of: [retryExpectation]) + await fulfillment(of: [retryExpectation, requestFiredExpectation]) } func test_client_withMultipleRetriersConcedingAndSuccessful_runsBothRetriersOnRequest() async throws { let url = createMockUrl() let expectedError = URLError(.cannotConnectToHost) + let requestFiredExpectation = expectation(description: "Expected request to be fired twice.") + requestFiredExpectation.expectedFulfillmentCount = 2 + requestFiredExpectation.assertForOverFulfill = true let retrierOneExpectation = expectation(description: "Expected retrier one to be called.") let retrierTwoExpectation = expectation(description: "Expected retrier two to be called.") var shouldRequestFail = true @@ -686,6 +703,8 @@ class HTTPRequestTests: XCTestCase { } else { return .success((self.data, self.createResponse(for: url, with: 200))) } + }, onRecieveRequest: { _ in + requestFiredExpectation.fulfill() }) ]), retriers: [ @@ -707,12 +726,15 @@ class HTTPRequestTests: XCTestCase { .request(for: .get, to: url, expecting: [String].self) .run() - await fulfillment(of: [retrierOneExpectation, retrierTwoExpectation]) + await fulfillment(of: [retrierOneExpectation, retrierTwoExpectation, requestFiredExpectation]) } func test_request_withRetrier_runsRetrierOnRequestAndSuccessfullyRetries() async throws { let url = createMockUrl() let expectedError = URLError(.cannotConnectToHost) + let requestFiredExpectation = expectation(description: "Expected request to be fired twice.") + requestFiredExpectation.expectedFulfillmentCount = 2 + requestFiredExpectation.assertForOverFulfill = true let retryExpectation = expectation(description: "Expected retry to be called.") var shouldRequestFail = true @@ -725,6 +747,8 @@ class HTTPRequestTests: XCTestCase { } else { return .success((self.data, self.createResponse(for: url, with: 200))) } + }, onRecieveRequest: { _ in + requestFiredExpectation.fulfill() }) ]) ) @@ -739,17 +763,21 @@ class HTTPRequestTests: XCTestCase { }) .run() - await fulfillment(of: [retryExpectation]) + await fulfillment(of: [retryExpectation, requestFiredExpectation]) } func test_request_withRetrier_runsRetrierOnRequestAndConcedesToFailure() async throws { let url = createMockUrl() let expectedError = URLError(.cannotConnectToHost) + let requestFiredExpectation = expectation(description: "Expected request to be fired once.") + requestFiredExpectation.assertForOverFulfill = true let retryExpectation = expectation(description: "Expected retry to be called.") let client = HTTPClient( dispatcher: .mock(responses: [ - url: .failure(expectedError) + url: .failure(expectedError, onRecieveRequest: { _ in + requestFiredExpectation.fulfill() + }), ]) ) @@ -767,12 +795,15 @@ class HTTPRequestTests: XCTestCase { XCTAssertEqual((error as? URLError)?.code, expectedError.code) } - await fulfillment(of: [retryExpectation]) + await fulfillment(of: [retryExpectation, requestFiredExpectation]) } func test_request_withMultipleRetriersSuccessfulAndConceding_runsFirstRetrierOnRequestAndNotSecond() async throws { let url = createMockUrl() let expectedError = URLError(.cannotConnectToHost) + let requestFiredExpectation = expectation(description: "Expected request to be fired twice.") + requestFiredExpectation.expectedFulfillmentCount = 2 + requestFiredExpectation.assertForOverFulfill = true let retryExpectation = expectation(description: "Expected retry to be called.") var shouldRequestFail = true @@ -784,6 +815,8 @@ class HTTPRequestTests: XCTestCase { } else { return .success((self.data, self.createResponse(for: url, with: 200))) } + }, onRecieveRequest: { _ in + requestFiredExpectation.fulfill() }) ]) ) @@ -802,12 +835,15 @@ class HTTPRequestTests: XCTestCase { }) .run() - await fulfillment(of: [retryExpectation]) + await fulfillment(of: [retryExpectation, requestFiredExpectation]) } func test_request_withMultipleRetriersConcedingAndSuccessful_runsBothRetriersOnRequest() async throws { let url = createMockUrl() let expectedError = URLError(.cannotConnectToHost) + let requestFiredExpectation = expectation(description: "Expected request to be fired twice.") + requestFiredExpectation.expectedFulfillmentCount = 2 + requestFiredExpectation.assertForOverFulfill = true let retrierOneExpectation = expectation(description: "Expected retrier one to be called.") let retrierTwoExpectation = expectation(description: "Expected retrier two to be called.") var shouldRequestFail = true @@ -820,6 +856,8 @@ class HTTPRequestTests: XCTestCase { } else { return .success((self.data, self.createResponse(for: url, with: 200))) } + }, onRecieveRequest: { _ in + requestFiredExpectation.fulfill() }) ]) ) @@ -839,7 +877,7 @@ class HTTPRequestTests: XCTestCase { }) .run() - await fulfillment(of: [retrierOneExpectation, retrierTwoExpectation]) + await fulfillment(of: [retrierOneExpectation, retrierTwoExpectation, requestFiredExpectation]) } func test_request_withMultipleClientAndRequestRetriers_runsAllRetriersOnRequest() async throws { From 09e4bcfab1d090a419874a17a3ca3d31f248f64b Mon Sep 17 00:00:00 2001 From: Connor Ricks Date: Wed, 26 Jul 2023 08:16:49 -0400 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=8F=8E=20=20Adds=20additional=20cance?= =?UTF-8?q?llation=20checks=20to=20prevent=20unnecessary=20work.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Sources/HTTPNetworking/HTTPRequest.swift | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/Sources/HTTPNetworking/HTTPRequest.swift b/Sources/HTTPNetworking/HTTPRequest.swift index fa2a950..527cb00 100644 --- a/Sources/HTTPNetworking/HTTPRequest.swift +++ b/Sources/HTTPNetworking/HTTPRequest.swift @@ -66,17 +66,26 @@ public class HTTPRequest { var request = self.request do { + + try Task.checkCancellation() + // Create the adapted request. request = try await ZipAdaptor(adaptors).adapt(request, for: dispatcher.session) + try Task.checkCancellation() + // Dispatch the request and wait for a response. let (data, response) = try await dispatcher.data(for: request) + try Task.checkCancellation() + // Validate the response. try await ZipValidator(validators) .validate(response, for: request, with: data) .get() + try Task.checkCancellation() + // Convert data to the expected type return try decoder.decode(T.self, from: data) } catch { From 7ec764e13749a3168df732fd50195e32365a089d Mon Sep 17 00:00:00 2001 From: Connor Ricks Date: Wed, 26 Jul 2023 08:28:56 -0400 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=9A=A8=20=20Adds=20tests=20to=20verif?= =?UTF-8?q?y=20delayed=20retry=20does=20retry.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../HTTPRequestTests.swift | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/Tests/HTTPNetworkingTests/HTTPRequestTests.swift b/Tests/HTTPNetworkingTests/HTTPRequestTests.swift index 5e1deca..8d80527 100644 --- a/Tests/HTTPNetworkingTests/HTTPRequestTests.swift +++ b/Tests/HTTPNetworkingTests/HTTPRequestTests.swift @@ -929,6 +929,82 @@ class HTTPRequestTests: XCTestCase { ]) } + func test_client_withDelayedRetry_doesRetryRequest() async throws { + let url = createMockUrl() + let expectedError = URLError(.cannotConnectToHost) + let requestFiredExpectation = expectation(description: "Expected request to be fired twice.") + requestFiredExpectation.expectedFulfillmentCount = 2 + requestFiredExpectation.assertForOverFulfill = true + let retryExpectation = expectation(description: "Expected retry to be called.") + var shouldRequestFail = true + + + let client = HTTPClient( + dispatcher: .mock(responses: [ + url: .init(result: { + if shouldRequestFail { + return .failure(expectedError) + } else { + return .success((self.data, self.createResponse(for: url, with: 200))) + } + }, onRecieveRequest: { request in + requestFiredExpectation.fulfill() + }) + ]), + retriers: [ + Retrier { request, session, error in + XCTAssertEqual((error as? URLError)?.code, expectedError.code) + retryExpectation.fulfill() + shouldRequestFail = false + return .retryAfterDelay(.nanoseconds(100)) + } + ] + ) + + _ = try await client + .request(for: .get, to: url, expecting: [String].self) + .run() + + await fulfillment(of: [retryExpectation, requestFiredExpectation]) + } + + func test_request_withDelayedRetry_doesRetryRequest() async throws { + let url = createMockUrl() + let expectedError = URLError(.cannotConnectToHost) + let requestFiredExpectation = expectation(description: "Expected request to be fired twice.") + requestFiredExpectation.expectedFulfillmentCount = 2 + requestFiredExpectation.assertForOverFulfill = true + let retryExpectation = expectation(description: "Expected retry to be called.") + var shouldRequestFail = true + + + let client = HTTPClient( + dispatcher: .mock(responses: [ + url: .init(result: { + if shouldRequestFail { + return .failure(expectedError) + } else { + return .success((self.data, self.createResponse(for: url, with: 200))) + } + }, onRecieveRequest: { request in + requestFiredExpectation.fulfill() + }) + ]) + ) + + _ = try await client + .request(for: .get, to: url, expecting: [String].self) + .retry { request, session, error in + XCTAssertEqual((error as? URLError)?.code, expectedError.code) + retryExpectation.fulfill() + shouldRequestFail = false + return .retryAfterDelay(.nanoseconds(100)) + } + .run() + + await fulfillment(of: [retryExpectation, requestFiredExpectation]) + } + // MARK: Task Cancellation Tests func test_request_whenCancelledBeforeAdaptorsFinish_doesNotProceed() async throws {