Browse Source

Added unit tests for retry policy and connection lost retry policy

Christian Noon 7 years ago
parent
commit
aa38fb730b
2 changed files with 411 additions and 0 deletions
  1. 8 0
      Alamofire.xcodeproj/project.pbxproj
  2. 403 0
      Tests/RetryPolicyTests.swift

+ 8 - 0
Alamofire.xcodeproj/project.pbxproj

@@ -239,6 +239,9 @@
 		4CB9282A1C66BFBC00CE5F08 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB928281C66BFBC00CE5F08 /* Notifications.swift */; };
 		4CB9282B1C66BFBC00CE5F08 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB928281C66BFBC00CE5F08 /* Notifications.swift */; };
 		4CB9282C1C66BFBC00CE5F08 /* Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CB928281C66BFBC00CE5F08 /* Notifications.swift */; };
+		4CBD2180220B48AE008F1C59 /* RetryPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBD217F220B48AE008F1C59 /* RetryPolicyTests.swift */; };
+		4CBD2181220B48AE008F1C59 /* RetryPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBD217F220B48AE008F1C59 /* RetryPolicyTests.swift */; };
+		4CBD2182220B48AE008F1C59 /* RetryPolicyTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4CBD217F220B48AE008F1C59 /* RetryPolicyTests.swift */; };
 		4CCB206C1D4549E000C64D5B /* expired.badssl.com-leaf.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4CCB20681D4549E000C64D5B /* expired.badssl.com-leaf.cer */; };
 		4CCB206D1D4549E000C64D5B /* expired.badssl.com-leaf.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4CCB20681D4549E000C64D5B /* expired.badssl.com-leaf.cer */; };
 		4CCB206E1D4549E000C64D5B /* expired.badssl.com-leaf.cer in Resources */ = {isa = PBXBuildFile; fileRef = 4CCB20681D4549E000C64D5B /* expired.badssl.com-leaf.cer */; };
@@ -418,6 +421,7 @@
 		4C9E88391F5FB3B0000BEC61 /* Alamofire 4.0 Migration Guide.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; name = "Alamofire 4.0 Migration Guide.md"; path = "Documentation/Alamofire 4.0 Migration Guide.md"; sourceTree = "<group>"; };
 		4CA028C41B7466C500C84163 /* ResultTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ResultTests.swift; sourceTree = "<group>"; };
 		4CB928281C66BFBC00CE5F08 /* Notifications.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Notifications.swift; sourceTree = "<group>"; };
+		4CBD217F220B48AE008F1C59 /* RetryPolicyTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RetryPolicyTests.swift; sourceTree = "<group>"; };
 		4CCB20681D4549E000C64D5B /* expired.badssl.com-leaf.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = "expired.badssl.com-leaf.cer"; sourceTree = "<group>"; };
 		4CCB20691D4549E000C64D5B /* expired.badssl.com-root-ca.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = "expired.badssl.com-root-ca.cer"; sourceTree = "<group>"; };
 		4CCB206A1D4549E000C64D5B /* expired.badssl.com-intermediate-ca-1.cer */ = {isa = PBXFileReference; lastKnownFileType = file; path = "expired.badssl.com-intermediate-ca-1.cer"; sourceTree = "<group>"; };
@@ -556,6 +560,7 @@
 				4C3D00571C66A8B900D1F709 /* NetworkReachabilityManagerTests.swift */,
 				4CFD6B0F2201145500FFB5E3 /* RedirectHandlerTests.swift */,
 				4C0B58381B747A4400C0B99C /* ResponseSerializationTests.swift */,
+				4CBD217F220B48AE008F1C59 /* RetryPolicyTests.swift */,
 				4C33A1421B52089C00873DFF /* ServerTrustEvaluatorTests.swift */,
 				F86AEFE51AE6A282007D9C76 /* TLSEvaluationTests.swift */,
 				4CCFA7991B2BE71600B6F460 /* URLProtocolTests.swift */,
@@ -1356,6 +1361,7 @@
 				3111CE8620A76370008315E2 /* SessionTests.swift in Sources */,
 				31C2B0F220B271380089BA7C /* TLSEvaluationTests.swift in Sources */,
 				3111CE9D20A7EC58008315E2 /* URLProtocolTests.swift in Sources */,
+				4CBD2182220B48AE008F1C59 /* RetryPolicyTests.swift in Sources */,
 				317A6A7820B2208000A9FEC5 /* DownloadTests.swift in Sources */,
 				31F9683E20BB70290009606F /* NSLoggingEventMonitor.swift in Sources */,
 				3113D46D21878227001CCD21 /* HTTPHeadersTests.swift in Sources */,
@@ -1500,6 +1506,7 @@
 				3111CE8420A7636E008315E2 /* SessionTests.swift in Sources */,
 				31C2B0F020B271370089BA7C /* TLSEvaluationTests.swift in Sources */,
 				3111CE9B20A7EC57008315E2 /* URLProtocolTests.swift in Sources */,
+				4CBD2180220B48AE008F1C59 /* RetryPolicyTests.swift in Sources */,
 				317A6A7620B2207F00A9FEC5 /* DownloadTests.swift in Sources */,
 				31F9683C20BB70290009606F /* NSLoggingEventMonitor.swift in Sources */,
 				3113D46B21878227001CCD21 /* HTTPHeadersTests.swift in Sources */,
@@ -1533,6 +1540,7 @@
 				3111CE8520A7636F008315E2 /* SessionTests.swift in Sources */,
 				31C2B0F120B271370089BA7C /* TLSEvaluationTests.swift in Sources */,
 				3111CE9C20A7EC58008315E2 /* URLProtocolTests.swift in Sources */,
+				4CBD2181220B48AE008F1C59 /* RetryPolicyTests.swift in Sources */,
 				317A6A7720B2208000A9FEC5 /* DownloadTests.swift in Sources */,
 				31F9683D20BB70290009606F /* NSLoggingEventMonitor.swift in Sources */,
 				3113D46C21878227001CCD21 /* HTTPHeadersTests.swift in Sources */,

+ 403 - 0
Tests/RetryPolicyTests.swift

@@ -0,0 +1,403 @@
+//
+//  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
+
+    class StubRequest: DataRequest {
+        let urlRequest: URLRequest
+        override var request: URLRequest? { return urlRequest }
+
+        let mockedResponse: HTTPURLResponse?
+        override var response: HTTPURLResponse? { return 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
+            )
+
+            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> { return idempotentMethods.union(nonIdempotentMethods) }
+
+    let session = Session(startRequestsImmediately: false)
+
+    let url = URL(string: "https://api.alamofire.org")!
+    let connectionLostError = NSError(domain: URLError.errorDomain, code: URLError.networkConnectionLost.rawValue, userInfo: nil)
+    let resourceUnavailableError = NSError(domain: URLError.errorDomain, code: URLError.resourceUnavailable.rawValue, userInfo: nil)
+    let unknownError = NSError(domain: URLError.errorDomain, code: URLError.unknown.rawValue, userInfo: nil)
+
+    let retryableStatusCodes: Set<Int> = [408, 500, 502, 503, 504]
+
+    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> {
+        return retryableErrorCodes.union(nonRetryableErrorCodes)
+    }
+}
+
+// MARK: -
+
+class RetryPolicyTestCase: BaseRetryPolicyTestCase {
+
+    // MARK: Tests - Retry
+
+    func testThatRetryPolicyRetriesRequestsBelowRetryLimit() {
+        // Given
+        let retryPolicy = RetryPolicy()
+        let request = self.request(method: .get)
+
+        var results: [Int: RetryResult] = [:]
+
+        // When
+        for index in 0...2 {
+            let expectation = self.expectation(description: "retry policy should complete")
+
+            retryPolicy.retry(request, for: session, dueTo: connectionLostError) { result in
+                results[index] = result
+                expectation.fulfill()
+            }
+
+            waitForExpectations(timeout: timeout, handler: nil)
+
+            request.requestIsRetrying()
+        }
+
+        // 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)
+        }
+    }
+
+    func testThatRetryPolicyRetriesIdempotentRequests() {
+        // Given
+        let retryPolicy = RetryPolicy()
+        var results: [HTTPMethod: RetryResult] = [:]
+
+        // When
+        for method in methods {
+            let request = self.request(method: method)
+            let expectation = self.expectation(description: "retry policy should complete")
+
+            retryPolicy.retry(request, for: session, dueTo: connectionLostError) { result in
+                results[method] = result
+                expectation.fulfill()
+            }
+
+            waitForExpectations(timeout: timeout, handler: nil)
+        }
+
+        // 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)
+        }
+    }
+
+    func testThatRetryPolicyRetriesRequestsWithRetryableStatusCodes() {
+        // Given
+        let retryPolicy = RetryPolicy()
+        let statusCodes = Set(100...599)
+        var results: [Int: RetryResult] = [:]
+
+        // When
+        for statusCode in statusCodes {
+            let request = self.request(method: .get, statusCode: statusCode)
+            let expectation = self.expectation(description: "retry policy should complete")
+
+            retryPolicy.retry(request, for: session, dueTo: unknownError) { result in
+                results[statusCode] = result
+                expectation.fulfill()
+            }
+
+            waitForExpectations(timeout: timeout, handler: nil)
+        }
+
+        // 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)
+        }
+    }
+
+    func testThatRetryPolicyRetriesRequestsWithRetryableErrors() {
+        // Given
+        let retryPolicy = RetryPolicy()
+        var results: [URLError.Code: RetryResult] = [:]
+
+        // When
+        for code in errorCodes {
+            let request = self.request(method: .get)
+            let error = urlError(with: code)
+
+            let expectation = self.expectation(description: "retry policy should complete")
+
+            retryPolicy.retry(request, for: session, dueTo: error) { result in
+                results[code] = result
+                expectation.fulfill()
+            }
+
+            waitForExpectations(timeout: timeout, handler: nil)
+        }
+
+        // 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)
+        }
+    }
+
+    func testThatRetryPolicyDoesNotRetryErrorsThatAreNotURLErrors() {
+        // Given
+        let retryPolicy = RetryPolicy()
+        let request = self.request(method: .get)
+
+        let errors: [Error] = [
+            resourceUnavailableError,
+            unknownError
+        ]
+
+        var results: [RetryResult] = []
+
+        // When
+        for error in errors {
+            let expectation = self.expectation(description: "retry policy should complete")
+
+            retryPolicy.retry(request, for: session, dueTo: error) { result in
+                results.append(result)
+                expectation.fulfill()
+            }
+
+            waitForExpectations(timeout: timeout, handler: nil)
+        }
+
+        // Then
+        XCTAssertEqual(results.count, errors.count)
+
+        for result in results {
+            XCTAssertEqual(result.retryRequired, false)
+            XCTAssertNil(result.delay)
+            XCTAssertNil(result.error)
+        }
+    }
+
+    // MARK: Tests - Exponential Backoff
+
+    func testThatRetryPolicyTimeDelayBacksOffExponentially() {
+        // Given
+        let retryPolicy = RetryPolicy(retryLimit: 4)
+        let request = self.request(method: .get)
+
+        var results: [Int: RetryResult] = [:]
+
+        // When
+        for index in 0...4 {
+            let expectation = self.expectation(description: "retry policy should complete")
+
+            retryPolicy.retry(request, for: session, dueTo: connectionLostError) { result in
+                results[index] = result
+                expectation.fulfill()
+            }
+
+            waitForExpectations(timeout: timeout, handler: nil)
+
+            request.requestIsRetrying()
+        }
+
+        // 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: Test Helpers
+
+    func request(method: HTTPMethod = .get, statusCode: Int? = nil) -> Request {
+        var response: HTTPURLResponse?
+
+        if let statusCode = 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 {
+        return NSError(domain: URLError.errorDomain, code: code.rawValue, userInfo: nil) as! URLError
+    }
+}
+
+// MARK: -
+
+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])
+    }
+}