CacheTests.swift 15.1 KB
Newer Older
1 2
// CacheTests.swift
//
3
// Copyright (c) 2014–2016 Alamofire Software Foundation (http://alamofire.org/)
4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
//
// 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.

import Alamofire
import Foundation
import XCTest

/**
28
    This test case tests all implemented cache policies against various `Cache-Control` header values. These tests
29
    are meant to cover the main cases of `Cache-Control` header usage, but are by no means exhaustive.
30 31 32 33 34

    These tests work as follows:

    - Set up an `NSURLCache`
    - Set up an `Alamofire.Manager`
35
    - Execute requests for all `Cache-Control` header values to prime the `NSURLCache` with cached responses
36 37 38 39 40 41 42 43 44 45
    - Start up a new test
    - Execute another round of the same requests with a given `NSURLRequestCachePolicy`
    - Verify whether the response came from the cache or from the network
        - This is determined by whether the cached response timestamp matches the new response timestamp

    An important thing to note is the difference in behavior between iOS and OS X. On iOS, a response with
    a `Cache-Control` header value of `no-store` is still written into the `NSURLCache` where on OS X, it is not.
    The different tests below reflect and demonstrate this behavior.

    For information about `Cache-Control` HTTP headers, please refer to RFC 2616 - Section 14.9.
46 47 48
*/
class CacheTestCase: BaseTestCase {

49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
    // MARK: -

    struct CacheControl {
        static let Public = "public"
        static let Private = "private"
        static let MaxAgeNonExpired = "max-age=3600"
        static let MaxAgeExpired = "max-age=0"
        static let NoCache = "no-cache"
        static let NoStore = "no-store"

        static var allValues: [String] {
            return [
                CacheControl.Public,
                CacheControl.Private,
                CacheControl.MaxAgeNonExpired,
                CacheControl.MaxAgeExpired,
                CacheControl.NoCache,
                CacheControl.NoStore
            ]
        }
    }

    // MARK: - Properties
72

73
    var URLCache: NSURLCache!
74 75
    var manager: Manager!

76
    let URLString = "https://httpbin.org/response-headers"
77 78 79 80 81 82
    let requestTimeout: NSTimeInterval = 30

    var requests: [String: NSURLRequest] = [:]
    var timestamps: [String: String] = [:]

    // MARK: - Setup and Teardown
83 84 85 86

    override func setUp() {
        super.setUp()

87
        URLCache = {
88 89
            let capacity = 50 * 1024 * 1024 // MBs
            let URLCache = NSURLCache(memoryCapacity: capacity, diskCapacity: capacity, diskPath: nil)
90

91 92
            return URLCache
        }()
93

94
        manager = {
95 96 97
            let configuration: NSURLSessionConfiguration = {
                let configuration = NSURLSessionConfiguration.defaultSessionConfiguration()
                configuration.HTTPAdditionalHeaders = Alamofire.Manager.defaultHTTPHeaders
98
                configuration.requestCachePolicy = .UseProtocolCachePolicy
99
                configuration.URLCache = URLCache
100 101 102 103 104 105 106 107 108

                return configuration
            }()

            let manager = Manager(configuration: configuration)

            return manager
        }()

109
        primeCachedResponses()
110 111 112 113 114
    }

    override func tearDown() {
        super.tearDown()

115
        URLCache.removeAllCachedResponses()
116 117
    }

118
    // MARK: - Cache Priming Methods
119

120 121 122 123 124 125 126 127 128 129 130
    /**
        Executes a request for all `Cache-Control` header values to load the response into the `URLCache`.
    
        This implementation leverages dispatch groups to execute all the requests as well as wait an additional
        second before returning. This ensures the cache contains responses for all requests that are at least
        one second old. This allows the tests to distinguish whether the subsequent responses come from the cache
        or the network based on the timestamp of the response.
    */
    func primeCachedResponses() {
        let dispatchGroup = dispatch_group_create()
        let highPriorityDispatchQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
131

132 133
        for cacheControl in CacheControl.allValues {
            dispatch_group_enter(dispatchGroup)
134

135
            let request = startRequest(
136 137 138 139 140
                cacheControl: cacheControl,
                queue: highPriorityDispatchQueue,
                completion: { _, response in
                    let timestamp = response!.allHeaderFields["Date"] as! String
                    self.timestamps[cacheControl] = timestamp
141

142 143 144
                    dispatch_group_leave(dispatchGroup)
                }
            )
145

146
            requests[cacheControl] = request
147
        }
148

149
        // Wait for all requests to complete
150
        dispatch_group_wait(dispatchGroup, dispatch_time(DISPATCH_TIME_NOW, Int64(30.0 * Float(NSEC_PER_SEC))))
151

152
        // Pause for 2 additional seconds to ensure all timestamps will be different
153
        dispatch_group_enter(dispatchGroup)
154
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(2.0 * Float(NSEC_PER_SEC))), highPriorityDispatchQueue) {
155 156
            dispatch_group_leave(dispatchGroup)
        }
157

158
        // Wait for our 2 second pause to complete
159
        dispatch_group_wait(dispatchGroup, dispatch_time(DISPATCH_TIME_NOW, Int64(10.0 * Float(NSEC_PER_SEC))))
160 161
    }

162
    // MARK: - Request Helper Methods
163

164
    func URLRequest(cacheControl cacheControl: String, cachePolicy: NSURLRequestCachePolicy) -> NSURLRequest {
165
        let parameters = ["Cache-Control": cacheControl]
166 167
        let URL = NSURL(string: URLString)!
        let URLRequest = NSMutableURLRequest(URL: URL, cachePolicy: cachePolicy, timeoutInterval: requestTimeout)
168
        URLRequest.HTTPMethod = Method.GET.rawValue
169

170
        return ParameterEncoding.URL.encode(URLRequest, parameters: parameters).0
171 172
    }

173
    func startRequest(
174
        cacheControl cacheControl: String,
175 176
        cachePolicy: NSURLRequestCachePolicy = .UseProtocolCachePolicy,
        queue: dispatch_queue_t = dispatch_get_main_queue(),
177
        completion: (NSURLRequest?, NSHTTPURLResponse?) -> Void)
178 179 180
        -> NSURLRequest
    {
        let urlRequest = URLRequest(cacheControl: cacheControl, cachePolicy: cachePolicy)
181

182
        let request = manager.request(urlRequest)
183
        request.response(
184
            queue: queue,
185
            completionHandler: { _, response, data, _ in
186 187 188
                completion(request.request, response)
            }
        )
189

190
        return urlRequest
191 192
    }

193
    // MARK: - Test Execution and Verification
194

195
    func executeTest(
196
        cachePolicy cachePolicy: NSURLRequestCachePolicy,
197 198 199
        cacheControl: String,
        shouldReturnCachedResponse: Bool)
    {
200 201 202 203 204
        // Given
        let expectation = expectationWithDescription("GET request to httpbin")
        var response: NSHTTPURLResponse?

        // When
205
        startRequest(cacheControl: cacheControl, cachePolicy: cachePolicy) { _, responseResponse in
206 207 208
            response = responseResponse
            expectation.fulfill()
        }
209

210
        waitForExpectationsWithTimeout(timeout, handler: nil)
211 212

        // Then
213 214 215 216
        verifyResponse(response, forCacheControl: cacheControl, isCachedResponse: shouldReturnCachedResponse)
    }

    func verifyResponse(response: NSHTTPURLResponse?, forCacheControl cacheControl: String, isCachedResponse: Bool) {
217 218 219 220
        guard let cachedResponseTimestamp = timestamps[cacheControl] else {
            XCTFail("cached response timestamp should not be nil")
            return
        }
221 222

        if let
223 224
            response = response,
            timestamp = response.allHeaderFields["Date"] as? String
225
        {
226 227 228 229 230 231 232
            if isCachedResponse {
                XCTAssertEqual(timestamp, cachedResponseTimestamp, "timestamps should be equal")
            } else {
                XCTAssertNotEqual(timestamp, cachedResponseTimestamp, "timestamps should not be equal")
            }
        } else {
            XCTFail("response should not be nil")
233 234 235
        }
    }

236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251
    // MARK: - Cache Helper Methods

    private func isCachedResponseForNoStoreHeaderExpected() -> Bool {
        var storedInCache = false

        #if os(iOS)
            let operatingSystemVersion = NSOperatingSystemVersion(majorVersion: 8, minorVersion: 3, patchVersion: 0)

            if !NSProcessInfo().isOperatingSystemAtLeastVersion(operatingSystemVersion) {
                storedInCache = true
            }
        #endif

        return storedInCache
    }

252
    // MARK: - Tests
253

254
    func testURLCacheContainsCachedResponsesForAllRequests() {
255
        // Given
256 257 258 259 260 261
        let publicRequest = requests[CacheControl.Public]!
        let privateRequest = requests[CacheControl.Private]!
        let maxAgeNonExpiredRequest = requests[CacheControl.MaxAgeNonExpired]!
        let maxAgeExpiredRequest = requests[CacheControl.MaxAgeExpired]!
        let noCacheRequest = requests[CacheControl.NoCache]!
        let noStoreRequest = requests[CacheControl.NoStore]!
262 263

        // When
264 265 266 267 268 269
        let publicResponse = URLCache.cachedResponseForRequest(publicRequest)
        let privateResponse = URLCache.cachedResponseForRequest(privateRequest)
        let maxAgeNonExpiredResponse = URLCache.cachedResponseForRequest(maxAgeNonExpiredRequest)
        let maxAgeExpiredResponse = URLCache.cachedResponseForRequest(maxAgeExpiredRequest)
        let noCacheResponse = URLCache.cachedResponseForRequest(noCacheRequest)
        let noStoreResponse = URLCache.cachedResponseForRequest(noStoreRequest)
270 271

        // Then
272 273 274 275 276
        XCTAssertNotNil(publicResponse, "\(CacheControl.Public) response should not be nil")
        XCTAssertNotNil(privateResponse, "\(CacheControl.Private) response should not be nil")
        XCTAssertNotNil(maxAgeNonExpiredResponse, "\(CacheControl.MaxAgeNonExpired) response should not be nil")
        XCTAssertNotNil(maxAgeExpiredResponse, "\(CacheControl.MaxAgeExpired) response should not be nil")
        XCTAssertNotNil(noCacheResponse, "\(CacheControl.NoCache) response should not be nil")
277 278 279 280 281 282

        if isCachedResponseForNoStoreHeaderExpected() {
            XCTAssertNotNil(noStoreResponse, "\(CacheControl.NoStore) response should not be nil")
        } else {
            XCTAssertNil(noStoreResponse, "\(CacheControl.NoStore) response should be nil")
        }
283
    }
284

285 286
    func testDefaultCachePolicy() {
        let cachePolicy: NSURLRequestCachePolicy = .UseProtocolCachePolicy
287

288 289 290 291 292 293
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.Public, shouldReturnCachedResponse: false)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.Private, shouldReturnCachedResponse: false)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.MaxAgeNonExpired, shouldReturnCachedResponse: true)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.MaxAgeExpired, shouldReturnCachedResponse: false)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoCache, shouldReturnCachedResponse: false)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoStore, shouldReturnCachedResponse: false)
294 295
    }

296 297
    func testIgnoreLocalCacheDataPolicy() {
        let cachePolicy: NSURLRequestCachePolicy = .ReloadIgnoringLocalCacheData
298

299 300 301 302 303 304
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.Public, shouldReturnCachedResponse: false)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.Private, shouldReturnCachedResponse: false)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.MaxAgeNonExpired, shouldReturnCachedResponse: false)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.MaxAgeExpired, shouldReturnCachedResponse: false)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoCache, shouldReturnCachedResponse: false)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoStore, shouldReturnCachedResponse: false)
305 306
    }

307 308 309 310 311 312 313 314
    func testUseLocalCacheDataIfExistsOtherwiseLoadFromNetworkPolicy() {
        let cachePolicy: NSURLRequestCachePolicy = .ReturnCacheDataElseLoad

        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.Public, shouldReturnCachedResponse: true)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.Private, shouldReturnCachedResponse: true)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.MaxAgeNonExpired, shouldReturnCachedResponse: true)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.MaxAgeExpired, shouldReturnCachedResponse: true)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoCache, shouldReturnCachedResponse: true)
315 316 317 318 319 320

        if isCachedResponseForNoStoreHeaderExpected() {
            executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoStore, shouldReturnCachedResponse: true)
        } else {
            executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoStore, shouldReturnCachedResponse: false)
        }
321 322
    }

323 324
    func testUseLocalCacheDataAndDontLoadFromNetworkPolicy() {
        let cachePolicy: NSURLRequestCachePolicy = .ReturnCacheDataDontLoad
325

326 327 328 329 330
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.Public, shouldReturnCachedResponse: true)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.Private, shouldReturnCachedResponse: true)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.MaxAgeNonExpired, shouldReturnCachedResponse: true)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.MaxAgeExpired, shouldReturnCachedResponse: true)
        executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoCache, shouldReturnCachedResponse: true)
331

332 333 334 335 336 337 338 339
        if isCachedResponseForNoStoreHeaderExpected() {
            executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoStore, shouldReturnCachedResponse: true)
        } else {
            // Given
            let expectation = expectationWithDescription("GET request to httpbin")
            var response: NSHTTPURLResponse?

            // When
340
            startRequest(cacheControl: CacheControl.NoStore, cachePolicy: cachePolicy) { _, responseResponse in
341 342 343
                response = responseResponse
                expectation.fulfill()
            }
344

345
            waitForExpectationsWithTimeout(timeout, handler: nil)
346

347 348
            // Then
            XCTAssertNil(response, "response should be nil")
349
        }
350 351
    }
}