| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 |
- //
- // RetryPolicyTests.swift
- //
- // Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/)
- //
- // Permission is hereby granted, free of charge, to any person obtaining a copy
- // of this software and associated documentation files (the "Software"), to deal
- // in the Software without restriction, including without limitation the rights
- // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- // copies of the Software, and to permit persons to whom the Software is
- // furnished to do so, subject to the following conditions:
- //
- // The above copyright notice and this permission notice shall be included in
- // all copies or substantial portions of the Software.
- //
- // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- // THE SOFTWARE.
- //
- @testable import Alamofire
- import Foundation
- import XCTest
- class BaseRetryPolicyTestCase: BaseTestCase {
- // MARK: Helper Types
- final class StubRequest: DataRequest, @unchecked Sendable {
- let urlRequest: URLRequest
- override var request: URLRequest? { urlRequest }
- let mockedResponse: HTTPURLResponse?
- override var response: HTTPURLResponse? { mockedResponse }
- init(_ url: URL, method: HTTPMethod, response: HTTPURLResponse?, session: Session) {
- mockedResponse = response
- let request = Session.RequestConvertible(url: url,
- method: method,
- parameters: nil,
- encoding: URLEncoding.default,
- headers: nil,
- requestModifier: nil)
- urlRequest = try! request.asURLRequest()
- super.init(convertible: request,
- underlyingQueue: session.rootQueue,
- serializationQueue: session.serializationQueue,
- eventMonitor: session.eventMonitor,
- interceptor: nil,
- delegate: session)
- }
- }
- // MARK: Properties
- let idempotentMethods: Set<HTTPMethod> = [.get, .head, .put, .delete, .options, .trace]
- let nonIdempotentMethods: Set<HTTPMethod> = [.post, .patch, .connect]
- var methods: Set<HTTPMethod> { idempotentMethods.union(nonIdempotentMethods) }
- let session = Session(rootQueue: .main, startRequestsImmediately: false)
- let url = Endpoint().url
- let connectionLost = URLError(.networkConnectionLost)
- let resourceUnavailable = URLError(.resourceUnavailable)
- let unknown = URLError(.unknown)
- lazy var connectionLostError = AFError.sessionTaskFailed(error: connectionLost)
- lazy var resourceUnavailableError = AFError.sessionTaskFailed(error: resourceUnavailable)
- lazy var unknownError = AFError.sessionTaskFailed(error: unknown)
- let retryableStatusCodes: Set<Int> = [408, 500, 502, 503, 504]
- let statusCodes = Set(100...599)
- let retryableErrorCodes: Set<URLError.Code> = [.backgroundSessionInUseByAnotherProcess,
- .backgroundSessionWasDisconnected,
- .badServerResponse,
- .callIsActive,
- .cannotConnectToHost,
- .cannotFindHost,
- .cannotLoadFromNetwork,
- .dataNotAllowed,
- .dnsLookupFailed,
- .downloadDecodingFailedMidStream,
- .downloadDecodingFailedToComplete,
- .internationalRoamingOff,
- .networkConnectionLost,
- .notConnectedToInternet,
- .secureConnectionFailed,
- .serverCertificateHasBadDate,
- .serverCertificateNotYetValid,
- .timedOut]
- let nonRetryableErrorCodes: Set<URLError.Code> = [.appTransportSecurityRequiresSecureConnection,
- .backgroundSessionRequiresSharedContainer,
- .badURL,
- .cancelled,
- .cannotCloseFile,
- .cannotCreateFile,
- .cannotDecodeContentData,
- .cannotDecodeRawData,
- .cannotMoveFile,
- .cannotOpenFile,
- .cannotParseResponse,
- .cannotRemoveFile,
- .cannotWriteToFile,
- .clientCertificateRejected,
- .clientCertificateRequired,
- .dataLengthExceedsMaximum,
- .fileDoesNotExist,
- .fileIsDirectory,
- .httpTooManyRedirects,
- .noPermissionsToReadFile,
- .redirectToNonExistentLocation,
- .requestBodyStreamExhausted,
- .resourceUnavailable,
- .serverCertificateHasUnknownRoot,
- .serverCertificateUntrusted,
- .unknown,
- .unsupportedURL,
- .userAuthenticationRequired,
- .userCancelledAuthentication,
- .zeroByteResource]
- var errorCodes: Set<URLError.Code> {
- retryableErrorCodes.union(nonRetryableErrorCodes)
- }
- // MARK: Test Helpers
- func request(method: HTTPMethod = .get, statusCode: Int? = nil) -> Request {
- var response: HTTPURLResponse?
- if let statusCode {
- response = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil)
- }
- return StubRequest(url, method: method, response: response, session: session)
- }
- func urlError(with code: URLError.Code) -> URLError {
- NSError(domain: URLError.errorDomain, code: code.rawValue, userInfo: nil) as! URLError
- }
- }
- // MARK: -
- final class RetryPolicyTestCase: BaseRetryPolicyTestCase {
- // MARK: Tests - Retry
- @MainActor
- func testThatRetryIsNotPerformedOnCancelledRequests() {
- // Given
- let retrier = InspectorInterceptor(Retrier { _, _, _, completion in
- completion(.retry)
- })
- let session = Session(interceptor: retrier)
- let didFinish = expectation(description: "didFinish request")
- // When
- let request = session.request(.default).responseDecodable(of: TestResponse.self) { _ in
- didFinish.fulfill()
- }
- request.cancel()
- waitForExpectations(timeout: timeout)
- // Then
- XCTAssertTrue(request.isCancelled)
- XCTAssertEqual(retrier.retryCalledCount, 0)
- }
- @MainActor
- func testThatRetryPolicyRetriesRequestsBelowRetryLimit() {
- // Given
- let retryPolicy = RetryPolicy()
- let request = request(method: .get)
- var results: [Int: RetryResult] = [:]
- // When
- for index in 0...2 {
- let expectation = expectation(description: "retry policy should complete")
- retryPolicy.retry(request, for: session, dueTo: connectionLostError) { result in
- results[index] = result
- expectation.fulfill()
- }
- waitForExpectations(timeout: timeout)
- request.prepareForRetry()
- }
- // Then
- XCTAssertEqual(results.count, 3)
- if results.count == 3 {
- XCTAssertEqual(results[0]?.retryRequired, true)
- XCTAssertEqual(results[0]?.delay, 0.5)
- XCTAssertNil(results[0]?.error)
- XCTAssertEqual(results[1]?.retryRequired, true)
- XCTAssertEqual(results[1]?.delay, 1.0)
- XCTAssertNil(results[1]?.error)
- XCTAssertEqual(results[2]?.retryRequired, false)
- XCTAssertNil(results[2]?.delay)
- XCTAssertNil(results[2]?.error)
- }
- }
- @MainActor
- func testThatRetryPolicyRetriesIdempotentRequests() {
- // Given
- let retryPolicy = RetryPolicy()
- var results: [HTTPMethod: RetryResult] = [:]
- // When
- for method in methods {
- let request = request(method: method)
- let expectation = expectation(description: "retry policy should complete")
- retryPolicy.retry(request, for: session, dueTo: connectionLostError) { result in
- results[method] = result
- expectation.fulfill()
- }
- waitForExpectations(timeout: timeout)
- }
- // Then
- XCTAssertEqual(results.count, methods.count)
- for (method, result) in results {
- XCTAssertEqual(result.retryRequired, idempotentMethods.contains(method))
- XCTAssertEqual(result.delay, result.retryRequired ? 0.5 : nil)
- XCTAssertNil(result.error)
- }
- }
- @MainActor
- func testThatRetryPolicyRetriesRequestsWithRetryableStatusCodes() {
- // Given
- let retryPolicy = RetryPolicy()
- var results: [Int: RetryResult] = [:]
- // When
- for statusCode in statusCodes {
- let request = request(method: .get, statusCode: statusCode)
- let expectation = expectation(description: "retry policy should complete")
- retryPolicy.retry(request, for: session, dueTo: unknownError) { result in
- results[statusCode] = result
- expectation.fulfill()
- }
- waitForExpectations(timeout: timeout)
- }
- // Then
- XCTAssertEqual(results.count, statusCodes.count)
- for (statusCode, result) in results {
- XCTAssertEqual(result.retryRequired, retryableStatusCodes.contains(statusCode))
- XCTAssertEqual(result.delay, result.retryRequired ? 0.5 : nil)
- XCTAssertNil(result.error)
- }
- }
- @MainActor
- func testThatRetryPolicyRetriesRequestsWithRetryableErrors() {
- // Given
- let retryPolicy = RetryPolicy()
- var results: [URLError.Code: RetryResult] = [:]
- // When
- for code in errorCodes {
- let request = request(method: .get)
- let error = URLError(code)
- let expectation = expectation(description: "retry policy should complete")
- retryPolicy.retry(request, for: session, dueTo: error) { result in
- results[code] = result
- expectation.fulfill()
- }
- waitForExpectations(timeout: timeout)
- }
- // Then
- XCTAssertEqual(results.count, errorCodes.count)
- for (urlErrorCode, result) in results {
- XCTAssertEqual(result.retryRequired, retryableErrorCodes.contains(urlErrorCode))
- XCTAssertEqual(result.delay, result.retryRequired ? 0.5 : nil)
- XCTAssertNil(result.error)
- }
- }
- @MainActor
- func testThatRetryPolicyRetriesRequestsWithRetryableAFErrors() {
- // Given
- let retryPolicy = RetryPolicy()
- var results: [URLError.Code: RetryResult] = [:]
- // When
- for code in errorCodes {
- let request = request(method: .get)
- let error = AFError.sessionTaskFailed(error: URLError(code))
- let expectation = expectation(description: "retry policy should complete")
- retryPolicy.retry(request, for: session, dueTo: error) { result in
- results[code] = result
- expectation.fulfill()
- }
- waitForExpectations(timeout: timeout)
- }
- // Then
- XCTAssertEqual(results.count, errorCodes.count)
- for (urlErrorCode, result) in results {
- XCTAssertEqual(result.retryRequired, retryableErrorCodes.contains(urlErrorCode))
- XCTAssertEqual(result.delay, result.retryRequired ? 0.5 : nil)
- XCTAssertNil(result.error)
- }
- }
- @MainActor
- func testThatRetryPolicyDoesNotRetryErrorsThatAreNotRetryable() {
- // Given
- let retryPolicy = RetryPolicy()
- let request = request(method: .get)
- let errors: [any Error] = [resourceUnavailable,
- unknown,
- resourceUnavailableError,
- unknownError]
- var results: [RetryResult] = []
- // When
- for error in errors {
- let expectation = expectation(description: "retry policy should complete")
- retryPolicy.retry(request, for: session, dueTo: error) { result in
- results.append(result)
- expectation.fulfill()
- }
- waitForExpectations(timeout: timeout)
- }
- // Then
- XCTAssertEqual(results.count, errors.count)
- for result in results {
- XCTAssertFalse(result.retryRequired)
- XCTAssertNil(result.delay)
- XCTAssertNil(result.error)
- }
- }
- // MARK: Tests - Exponential Backoff
- @MainActor
- func testThatRetryPolicyTimeDelayBacksOffExponentially() {
- // Given
- let retryPolicy = RetryPolicy(retryLimit: 4)
- let request = request(method: .get)
- var results: [Int: RetryResult] = [:]
- // When
- for index in 0...4 {
- let expectation = expectation(description: "retry policy should complete")
- retryPolicy.retry(request, for: session, dueTo: connectionLostError) { result in
- results[index] = result
- expectation.fulfill()
- }
- waitForExpectations(timeout: timeout)
- request.prepareForRetry()
- }
- // Then
- XCTAssertEqual(results.count, 5)
- if results.count == 5 {
- XCTAssertEqual(results[0]?.retryRequired, true)
- XCTAssertEqual(results[0]?.delay, 0.5)
- XCTAssertNil(results[0]?.error)
- XCTAssertEqual(results[1]?.retryRequired, true)
- XCTAssertEqual(results[1]?.delay, 1.0)
- XCTAssertNil(results[1]?.error)
- XCTAssertEqual(results[2]?.retryRequired, true)
- XCTAssertEqual(results[2]?.delay, 2.0)
- XCTAssertNil(results[2]?.error)
- XCTAssertEqual(results[3]?.retryRequired, true)
- XCTAssertEqual(results[3]?.delay, 4.0)
- XCTAssertNil(results[3]?.error)
- XCTAssertEqual(results[4]?.retryRequired, false)
- XCTAssertNil(results[4]?.delay)
- XCTAssertNil(results[4]?.error)
- }
- }
- }
- // MARK: -
- final class ConnectionLostRetryPolicyTestCase: BaseRetryPolicyTestCase {
- func testThatConnectionLostRetryPolicyCanBeInitializedWithDefaultValues() {
- // Given, When
- let retryPolicy = ConnectionLostRetryPolicy()
- // Then
- XCTAssertEqual(retryPolicy.retryLimit, 2)
- XCTAssertEqual(retryPolicy.exponentialBackoffBase, 2)
- XCTAssertEqual(retryPolicy.exponentialBackoffScale, 0.5)
- XCTAssertEqual(retryPolicy.retryableHTTPMethods, idempotentMethods)
- XCTAssertEqual(retryPolicy.retryableHTTPStatusCodes, [])
- XCTAssertEqual(retryPolicy.retryableURLErrorCodes, [.networkConnectionLost])
- }
- func testThatConnectionLostRetryPolicyCanBeInitializedWithCustomValues() {
- // Given, When
- let retryPolicy = ConnectionLostRetryPolicy(retryLimit: 3,
- exponentialBackoffBase: 4,
- exponentialBackoffScale: 0.25,
- retryableHTTPMethods: [.delete, .get])
- // Then
- XCTAssertEqual(retryPolicy.retryLimit, 3)
- XCTAssertEqual(retryPolicy.exponentialBackoffBase, 4)
- XCTAssertEqual(retryPolicy.exponentialBackoffScale, 0.25)
- XCTAssertEqual(retryPolicy.retryableHTTPMethods, [.delete, .get])
- XCTAssertEqual(retryPolicy.retryableHTTPStatusCodes, [])
- XCTAssertEqual(retryPolicy.retryableURLErrorCodes, [.networkConnectionLost])
- }
- }
|