CacheTests.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. // CacheTests.swift
  2. //
  3. // Copyright (c) 2014–2015 Alamofire Software Foundation (http://alamofire.org/)
  4. //
  5. // Permission is hereby granted, free of charge, to any person obtaining a copy
  6. // of this software and associated documentation files (the "Software"), to deal
  7. // in the Software without restriction, including without limitation the rights
  8. // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. // copies of the Software, and to permit persons to whom the Software is
  10. // furnished to do so, subject to the following conditions:
  11. //
  12. // The above copyright notice and this permission notice shall be included in
  13. // all copies or substantial portions of the Software.
  14. //
  15. // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  16. // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  17. // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  18. // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  19. // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  20. // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  21. // THE SOFTWARE.
  22. import Alamofire
  23. import Foundation
  24. import XCTest
  25. /**
  26. This test case tests all implemented cache policies against various `Cache-Control` header values. These tests
  27. are meant to cover the main cases of `Cache-Control` header usage, but are no means exhaustive.
  28. These tests work as follows:
  29. - Set up an `NSURLCache`
  30. - Set up an `Alamofire.Manager`
  31. - Execute requests for all `Cache-Control` headers values to prime the `NSURLCache` with cached responses
  32. - Start up a new test
  33. - Execute another round of the same requests with a given `NSURLRequestCachePolicy`
  34. - Verify whether the response came from the cache or from the network
  35. - This is determined by whether the cached response timestamp matches the new response timestamp
  36. An important thing to note is the difference in behavior between iOS and OS X. On iOS, a response with
  37. a `Cache-Control` header value of `no-store` is still written into the `NSURLCache` where on OS X, it is not.
  38. The different tests below reflect and demonstrate this behavior.
  39. For information about `Cache-Control` HTTP headers, please refer to RFC 2616 - Section 14.9.
  40. */
  41. class CacheTestCase: BaseTestCase {
  42. // MARK: -
  43. struct CacheControl {
  44. static let Public = "public"
  45. static let Private = "private"
  46. static let MaxAgeNonExpired = "max-age=3600"
  47. static let MaxAgeExpired = "max-age=0"
  48. static let NoCache = "no-cache"
  49. static let NoStore = "no-store"
  50. static var allValues: [String] {
  51. return [
  52. CacheControl.Public,
  53. CacheControl.Private,
  54. CacheControl.MaxAgeNonExpired,
  55. CacheControl.MaxAgeExpired,
  56. CacheControl.NoCache,
  57. CacheControl.NoStore
  58. ]
  59. }
  60. }
  61. // MARK: - Properties
  62. var URLCache: NSURLCache!
  63. var manager: Manager!
  64. let URLString = "https://httpbin.org/response-headers"
  65. let requestTimeout: NSTimeInterval = 30
  66. var requests: [String: NSURLRequest] = [:]
  67. var timestamps: [String: String] = [:]
  68. // MARK: - Setup and Teardown
  69. override func setUp() {
  70. super.setUp()
  71. URLCache = {
  72. let capacity = 50 * 1024 * 1024 // MBs
  73. let URLCache = NSURLCache(memoryCapacity: capacity, diskCapacity: capacity, diskPath: nil)
  74. return URLCache
  75. }()
  76. manager = {
  77. let configuration: NSURLSessionConfiguration = {
  78. let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
  79. configuration.HTTPAdditionalHeaders = Alamofire.Manager.defaultHTTPHeaders
  80. configuration.requestCachePolicy = .UseProtocolCachePolicy
  81. configuration.URLCache = self.URLCache
  82. return configuration
  83. }()
  84. let manager = Manager(configuration: configuration)
  85. return manager
  86. }()
  87. primeCachedResponses()
  88. }
  89. override func tearDown() {
  90. super.tearDown()
  91. URLCache.removeAllCachedResponses()
  92. }
  93. // MARK: - Cache Priming Methods
  94. /**
  95. Executes a request for all `Cache-Control` header values to load the response into the `URLCache`.
  96. This implementation leverages dispatch groups to execute all the requests as well as wait an additional
  97. second before returning. This ensures the cache contains responses for all requests that are at least
  98. one second old. This allows the tests to distinguish whether the subsequent responses come from the cache
  99. or the network based on the timestamp of the response.
  100. */
  101. func primeCachedResponses() {
  102. let dispatchGroup = dispatch_group_create()
  103. let highPriorityDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
  104. for cacheControl in CacheControl.allValues {
  105. dispatch_group_enter(dispatchGroup)
  106. let request = startRequest(
  107. cacheControl: cacheControl,
  108. queue: highPriorityDispatchQueue,
  109. completion: { _, response in
  110. let timestamp = response!.allHeaderFields["Date"] as! String
  111. self.timestamps[cacheControl] = timestamp
  112. dispatch_group_leave(dispatchGroup)
  113. }
  114. )
  115. requests[cacheControl] = request
  116. }
  117. // Wait for all requests to complete
  118. dispatch_group_wait(dispatchGroup, dispatch_time(DISPATCH_TIME_NOW, Int64(10.0 * Float(NSEC_PER_SEC))))
  119. // Pause for 1 additional second to ensure all timestamps will be different
  120. dispatch_group_enter(dispatchGroup)
  121. dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1.0 * Float(NSEC_PER_SEC))), highPriorityDispatchQueue) {
  122. dispatch_group_leave(dispatchGroup)
  123. }
  124. // Wait for our 1 second pause to complete
  125. dispatch_group_wait(dispatchGroup, dispatch_time(DISPATCH_TIME_NOW, Int64(10.0 * Float(NSEC_PER_SEC))))
  126. }
  127. // MARK: - Request Helper Methods
  128. func URLRequest(cacheControl cacheControl: String, cachePolicy: NSURLRequestCachePolicy) -> NSURLRequest {
  129. let parameters = ["Cache-Control": cacheControl]
  130. let URL = NSURL(string: URLString)!
  131. let URLRequest = NSMutableURLRequest(URL: URL, cachePolicy: cachePolicy, timeoutInterval: requestTimeout)
  132. URLRequest.HTTPMethod = Method.GET.rawValue
  133. return ParameterEncoding.URL.encode(URLRequest, parameters: parameters).0
  134. }
  135. func startRequest(
  136. cacheControl cacheControl: String,
  137. cachePolicy: NSURLRequestCachePolicy = .UseProtocolCachePolicy,
  138. queue: dispatch_queue_t = dispatch_get_main_queue(),
  139. completion: (NSURLRequest?, NSHTTPURLResponse?) -> Void)
  140. -> NSURLRequest
  141. {
  142. let urlRequest = URLRequest(cacheControl: cacheControl, cachePolicy: cachePolicy)
  143. let request = manager.request(urlRequest)
  144. request.response(
  145. queue,
  146. responseSerializer: Request.dataResponseSerializer(),
  147. completionHandler: { (_, response, data: NSData?, _) in
  148. completion(request.request, response)
  149. }
  150. )
  151. return urlRequest
  152. }
  153. // MARK: - Test Execution and Verification
  154. func executeTest(
  155. cachePolicy cachePolicy: NSURLRequestCachePolicy,
  156. cacheControl: String,
  157. shouldReturnCachedResponse: Bool)
  158. {
  159. // Given
  160. let expectation = expectationWithDescription("GET request to httpbin")
  161. var response: NSHTTPURLResponse?
  162. // When
  163. startRequest(cacheControl: cacheControl, cachePolicy: cachePolicy) { _, responseResponse in
  164. response = responseResponse
  165. expectation.fulfill()
  166. }
  167. waitForExpectationsWithTimeout(defaultTimeout, handler: nil)
  168. // Then
  169. verifyResponse(response, forCacheControl: cacheControl, isCachedResponse: shouldReturnCachedResponse)
  170. }
  171. func verifyResponse(response: NSHTTPURLResponse?, forCacheControl cacheControl: String, isCachedResponse: Bool) {
  172. let cachedResponseTimestamp = timestamps[cacheControl]!
  173. if let
  174. response = response,
  175. timestamp = response.allHeaderFields["Date"] as? String
  176. {
  177. if isCachedResponse {
  178. XCTAssertEqual(timestamp, cachedResponseTimestamp, "timestamps should be equal")
  179. } else {
  180. XCTAssertNotEqual(timestamp, cachedResponseTimestamp, "timestamps should not be equal")
  181. }
  182. } else {
  183. XCTFail("response should not be nil")
  184. }
  185. }
  186. // MARK: - Cache Helper Methods
  187. private func isCachedResponseForNoStoreHeaderExpected() -> Bool {
  188. var storedInCache = false
  189. #if os(iOS)
  190. let operatingSystemVersion = NSOperatingSystemVersion(majorVersion: 8, minorVersion: 3, patchVersion: 0)
  191. if !NSProcessInfo().isOperatingSystemAtLeastVersion(operatingSystemVersion) {
  192. storedInCache = true
  193. }
  194. #endif
  195. return storedInCache
  196. }
  197. // MARK: - Tests
  198. func testURLCacheContainsCachedResponsesForAllRequests() {
  199. // Given
  200. let publicRequest = requests[CacheControl.Public]!
  201. let privateRequest = requests[CacheControl.Private]!
  202. let maxAgeNonExpiredRequest = requests[CacheControl.MaxAgeNonExpired]!
  203. let maxAgeExpiredRequest = requests[CacheControl.MaxAgeExpired]!
  204. let noCacheRequest = requests[CacheControl.NoCache]!
  205. let noStoreRequest = requests[CacheControl.NoStore]!
  206. // When
  207. let publicResponse = URLCache.cachedResponseForRequest(publicRequest)
  208. let privateResponse = URLCache.cachedResponseForRequest(privateRequest)
  209. let maxAgeNonExpiredResponse = URLCache.cachedResponseForRequest(maxAgeNonExpiredRequest)
  210. let maxAgeExpiredResponse = URLCache.cachedResponseForRequest(maxAgeExpiredRequest)
  211. let noCacheResponse = URLCache.cachedResponseForRequest(noCacheRequest)
  212. let noStoreResponse = URLCache.cachedResponseForRequest(noStoreRequest)
  213. // Then
  214. XCTAssertNotNil(publicResponse, "\(CacheControl.Public) response should not be nil")
  215. XCTAssertNotNil(privateResponse, "\(CacheControl.Private) response should not be nil")
  216. XCTAssertNotNil(maxAgeNonExpiredResponse, "\(CacheControl.MaxAgeNonExpired) response should not be nil")
  217. XCTAssertNotNil(maxAgeExpiredResponse, "\(CacheControl.MaxAgeExpired) response should not be nil")
  218. XCTAssertNotNil(noCacheResponse, "\(CacheControl.NoCache) response should not be nil")
  219. if isCachedResponseForNoStoreHeaderExpected() {
  220. XCTAssertNotNil(noStoreResponse, "\(CacheControl.NoStore) response should not be nil")
  221. } else {
  222. XCTAssertNil(noStoreResponse, "\(CacheControl.NoStore) response should be nil")
  223. }
  224. }
  225. func testDefaultCachePolicy() {
  226. let cachePolicy: NSURLRequestCachePolicy = .UseProtocolCachePolicy
  227. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.Public, shouldReturnCachedResponse: false)
  228. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.Private, shouldReturnCachedResponse: false)
  229. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.MaxAgeNonExpired, shouldReturnCachedResponse: true)
  230. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.MaxAgeExpired, shouldReturnCachedResponse: false)
  231. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoCache, shouldReturnCachedResponse: false)
  232. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoStore, shouldReturnCachedResponse: false)
  233. }
  234. func testIgnoreLocalCacheDataPolicy() {
  235. let cachePolicy: NSURLRequestCachePolicy = .ReloadIgnoringLocalCacheData
  236. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.Public, shouldReturnCachedResponse: false)
  237. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.Private, shouldReturnCachedResponse: false)
  238. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.MaxAgeNonExpired, shouldReturnCachedResponse: false)
  239. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.MaxAgeExpired, shouldReturnCachedResponse: false)
  240. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoCache, shouldReturnCachedResponse: false)
  241. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoStore, shouldReturnCachedResponse: false)
  242. }
  243. func testUseLocalCacheDataIfExistsOtherwiseLoadFromNetworkPolicy() {
  244. let cachePolicy: NSURLRequestCachePolicy = .ReturnCacheDataElseLoad
  245. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.Public, shouldReturnCachedResponse: true)
  246. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.Private, shouldReturnCachedResponse: true)
  247. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.MaxAgeNonExpired, shouldReturnCachedResponse: true)
  248. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.MaxAgeExpired, shouldReturnCachedResponse: true)
  249. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoCache, shouldReturnCachedResponse: true)
  250. if isCachedResponseForNoStoreHeaderExpected() {
  251. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoStore, shouldReturnCachedResponse: true)
  252. } else {
  253. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoStore, shouldReturnCachedResponse: false)
  254. }
  255. }
  256. func testUseLocalCacheDataAndDontLoadFromNetworkPolicy() {
  257. let cachePolicy: NSURLRequestCachePolicy = .ReturnCacheDataDontLoad
  258. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.Public, shouldReturnCachedResponse: true)
  259. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.Private, shouldReturnCachedResponse: true)
  260. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.MaxAgeNonExpired, shouldReturnCachedResponse: true)
  261. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.MaxAgeExpired, shouldReturnCachedResponse: true)
  262. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoCache, shouldReturnCachedResponse: true)
  263. if isCachedResponseForNoStoreHeaderExpected() {
  264. executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoStore, shouldReturnCachedResponse: true)
  265. } else {
  266. // Given
  267. let expectation = expectationWithDescription("GET request to httpbin")
  268. var response: NSHTTPURLResponse?
  269. // When
  270. startRequest(cacheControl: CacheControl.NoStore, cachePolicy: cachePolicy) { _, responseResponse in
  271. response = responseResponse
  272. expectation.fulfill()
  273. }
  274. waitForExpectationsWithTimeout(defaultTimeout, handler: nil)
  275. // Then
  276. XCTAssertNil(response, "response should be nil")
  277. }
  278. }
  279. }