CacheTests.swift 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324
  1. //
  2. // CacheTests.swift
  3. //
  4. // Copyright (c) 2014-2018 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. import Alamofire
  25. import Foundation
  26. import XCTest
  27. /// This test case tests all implemented cache policies against various `Cache-Control` header values. These tests
  28. /// are meant to cover the main cases of `Cache-Control` header usage, but are by no means exhaustive.
  29. ///
  30. /// These tests work as follows:
  31. ///
  32. /// - Set up an `URLCache`
  33. /// - Set up an `Alamofire.SessionManager`
  34. /// - Execute requests for all `Cache-Control` header values to prime the `NSURLCache` with cached responses
  35. /// - Start up a new test
  36. /// - Execute another round of the same requests with a given `URLRequestCachePolicy`
  37. /// - Verify whether the response came from the cache or from the network
  38. /// - This is determined by whether the cached response timestamp matches the new response timestamp
  39. ///
  40. /// An important thing to note is the difference in behavior between iOS and macOS. On iOS, a response with
  41. /// a `Cache-Control` header value of `no-store` is still written into the `NSURLCache` where on macOS, it is not.
  42. /// The different tests below reflect and demonstrate this behavior.
  43. ///
  44. /// For information about `Cache-Control` HTTP headers, please refer to RFC 2616 - Section 14.9.
  45. class CacheTestCase: BaseTestCase {
  46. // MARK: -
  47. struct CacheControl {
  48. static let publicControl = "public"
  49. static let privateControl = "private"
  50. static let maxAgeNonExpired = "max-age=3600"
  51. static let maxAgeExpired = "max-age=0"
  52. static let noCache = "no-cache"
  53. static let noStore = "no-store"
  54. static var allValues: [String] {
  55. return [CacheControl.publicControl,
  56. CacheControl.privateControl,
  57. CacheControl.maxAgeNonExpired,
  58. CacheControl.maxAgeExpired,
  59. CacheControl.noCache,
  60. CacheControl.noStore]
  61. }
  62. }
  63. // MARK: - Properties
  64. var urlCache: URLCache!
  65. var manager: Session!
  66. let urlString = "https://httpbin.org/response-headers"
  67. let requestTimeout: TimeInterval = 30
  68. var requests: [String: URLRequest] = [:]
  69. var timestamps: [String: String] = [:]
  70. // MARK: - Setup and Teardown
  71. override func setUp() {
  72. super.setUp()
  73. urlCache = {
  74. let capacity = 50 * 1024 * 1024 // MBs
  75. // swiftformat:disable indent
  76. #if swift(>=5.1)
  77. if #available(OSX 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) {
  78. return URLCache(memoryCapacity: capacity, diskCapacity: capacity)
  79. } else {
  80. return URLCache(memoryCapacity: capacity, diskCapacity: capacity, diskPath: nil)
  81. }
  82. #else
  83. return URLCache(memoryCapacity: capacity, diskCapacity: capacity, diskPath: nil)
  84. #endif
  85. // swiftformat:enable indent
  86. }()
  87. manager = {
  88. let configuration: URLSessionConfiguration = {
  89. let configuration = URLSessionConfiguration.default
  90. configuration.headers = HTTPHeaders.default
  91. configuration.requestCachePolicy = .useProtocolCachePolicy
  92. configuration.urlCache = urlCache
  93. return configuration
  94. }()
  95. let manager = Session(configuration: configuration)
  96. return manager
  97. }()
  98. primeCachedResponses()
  99. }
  100. override func tearDown() {
  101. super.tearDown()
  102. requests.removeAll()
  103. timestamps.removeAll()
  104. urlCache.removeAllCachedResponses()
  105. }
  106. // MARK: - Cache Priming Methods
  107. /**
  108. Executes a request for all `Cache-Control` header values to load the response into the `URLCache`.
  109. This implementation leverages dispatch groups to execute all the requests as well as wait an additional
  110. second before returning. This ensures the cache contains responses for all requests that are at least
  111. one second old. This allows the tests to distinguish whether the subsequent responses come from the cache
  112. or the network based on the timestamp of the response.
  113. */
  114. func primeCachedResponses() {
  115. let dispatchGroup = DispatchGroup()
  116. let serialQueue = DispatchQueue(label: "org.alamofire.cache-tests")
  117. for cacheControl in CacheControl.allValues {
  118. dispatchGroup.enter()
  119. let request = startRequest(cacheControl: cacheControl,
  120. queue: serialQueue,
  121. completion: { _, response in
  122. let timestamp = response!.allHeaderFields["Date"] as! String
  123. self.timestamps[cacheControl] = timestamp
  124. dispatchGroup.leave()
  125. })
  126. requests[cacheControl] = request
  127. }
  128. // Wait for all requests to complete
  129. _ = dispatchGroup.wait(timeout: .now() + 30)
  130. // Pause for 1 additional second to ensure all timestamps will be different
  131. dispatchGroup.enter()
  132. serialQueue.asyncAfter(deadline: .now() + 1) {
  133. dispatchGroup.leave()
  134. }
  135. // Wait for our 1 second pause to complete
  136. _ = dispatchGroup.wait(timeout: .now() + 1.25)
  137. }
  138. // MARK: - Request Helper Methods
  139. func urlRequest(cacheControl: String, cachePolicy: URLRequest.CachePolicy) -> URLRequest {
  140. let parameters = ["Cache-Control": cacheControl]
  141. let url = URL(string: urlString)!
  142. var urlRequest = URLRequest(url: url, cachePolicy: cachePolicy, timeoutInterval: requestTimeout)
  143. urlRequest.httpMethod = HTTPMethod.get.rawValue
  144. do {
  145. return try URLEncoding.default.encode(urlRequest, with: parameters)
  146. } catch {
  147. return urlRequest
  148. }
  149. }
  150. @discardableResult
  151. func startRequest(cacheControl: String,
  152. cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy,
  153. queue: DispatchQueue = .main,
  154. completion: @escaping (URLRequest?, HTTPURLResponse?) -> Void)
  155. -> URLRequest {
  156. let urlRequest = self.urlRequest(cacheControl: cacheControl, cachePolicy: cachePolicy)
  157. let request = manager.request(urlRequest)
  158. request.response(queue: queue,
  159. completionHandler: { response in
  160. completion(response.request, response.response)
  161. })
  162. return urlRequest
  163. }
  164. // MARK: - Test Execution and Verification
  165. func executeTest(cachePolicy: URLRequest.CachePolicy,
  166. cacheControl: String,
  167. shouldReturnCachedResponse: Bool) {
  168. // Given
  169. let expectation = self.expectation(description: "GET request to httpbin")
  170. var response: HTTPURLResponse?
  171. // When
  172. startRequest(cacheControl: cacheControl, cachePolicy: cachePolicy) { _, responseResponse in
  173. response = responseResponse
  174. expectation.fulfill()
  175. }
  176. waitForExpectations(timeout: timeout, handler: nil)
  177. // Then
  178. verifyResponse(response, forCacheControl: cacheControl, isCachedResponse: shouldReturnCachedResponse)
  179. }
  180. func verifyResponse(_ response: HTTPURLResponse?, forCacheControl cacheControl: String, isCachedResponse: Bool) {
  181. guard let cachedResponseTimestamp = timestamps[cacheControl] else {
  182. XCTFail("cached response timestamp should not be nil")
  183. return
  184. }
  185. if let response = response, let timestamp = response.allHeaderFields["Date"] as? String {
  186. if isCachedResponse {
  187. XCTAssertEqual(timestamp, cachedResponseTimestamp, "timestamps should be equal")
  188. } else {
  189. XCTAssertNotEqual(timestamp, cachedResponseTimestamp, "timestamps should not be equal")
  190. }
  191. } else {
  192. XCTFail("response should not be nil")
  193. }
  194. }
  195. // MARK: - Tests
  196. func testURLCacheContainsCachedResponsesForAllRequests() {
  197. // Given
  198. let publicRequest = requests[CacheControl.publicControl]!
  199. let privateRequest = requests[CacheControl.privateControl]!
  200. let maxAgeNonExpiredRequest = requests[CacheControl.maxAgeNonExpired]!
  201. let maxAgeExpiredRequest = requests[CacheControl.maxAgeExpired]!
  202. let noCacheRequest = requests[CacheControl.noCache]!
  203. let noStoreRequest = requests[CacheControl.noStore]!
  204. // When
  205. let publicResponse = urlCache.cachedResponse(for: publicRequest)
  206. let privateResponse = urlCache.cachedResponse(for: privateRequest)
  207. let maxAgeNonExpiredResponse = urlCache.cachedResponse(for: maxAgeNonExpiredRequest)
  208. let maxAgeExpiredResponse = urlCache.cachedResponse(for: maxAgeExpiredRequest)
  209. let noCacheResponse = urlCache.cachedResponse(for: noCacheRequest)
  210. let noStoreResponse = urlCache.cachedResponse(for: noStoreRequest)
  211. // Then
  212. XCTAssertNotNil(publicResponse, "\(CacheControl.publicControl) response should not be nil")
  213. XCTAssertNotNil(privateResponse, "\(CacheControl.privateControl) response should not be nil")
  214. XCTAssertNotNil(maxAgeNonExpiredResponse, "\(CacheControl.maxAgeNonExpired) response should not be nil")
  215. XCTAssertNotNil(maxAgeExpiredResponse, "\(CacheControl.maxAgeExpired) response should not be nil")
  216. XCTAssertNotNil(noCacheResponse, "\(CacheControl.noCache) response should not be nil")
  217. XCTAssertNil(noStoreResponse, "\(CacheControl.noStore) response should be nil")
  218. }
  219. func testDefaultCachePolicy() {
  220. let cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy
  221. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.publicControl, shouldReturnCachedResponse: false)
  222. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.privateControl, shouldReturnCachedResponse: false)
  223. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeNonExpired, shouldReturnCachedResponse: true)
  224. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeExpired, shouldReturnCachedResponse: false)
  225. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noCache, shouldReturnCachedResponse: false)
  226. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noStore, shouldReturnCachedResponse: false)
  227. }
  228. func testIgnoreLocalCacheDataPolicy() {
  229. let cachePolicy: URLRequest.CachePolicy = .reloadIgnoringLocalCacheData
  230. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.publicControl, shouldReturnCachedResponse: false)
  231. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.privateControl, shouldReturnCachedResponse: false)
  232. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeNonExpired, shouldReturnCachedResponse: false)
  233. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeExpired, shouldReturnCachedResponse: false)
  234. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noCache, shouldReturnCachedResponse: false)
  235. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noStore, shouldReturnCachedResponse: false)
  236. }
  237. func testUseLocalCacheDataIfExistsOtherwiseLoadFromNetworkPolicy() {
  238. let cachePolicy: URLRequest.CachePolicy = .returnCacheDataElseLoad
  239. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.publicControl, shouldReturnCachedResponse: true)
  240. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.privateControl, shouldReturnCachedResponse: true)
  241. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeNonExpired, shouldReturnCachedResponse: true)
  242. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeExpired, shouldReturnCachedResponse: true)
  243. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noCache, shouldReturnCachedResponse: true)
  244. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noStore, shouldReturnCachedResponse: false)
  245. }
  246. func testUseLocalCacheDataAndDontLoadFromNetworkPolicy() {
  247. let cachePolicy: URLRequest.CachePolicy = .returnCacheDataDontLoad
  248. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.publicControl, shouldReturnCachedResponse: true)
  249. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.privateControl, shouldReturnCachedResponse: true)
  250. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeNonExpired, shouldReturnCachedResponse: true)
  251. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.maxAgeExpired, shouldReturnCachedResponse: true)
  252. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.noCache, shouldReturnCachedResponse: true)
  253. // Given
  254. let expectation = self.expectation(description: "GET request to httpbin")
  255. var response: HTTPURLResponse?
  256. // When
  257. startRequest(cacheControl: CacheControl.noStore, cachePolicy: cachePolicy) { _, responseResponse in
  258. response = responseResponse
  259. expectation.fulfill()
  260. }
  261. waitForExpectations(timeout: timeout, handler: nil)
  262. // Then
  263. XCTAssertNil(response, "response should be nil")
  264. }
  265. }