CacheTests.swift 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. //
  2. // CacheTests.swift
  3. //
  4. // Copyright (c) 2022 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.Session`
  34. /// - Execute requests for all `Cache-Control` header values to prime the `URLCache` 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. /// For information about `Cache-Control` HTTP headers, please refer to RFC 2616 - Section 14.9.
  41. final class CacheTestCase: BaseTestCase {
  42. // MARK: -
  43. enum CacheControl: String, CaseIterable {
  44. case publicControl = "public"
  45. case privateControl = "private"
  46. case maxAgeNonExpired = "max-age=3600"
  47. case maxAgeExpired = "max-age=0"
  48. case noCache = "no-cache"
  49. case noStore = "no-store"
  50. }
  51. // MARK: - Properties
  52. var urlCache: URLCache!
  53. var manager: Session!
  54. var requests: [CacheControl: URLRequest] = [:]
  55. var timestamps: [CacheControl: String] = [:]
  56. // MARK: - Setup and Teardown
  57. override func setUp() {
  58. super.setUp()
  59. urlCache = {
  60. let capacity = 50 * 1024 * 1024 // MBs
  61. #if targetEnvironment(macCatalyst)
  62. let directory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
  63. return URLCache(memoryCapacity: capacity, diskCapacity: capacity, directory: directory)
  64. #else
  65. let directory = (NSTemporaryDirectory() as NSString).appendingPathComponent(UUID().uuidString)
  66. return URLCache(memoryCapacity: capacity, diskCapacity: capacity, diskPath: directory)
  67. #endif
  68. }()
  69. manager = {
  70. let configuration: URLSessionConfiguration = {
  71. let configuration = URLSessionConfiguration.default
  72. configuration.headers = HTTPHeaders.default
  73. configuration.requestCachePolicy = .useProtocolCachePolicy
  74. configuration.urlCache = urlCache
  75. return configuration
  76. }()
  77. let manager = Session(configuration: configuration)
  78. return manager
  79. }()
  80. primeCachedResponses()
  81. }
  82. override func tearDown() {
  83. super.tearDown()
  84. requests.removeAll()
  85. timestamps.removeAll()
  86. urlCache.removeAllCachedResponses()
  87. }
  88. // MARK: - Cache Priming Methods
  89. /// Executes a request for all `Cache-Control` header values to load the response into the `URLCache`.
  90. ///
  91. /// - Note: This implementation leverages dispatch groups to execute all the requests. This ensures the cache
  92. /// contains responses for all requests, properly aged from Firewalk. This allows the tests to distinguish
  93. /// whether the subsequent responses come from the cache or the network based on the timestamp of the
  94. /// response.
  95. private func primeCachedResponses() {
  96. let dispatchGroup = DispatchGroup()
  97. let serialQueue = DispatchQueue(label: "org.alamofire.cache-tests")
  98. for cacheControl in CacheControl.allCases {
  99. dispatchGroup.enter()
  100. let request = startRequest(cacheControl: cacheControl,
  101. queue: serialQueue,
  102. completion: { _, response in
  103. let timestamp = response!.headers["Date"]
  104. self.timestamps[cacheControl] = timestamp
  105. dispatchGroup.leave()
  106. })
  107. requests[cacheControl] = request
  108. }
  109. // Wait for all requests to complete
  110. _ = dispatchGroup.wait(timeout: .now() + timeout)
  111. }
  112. // MARK: - Request Helper Methods
  113. @preconcurrency
  114. @discardableResult
  115. private func startRequest(cacheControl: CacheControl,
  116. cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy,
  117. queue: DispatchQueue = .main,
  118. completion: @escaping @Sendable (URLRequest?, HTTPURLResponse?) -> Void)
  119. -> URLRequest {
  120. let urlRequest = Endpoint(path: .cache,
  121. timeout: 30,
  122. queryItems: [.init(name: "Cache-Control", value: cacheControl.rawValue)],
  123. cachePolicy: cachePolicy).urlRequest
  124. let request = manager.request(urlRequest)
  125. request.response(queue: queue) { response in
  126. completion(response.request, response.response)
  127. }
  128. return urlRequest
  129. }
  130. // MARK: - Test Execution and Verification
  131. @MainActor
  132. private func executeTest(cachePolicy: URLRequest.CachePolicy,
  133. cacheControl: CacheControl,
  134. shouldReturnCachedResponse: Bool) {
  135. // Given
  136. let requestDidFinish = expectation(description: "cache test request did finish")
  137. var response: HTTPURLResponse?
  138. // When
  139. startRequest(cacheControl: cacheControl, cachePolicy: cachePolicy) { _, responseResponse in
  140. response = responseResponse
  141. requestDidFinish.fulfill()
  142. }
  143. waitForExpectations(timeout: timeout)
  144. // Then
  145. verifyResponse(response, forCacheControl: cacheControl, isCachedResponse: shouldReturnCachedResponse)
  146. }
  147. private func verifyResponse(_ response: HTTPURLResponse?, forCacheControl cacheControl: CacheControl, isCachedResponse: Bool) {
  148. guard let cachedResponseTimestamp = timestamps[cacheControl] else {
  149. XCTFail("cached response timestamp should not be nil")
  150. return
  151. }
  152. if let response, let timestamp = response.headers["Date"] {
  153. if isCachedResponse {
  154. XCTAssertEqual(timestamp, cachedResponseTimestamp, "timestamps should be equal")
  155. } else {
  156. XCTAssertNotEqual(timestamp, cachedResponseTimestamp, "timestamps should not be equal")
  157. }
  158. } else {
  159. XCTFail("response should not be nil")
  160. }
  161. }
  162. // MARK: - Tests
  163. func testURLCacheContainsCachedResponsesForAllRequests() {
  164. // Given
  165. let publicRequest = requests[.publicControl]!
  166. let privateRequest = requests[.privateControl]!
  167. let maxAgeNonExpiredRequest = requests[.maxAgeNonExpired]!
  168. let maxAgeExpiredRequest = requests[.maxAgeExpired]!
  169. let noCacheRequest = requests[.noCache]!
  170. let noStoreRequest = requests[.noStore]!
  171. // When
  172. let publicResponse = urlCache.cachedResponse(for: publicRequest)
  173. let privateResponse = urlCache.cachedResponse(for: privateRequest)
  174. let maxAgeNonExpiredResponse = urlCache.cachedResponse(for: maxAgeNonExpiredRequest)
  175. let maxAgeExpiredResponse = urlCache.cachedResponse(for: maxAgeExpiredRequest)
  176. let noCacheResponse = urlCache.cachedResponse(for: noCacheRequest)
  177. let noStoreResponse = urlCache.cachedResponse(for: noStoreRequest)
  178. // Then
  179. XCTAssertNotNil(publicResponse, "\(CacheControl.publicControl) response should not be nil")
  180. XCTAssertNotNil(privateResponse, "\(CacheControl.privateControl) response should not be nil")
  181. XCTAssertNotNil(maxAgeNonExpiredResponse, "\(CacheControl.maxAgeNonExpired) response should not be nil")
  182. XCTAssertNotNil(maxAgeExpiredResponse, "\(CacheControl.maxAgeExpired) response should not be nil")
  183. XCTAssertNotNil(noCacheResponse, "\(CacheControl.noCache) response should not be nil")
  184. XCTAssertNil(noStoreResponse, "\(CacheControl.noStore) response should be nil")
  185. }
  186. @MainActor
  187. func testDefaultCachePolicy() {
  188. let cachePolicy: URLRequest.CachePolicy = .useProtocolCachePolicy
  189. executeTest(cachePolicy: cachePolicy, cacheControl: .publicControl, shouldReturnCachedResponse: false)
  190. executeTest(cachePolicy: cachePolicy, cacheControl: .privateControl, shouldReturnCachedResponse: false)
  191. executeTest(cachePolicy: cachePolicy, cacheControl: .maxAgeNonExpired, shouldReturnCachedResponse: true)
  192. executeTest(cachePolicy: cachePolicy, cacheControl: .maxAgeExpired, shouldReturnCachedResponse: false)
  193. executeTest(cachePolicy: cachePolicy, cacheControl: .noCache, shouldReturnCachedResponse: false)
  194. executeTest(cachePolicy: cachePolicy, cacheControl: .noStore, shouldReturnCachedResponse: false)
  195. }
  196. @MainActor
  197. func testIgnoreLocalCacheDataPolicy() {
  198. let cachePolicy: URLRequest.CachePolicy = .reloadIgnoringLocalCacheData
  199. executeTest(cachePolicy: cachePolicy, cacheControl: .publicControl, shouldReturnCachedResponse: false)
  200. executeTest(cachePolicy: cachePolicy, cacheControl: .privateControl, shouldReturnCachedResponse: false)
  201. executeTest(cachePolicy: cachePolicy, cacheControl: .maxAgeNonExpired, shouldReturnCachedResponse: false)
  202. executeTest(cachePolicy: cachePolicy, cacheControl: .maxAgeExpired, shouldReturnCachedResponse: false)
  203. executeTest(cachePolicy: cachePolicy, cacheControl: .noCache, shouldReturnCachedResponse: false)
  204. executeTest(cachePolicy: cachePolicy, cacheControl: .noStore, shouldReturnCachedResponse: false)
  205. }
  206. @MainActor
  207. func testUseLocalCacheDataIfExistsOtherwiseLoadFromNetworkPolicy() {
  208. let cachePolicy: URLRequest.CachePolicy = .returnCacheDataElseLoad
  209. executeTest(cachePolicy: cachePolicy, cacheControl: .publicControl, shouldReturnCachedResponse: true)
  210. executeTest(cachePolicy: cachePolicy, cacheControl: .privateControl, shouldReturnCachedResponse: true)
  211. executeTest(cachePolicy: cachePolicy, cacheControl: .maxAgeNonExpired, shouldReturnCachedResponse: true)
  212. executeTest(cachePolicy: cachePolicy, cacheControl: .maxAgeExpired, shouldReturnCachedResponse: true)
  213. executeTest(cachePolicy: cachePolicy, cacheControl: .noCache, shouldReturnCachedResponse: true)
  214. executeTest(cachePolicy: cachePolicy, cacheControl: .noStore, shouldReturnCachedResponse: false)
  215. }
  216. @MainActor
  217. func testUseLocalCacheDataAndDontLoadFromNetworkPolicy() {
  218. let cachePolicy: URLRequest.CachePolicy = .returnCacheDataDontLoad
  219. executeTest(cachePolicy: cachePolicy, cacheControl: .publicControl, shouldReturnCachedResponse: true)
  220. executeTest(cachePolicy: cachePolicy, cacheControl: .privateControl, shouldReturnCachedResponse: true)
  221. executeTest(cachePolicy: cachePolicy, cacheControl: .maxAgeNonExpired, shouldReturnCachedResponse: true)
  222. executeTest(cachePolicy: cachePolicy, cacheControl: .maxAgeExpired, shouldReturnCachedResponse: true)
  223. executeTest(cachePolicy: cachePolicy, cacheControl: .noCache, shouldReturnCachedResponse: true)
  224. // Given
  225. let requestDidFinish = expectation(description: "don't load from network request finished")
  226. var response: HTTPURLResponse?
  227. // When
  228. startRequest(cacheControl: .noStore, cachePolicy: cachePolicy) { _, responseResponse in
  229. response = responseResponse
  230. requestDidFinish.fulfill()
  231. }
  232. waitForExpectations(timeout: timeout)
  233. // Then
  234. XCTAssertNil(response, "response should be nil")
  235. }
  236. }