RetryPolicyTests.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403
  1. //
  2. // RetryPolicyTests.swift
  3. //
  4. // Copyright (c) 2019 Alamofire Software Foundation (http://alamofire.org/)
  5. //
  6. // Permission is hereby granted, free of charge, to any person obtaining a copy
  7. // of this software and associated documentation files (the "Software"), to deal
  8. // in the Software without restriction, including without limitation the rights
  9. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  10. // copies of the Software, and to permit persons to whom the Software is
  11. // furnished to do so, subject to the following conditions:
  12. //
  13. // The above copyright notice and this permission notice shall be included in
  14. // all copies or substantial portions of the Software.
  15. //
  16. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  17. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  18. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  19. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  20. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  21. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  22. // THE SOFTWARE.
  23. //
  24. @testable import Alamofire
  25. import Foundation
  26. import XCTest
  27. class BaseRetryPolicyTestCase: BaseTestCase {
  28. // MARK: Helper Types
  29. class StubRequest: DataRequest {
  30. let urlRequest: URLRequest
  31. override var request: URLRequest? { return urlRequest }
  32. let mockedResponse: HTTPURLResponse?
  33. override var response: HTTPURLResponse? { return mockedResponse }
  34. init(_ url: URL, method: HTTPMethod, response: HTTPURLResponse?, session: Session) {
  35. mockedResponse = response
  36. let request = Session.RequestConvertible(
  37. url: url,
  38. method: method,
  39. parameters: nil,
  40. encoding: URLEncoding.default,
  41. headers: nil
  42. )
  43. urlRequest = try! request.asURLRequest()
  44. super.init(
  45. convertible: request,
  46. underlyingQueue: session.rootQueue,
  47. serializationQueue: session.serializationQueue,
  48. eventMonitor: session.eventMonitor,
  49. interceptor: nil,
  50. delegate: session
  51. )
  52. }
  53. }
  54. // MARK: Properties
  55. let idempotentMethods: Set<HTTPMethod> = [.get, .head, .put, .delete, .options, .trace]
  56. let nonIdempotentMethods: Set<HTTPMethod> = [.post, .patch, .connect]
  57. var methods: Set<HTTPMethod> { return idempotentMethods.union(nonIdempotentMethods) }
  58. let session = Session(startRequestsImmediately: false)
  59. let url = URL(string: "https://api.alamofire.org")!
  60. let connectionLostError = NSError(domain: URLError.errorDomain, code: URLError.networkConnectionLost.rawValue, userInfo: nil)
  61. let resourceUnavailableError = NSError(domain: URLError.errorDomain, code: URLError.resourceUnavailable.rawValue, userInfo: nil)
  62. let unknownError = NSError(domain: URLError.errorDomain, code: URLError.unknown.rawValue, userInfo: nil)
  63. let retryableStatusCodes: Set<Int> = [408, 500, 502, 503, 504]
  64. let retryableErrorCodes: Set<URLError.Code> = [
  65. .backgroundSessionInUseByAnotherProcess,
  66. .backgroundSessionWasDisconnected,
  67. .badServerResponse,
  68. .callIsActive,
  69. .cannotConnectToHost,
  70. .cannotFindHost,
  71. .cannotLoadFromNetwork,
  72. .dataNotAllowed,
  73. .dnsLookupFailed,
  74. .downloadDecodingFailedMidStream,
  75. .downloadDecodingFailedToComplete,
  76. .internationalRoamingOff,
  77. .networkConnectionLost,
  78. .notConnectedToInternet,
  79. .secureConnectionFailed,
  80. .serverCertificateHasBadDate,
  81. .serverCertificateNotYetValid,
  82. .timedOut
  83. ]
  84. let nonRetryableErrorCodes: Set<URLError.Code> = [
  85. .appTransportSecurityRequiresSecureConnection,
  86. .backgroundSessionRequiresSharedContainer,
  87. .badURL,
  88. .cancelled,
  89. .cannotCloseFile,
  90. .cannotCreateFile,
  91. .cannotDecodeContentData,
  92. .cannotDecodeRawData,
  93. .cannotMoveFile,
  94. .cannotOpenFile,
  95. .cannotParseResponse,
  96. .cannotRemoveFile,
  97. .cannotWriteToFile,
  98. .clientCertificateRejected,
  99. .clientCertificateRequired,
  100. .dataLengthExceedsMaximum,
  101. .fileDoesNotExist,
  102. .fileIsDirectory,
  103. .httpTooManyRedirects,
  104. .noPermissionsToReadFile,
  105. .redirectToNonExistentLocation,
  106. .requestBodyStreamExhausted,
  107. .resourceUnavailable,
  108. .serverCertificateHasUnknownRoot,
  109. .serverCertificateUntrusted,
  110. .unknown,
  111. .unsupportedURL,
  112. .userAuthenticationRequired,
  113. .userCancelledAuthentication,
  114. .zeroByteResource
  115. ]
  116. var errorCodes: Set<URLError.Code> {
  117. return retryableErrorCodes.union(nonRetryableErrorCodes)
  118. }
  119. }
  120. // MARK: -
  121. class RetryPolicyTestCase: BaseRetryPolicyTestCase {
  122. // MARK: Tests - Retry
  123. func testThatRetryPolicyRetriesRequestsBelowRetryLimit() {
  124. // Given
  125. let retryPolicy = RetryPolicy()
  126. let request = self.request(method: .get)
  127. var results: [Int: RetryResult] = [:]
  128. // When
  129. for index in 0...2 {
  130. let expectation = self.expectation(description: "retry policy should complete")
  131. retryPolicy.retry(request, for: session, dueTo: connectionLostError) { result in
  132. results[index] = result
  133. expectation.fulfill()
  134. }
  135. waitForExpectations(timeout: timeout, handler: nil)
  136. request.prepareForRetry()
  137. }
  138. // Then
  139. XCTAssertEqual(results.count, 3)
  140. if results.count == 3 {
  141. XCTAssertEqual(results[0]?.retryRequired, true)
  142. XCTAssertEqual(results[0]?.delay, 0.5)
  143. XCTAssertNil(results[0]?.error)
  144. XCTAssertEqual(results[1]?.retryRequired, true)
  145. XCTAssertEqual(results[1]?.delay, 1.0)
  146. XCTAssertNil(results[1]?.error)
  147. XCTAssertEqual(results[2]?.retryRequired, false)
  148. XCTAssertNil(results[2]?.delay)
  149. XCTAssertNil(results[2]?.error)
  150. }
  151. }
  152. func testThatRetryPolicyRetriesIdempotentRequests() {
  153. // Given
  154. let retryPolicy = RetryPolicy()
  155. var results: [HTTPMethod: RetryResult] = [:]
  156. // When
  157. for method in methods {
  158. let request = self.request(method: method)
  159. let expectation = self.expectation(description: "retry policy should complete")
  160. retryPolicy.retry(request, for: session, dueTo: connectionLostError) { result in
  161. results[method] = result
  162. expectation.fulfill()
  163. }
  164. waitForExpectations(timeout: timeout, handler: nil)
  165. }
  166. // Then
  167. XCTAssertEqual(results.count, methods.count)
  168. for (method, result) in results {
  169. XCTAssertEqual(result.retryRequired, idempotentMethods.contains(method))
  170. XCTAssertEqual(result.delay, result.retryRequired ? 0.5 : nil)
  171. XCTAssertNil(result.error)
  172. }
  173. }
  174. func testThatRetryPolicyRetriesRequestsWithRetryableStatusCodes() {
  175. // Given
  176. let retryPolicy = RetryPolicy()
  177. let statusCodes = Set(100...599)
  178. var results: [Int: RetryResult] = [:]
  179. // When
  180. for statusCode in statusCodes {
  181. let request = self.request(method: .get, statusCode: statusCode)
  182. let expectation = self.expectation(description: "retry policy should complete")
  183. retryPolicy.retry(request, for: session, dueTo: unknownError) { result in
  184. results[statusCode] = result
  185. expectation.fulfill()
  186. }
  187. waitForExpectations(timeout: timeout, handler: nil)
  188. }
  189. // Then
  190. XCTAssertEqual(results.count, statusCodes.count)
  191. for (statusCode, result) in results {
  192. XCTAssertEqual(result.retryRequired, retryableStatusCodes.contains(statusCode))
  193. XCTAssertEqual(result.delay, result.retryRequired ? 0.5 : nil)
  194. XCTAssertNil(result.error)
  195. }
  196. }
  197. func testThatRetryPolicyRetriesRequestsWithRetryableErrors() {
  198. // Given
  199. let retryPolicy = RetryPolicy()
  200. var results: [URLError.Code: RetryResult] = [:]
  201. // When
  202. for code in errorCodes {
  203. let request = self.request(method: .get)
  204. let error = urlError(with: code)
  205. let expectation = self.expectation(description: "retry policy should complete")
  206. retryPolicy.retry(request, for: session, dueTo: error) { result in
  207. results[code] = result
  208. expectation.fulfill()
  209. }
  210. waitForExpectations(timeout: timeout, handler: nil)
  211. }
  212. // Then
  213. XCTAssertEqual(results.count, errorCodes.count)
  214. for (urlErrorCode, result) in results {
  215. XCTAssertEqual(result.retryRequired, retryableErrorCodes.contains(urlErrorCode))
  216. XCTAssertEqual(result.delay, result.retryRequired ? 0.5 : nil)
  217. XCTAssertNil(result.error)
  218. }
  219. }
  220. func testThatRetryPolicyDoesNotRetryErrorsThatAreNotURLErrors() {
  221. // Given
  222. let retryPolicy = RetryPolicy()
  223. let request = self.request(method: .get)
  224. let errors: [Error] = [
  225. resourceUnavailableError,
  226. unknownError
  227. ]
  228. var results: [RetryResult] = []
  229. // When
  230. for error in errors {
  231. let expectation = self.expectation(description: "retry policy should complete")
  232. retryPolicy.retry(request, for: session, dueTo: error) { result in
  233. results.append(result)
  234. expectation.fulfill()
  235. }
  236. waitForExpectations(timeout: timeout, handler: nil)
  237. }
  238. // Then
  239. XCTAssertEqual(results.count, errors.count)
  240. for result in results {
  241. XCTAssertEqual(result.retryRequired, false)
  242. XCTAssertNil(result.delay)
  243. XCTAssertNil(result.error)
  244. }
  245. }
  246. // MARK: Tests - Exponential Backoff
  247. func testThatRetryPolicyTimeDelayBacksOffExponentially() {
  248. // Given
  249. let retryPolicy = RetryPolicy(retryLimit: 4)
  250. let request = self.request(method: .get)
  251. var results: [Int: RetryResult] = [:]
  252. // When
  253. for index in 0...4 {
  254. let expectation = self.expectation(description: "retry policy should complete")
  255. retryPolicy.retry(request, for: session, dueTo: connectionLostError) { result in
  256. results[index] = result
  257. expectation.fulfill()
  258. }
  259. waitForExpectations(timeout: timeout, handler: nil)
  260. request.prepareForRetry()
  261. }
  262. // Then
  263. XCTAssertEqual(results.count, 5)
  264. if results.count == 5 {
  265. XCTAssertEqual(results[0]?.retryRequired, true)
  266. XCTAssertEqual(results[0]?.delay, 0.5)
  267. XCTAssertNil(results[0]?.error)
  268. XCTAssertEqual(results[1]?.retryRequired, true)
  269. XCTAssertEqual(results[1]?.delay, 1.0)
  270. XCTAssertNil(results[1]?.error)
  271. XCTAssertEqual(results[2]?.retryRequired, true)
  272. XCTAssertEqual(results[2]?.delay, 2.0)
  273. XCTAssertNil(results[2]?.error)
  274. XCTAssertEqual(results[3]?.retryRequired, true)
  275. XCTAssertEqual(results[3]?.delay, 4.0)
  276. XCTAssertNil(results[3]?.error)
  277. XCTAssertEqual(results[4]?.retryRequired, false)
  278. XCTAssertNil(results[4]?.delay)
  279. XCTAssertNil(results[4]?.error)
  280. }
  281. }
  282. // MARK: Test Helpers
  283. func request(method: HTTPMethod = .get, statusCode: Int? = nil) -> Request {
  284. var response: HTTPURLResponse?
  285. if let statusCode = statusCode {
  286. response = HTTPURLResponse(url: url, statusCode: statusCode, httpVersion: nil, headerFields: nil)
  287. }
  288. return StubRequest(url, method: method, response: response, session: session)
  289. }
  290. func urlError(with code: URLError.Code) -> URLError {
  291. return NSError(domain: URLError.errorDomain, code: code.rawValue, userInfo: nil) as! URLError
  292. }
  293. }
  294. // MARK: -
  295. class ConnectionLostRetryPolicyTestCase: BaseRetryPolicyTestCase {
  296. func testThatConnectionLostRetryPolicyCanBeInitializedWithDefaultValues() {
  297. // Given, When
  298. let retryPolicy = ConnectionLostRetryPolicy()
  299. // Then
  300. XCTAssertEqual(retryPolicy.retryLimit, 2)
  301. XCTAssertEqual(retryPolicy.exponentialBackoffBase, 2)
  302. XCTAssertEqual(retryPolicy.exponentialBackoffScale, 0.5)
  303. XCTAssertEqual(retryPolicy.retryableHTTPMethods, idempotentMethods)
  304. XCTAssertEqual(retryPolicy.retryableHTTPStatusCodes, [])
  305. XCTAssertEqual(retryPolicy.retryableURLErrorCodes, [.networkConnectionLost])
  306. }
  307. func testThatConnectionLostRetryPolicyCanBeInitializedWithCustomValues() {
  308. // Given, When
  309. let retryPolicy = ConnectionLostRetryPolicy(
  310. retryLimit: 3,
  311. exponentialBackoffBase: 4,
  312. exponentialBackoffScale: 0.25,
  313. retryableHTTPMethods: [.delete, .get]
  314. )
  315. // Then
  316. XCTAssertEqual(retryPolicy.retryLimit, 3)
  317. XCTAssertEqual(retryPolicy.exponentialBackoffBase, 4)
  318. XCTAssertEqual(retryPolicy.exponentialBackoffScale, 0.25)
  319. XCTAssertEqual(retryPolicy.retryableHTTPMethods, [.delete, .get])
  320. XCTAssertEqual(retryPolicy.retryableHTTPStatusCodes, [])
  321. XCTAssertEqual(retryPolicy.retryableURLErrorCodes, [.networkConnectionLost])
  322. }
  323. }