| // CacheTests.swift |
| // |
| // Copyright (c) 2014–2016 Alamofire Software Foundation (http://alamofire.org/) |
| // |
| // 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 |
| |
| /** |
| This test case tests all implemented cache policies against various `Cache-Control` header values. These tests |
| are meant to cover the main cases of `Cache-Control` header usage, but are no means exhaustive. |
| |
| These tests work as follows: |
| |
| - Set up an `NSURLCache` |
| - Set up an `Alamofire.Manager` |
| - Execute requests for all `Cache-Control` headers values to prime the `NSURLCache` with cached responses |
| - 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. |
| */ |
| class CacheTestCase: BaseTestCase { |
| |
| // 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 |
| |
| var URLCache: NSURLCache! |
| var manager: Manager! |
| |
| let URLString = "https://httpbin.org/response-headers" |
| let requestTimeout: NSTimeInterval = 30 |
| |
| var requests: [String: NSURLRequest] = [:] |
| var timestamps: [String: String] = [:] |
| |
| // MARK: - Setup and Teardown |
| |
| override func setUp() { |
| super.setUp() |
| |
| URLCache = { |
| let capacity = 50 * 1024 * 1024 // MBs |
| let URLCache = NSURLCache(memoryCapacity: capacity, diskCapacity: capacity, diskPath: nil) |
| |
| return URLCache |
| }() |
| |
| manager = { |
| let configuration: NSURLSessionConfiguration = { |
| let configuration = NSURLSessionConfiguration.defaultSessionConfiguration() |
| configuration.HTTPAdditionalHeaders = Alamofire.Manager.defaultHTTPHeaders |
| configuration.requestCachePolicy = .UseProtocolCachePolicy |
| configuration.URLCache = self.URLCache |
| |
| return configuration |
| }() |
| |
| let manager = Manager(configuration: configuration) |
| |
| return manager |
| }() |
| |
| primeCachedResponses() |
| } |
| |
| override func tearDown() { |
| super.tearDown() |
| |
| URLCache.removeAllCachedResponses() |
| } |
| |
| // MARK: - Cache Priming Methods |
| |
| /** |
| 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) |
| |
| for cacheControl in CacheControl.allValues { |
| dispatch_group_enter(dispatchGroup) |
| |
| let request = startRequest( |
| cacheControl: cacheControl, |
| queue: highPriorityDispatchQueue, |
| completion: { _, response in |
| let timestamp = response!.allHeaderFields["Date"] as! String |
| self.timestamps[cacheControl] = timestamp |
| |
| dispatch_group_leave(dispatchGroup) |
| } |
| ) |
| |
| requests[cacheControl] = request |
| } |
| |
| // Wait for all requests to complete |
| dispatch_group_wait(dispatchGroup, dispatch_time(DISPATCH_TIME_NOW, Int64(10.0 * Float(NSEC_PER_SEC)))) |
| |
| // Pause for 1 additional second to ensure all timestamps will be different |
| dispatch_group_enter(dispatchGroup) |
| dispatch_after(dispatch_time(DISPATCH_TIME_NOW, Int64(1.0 * Float(NSEC_PER_SEC))), highPriorityDispatchQueue) { |
| dispatch_group_leave(dispatchGroup) |
| } |
| |
| // Wait for our 1 second pause to complete |
| dispatch_group_wait(dispatchGroup, dispatch_time(DISPATCH_TIME_NOW, Int64(10.0 * Float(NSEC_PER_SEC)))) |
| } |
| |
| // MARK: - Request Helper Methods |
| |
| func URLRequest(cacheControl cacheControl: String, cachePolicy: NSURLRequestCachePolicy) -> NSURLRequest { |
| let parameters = ["Cache-Control": cacheControl] |
| let URL = NSURL(string: URLString)! |
| let URLRequest = NSMutableURLRequest(URL: URL, cachePolicy: cachePolicy, timeoutInterval: requestTimeout) |
| URLRequest.HTTPMethod = Method.GET.rawValue |
| |
| return ParameterEncoding.URL.encode(URLRequest, parameters: parameters).0 |
| } |
| |
| func startRequest( |
| cacheControl cacheControl: String, |
| cachePolicy: NSURLRequestCachePolicy = .UseProtocolCachePolicy, |
| queue: dispatch_queue_t = dispatch_get_main_queue(), |
| completion: (NSURLRequest?, NSHTTPURLResponse?) -> Void) |
| -> NSURLRequest |
| { |
| let urlRequest = URLRequest(cacheControl: cacheControl, cachePolicy: cachePolicy) |
| |
| let request = manager.request(urlRequest) |
| request.response( |
| queue: queue, |
| completionHandler: { _, response, data, _ in |
| completion(request.request, response) |
| } |
| ) |
| |
| return urlRequest |
| } |
| |
| // MARK: - Test Execution and Verification |
| |
| func executeTest( |
| cachePolicy cachePolicy: NSURLRequestCachePolicy, |
| cacheControl: String, |
| shouldReturnCachedResponse: Bool) |
| { |
| // Given |
| let expectation = expectationWithDescription("GET request to httpbin") |
| var response: NSHTTPURLResponse? |
| |
| // When |
| startRequest(cacheControl: cacheControl, cachePolicy: cachePolicy) { _, responseResponse in |
| response = responseResponse |
| expectation.fulfill() |
| } |
| |
| waitForExpectationsWithTimeout(timeout, handler: nil) |
| |
| // Then |
| verifyResponse(response, forCacheControl: cacheControl, isCachedResponse: shouldReturnCachedResponse) |
| } |
| |
| func verifyResponse(response: NSHTTPURLResponse?, forCacheControl cacheControl: String, isCachedResponse: Bool) { |
| guard let cachedResponseTimestamp = timestamps[cacheControl] else { |
| XCTFail("cached response timestamp should not be nil") |
| return |
| } |
| |
| if let |
| response = response, |
| timestamp = response.allHeaderFields["Date"] as? String |
| { |
| 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") |
| } |
| } |
| |
| // 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 |
| } |
| |
| // MARK: - Tests |
| |
| func testURLCacheContainsCachedResponsesForAllRequests() { |
| // Given |
| 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]! |
| |
| // When |
| 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) |
| |
| // Then |
| 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") |
| |
| if isCachedResponseForNoStoreHeaderExpected() { |
| XCTAssertNotNil(noStoreResponse, "\(CacheControl.NoStore) response should not be nil") |
| } else { |
| XCTAssertNil(noStoreResponse, "\(CacheControl.NoStore) response should be nil") |
| } |
| } |
| |
| func testDefaultCachePolicy() { |
| let cachePolicy: NSURLRequestCachePolicy = .UseProtocolCachePolicy |
| |
| 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) |
| } |
| |
| func testIgnoreLocalCacheDataPolicy() { |
| let cachePolicy: NSURLRequestCachePolicy = .ReloadIgnoringLocalCacheData |
| |
| 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) |
| } |
| |
| 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) |
| |
| if isCachedResponseForNoStoreHeaderExpected() { |
| executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoStore, shouldReturnCachedResponse: true) |
| } else { |
| executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoStore, shouldReturnCachedResponse: false) |
| } |
| } |
| |
| func testUseLocalCacheDataAndDontLoadFromNetworkPolicy() { |
| let cachePolicy: NSURLRequestCachePolicy = .ReturnCacheDataDontLoad |
| |
| 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) |
| |
| if isCachedResponseForNoStoreHeaderExpected() { |
| executeTest(cachePolicy: cachePolicy, cacheControl: CacheControl.NoStore, shouldReturnCachedResponse: true) |
| } else { |
| // Given |
| let expectation = expectationWithDescription("GET request to httpbin") |
| var response: NSHTTPURLResponse? |
| |
| // When |
| startRequest(cacheControl: CacheControl.NoStore, cachePolicy: cachePolicy) { _, responseResponse in |
| response = responseResponse |
| expectation.fulfill() |
| } |
| |
| waitForExpectationsWithTimeout(timeout, handler: nil) |
| |
| // Then |
| XCTAssertNil(response, "response should be nil") |
| } |
| } |
| } |