diff --git a/Sources/Networking/HTTPClient/ETagManager.swift b/Sources/Networking/HTTPClient/ETagManager.swift index 1d3933fc24..e2a5e19f44 100644 --- a/Sources/Networking/HTTPClient/ETagManager.swift +++ b/Sources/Networking/HTTPClient/ETagManager.swift @@ -21,27 +21,23 @@ class ETagManager { static let eTagResponseHeaderName = HTTPClient.ResponseHeader.eTag.rawValue private let userDefaults: SynchronizedUserDefaults - private let verificationMode: Signing.ResponseVerificationMode - convenience init(verificationMode: Signing.ResponseVerificationMode) { + convenience init() { self.init( userDefaults: UserDefaults(suiteName: Self.suiteName) // This should never return `nil` for this known `suiteName`, // but `.standard` is a good fallback anyway. - ?? UserDefaults.standard, - verificationMode: verificationMode + ?? UserDefaults.standard ) } - init(userDefaults: UserDefaults, verificationMode: Signing.ResponseVerificationMode) { + init(userDefaults: UserDefaults) { self.userDefaults = .init(userDefaults: userDefaults) - self.verificationMode = verificationMode } /// - Parameter withSignatureVerification: whether the request contains a nonce. func eTagHeader( for urlRequest: URLRequest, - withSignatureVerification: Bool, refreshETag: Bool = false ) -> [String: String] { func eTag() -> (tag: String, date: String?)? { @@ -51,28 +47,12 @@ class ETagManager { return nil } - let shouldUseETag = ( - !withSignatureVerification || - self.shouldIgnoreVerificationErrors || - storedETagAndResponse.verificationResult == .verified - ) + Logger.verbose(Strings.etag.using_etag(urlRequest, + storedETagAndResponse.eTag, + storedETagAndResponse.validationTime)) - if shouldUseETag { - Logger.verbose(Strings.etag.using_etag(urlRequest, - storedETagAndResponse.eTag, - storedETagAndResponse.validationTime)) - - return (tag: storedETagAndResponse.eTag, - date: storedETagAndResponse.validationTime?.millisecondsSince1970.description) - } else { - Logger.verbose(Strings.etag.not_using_etag( - urlRequest, - storedETagAndResponse.verificationResult, - needsSignatureVerification: withSignatureVerification - - )) - return nil - } + return (tag: storedETagAndResponse.eTag, + date: storedETagAndResponse.validationTime?.millisecondsSince1970.description) } let (etag, date) = eTag() ?? ("", nil) @@ -166,13 +146,12 @@ private extension ETagManager { response: HTTPResponse, eTag: String) { if let data = response.body { - if response.shouldStore(ignoreVerificationErrors: self.shouldIgnoreVerificationErrors) { + if response.shouldStore() { self.storeIfPossible( Response( eTag: eTag, statusCode: response.statusCode, - data: data, - verificationResult: response.verificationResult + data: data ), for: request ) @@ -193,10 +172,6 @@ private extension ETagManager { } } - var shouldIgnoreVerificationErrors: Bool { - return !self.verificationMode.isEnabled - } - static let suiteNameBase: String = "revenuecat.etags" static var suiteName: String { guard let bundleID = Bundle.main.bundleIdentifier else { @@ -223,21 +198,17 @@ extension ETagManager { /// Used by the backend for advanced load shedding techniques. @DefaultValue var validationTime: Date? - @DefaultValue - var verificationResult: VerificationResult init( eTag: String, statusCode: HTTPStatusCode, data: Data, - validationTime: Date? = nil, - verificationResult: VerificationResult + validationTime: Date? = nil ) { self.eTag = eTag self.statusCode = statusCode self.data = data self.validationTime = validationTime - self.verificationResult = verificationResult } } @@ -260,8 +231,7 @@ extension ETagManager.Response { statusCode: self.statusCode, responseHeaders: headers, body: self.data, - requestDate: requestDate, - verificationResult: self.verificationResult + requestDate: requestDate ) } @@ -278,13 +248,12 @@ extension ETagManager.Response { private extension HTTPResponse { - func shouldStore(ignoreVerificationErrors: Bool) -> Bool { + func shouldStore() -> Bool { return ( self.statusCode != .notModified && // Note that we do want to store 400 responses to help the server // If the request was wrong, it will also be wrong the next time. - !self.statusCode.isServerError && - (ignoreVerificationErrors || self.verificationResult != .failed) + !self.statusCode.isServerError ) } diff --git a/Sources/Networking/HTTPClient/HTTPClient.swift b/Sources/Networking/HTTPClient/HTTPClient.swift index 7227a1304e..e8fed4148a 100644 --- a/Sources/Networking/HTTPClient/HTTPClient.swift +++ b/Sources/Networking/HTTPClient/HTTPClient.swift @@ -19,7 +19,7 @@ class HTTPClient { typealias RequestHeaders = HTTPRequest.Headers typealias ResponseHeaders = HTTPResponse.Headers - typealias Completion = (HTTPResponse.Result) -> Void + typealias Completion = (VerifiedHTTPResponse.Result) -> Void let systemInfo: SystemInfo let timeout: TimeInterval @@ -253,7 +253,7 @@ private extension HTTPClient { request: Request, urlRequest: URLRequest, data: Data?, - error networkError: Error?) -> HTTPResponse.Result? { + error networkError: Error?) -> VerifiedHTTPResponse.Result? { if let networkError = networkError { return .failure(NetworkError(networkError, dnsChecker: self.dnsChecker)) } @@ -262,36 +262,55 @@ private extension HTTPClient { return .failure(.unexpectedResponse(urlResponse)) } - /// - Returns `nil` if status code is 304, since the response will be empty - /// and fetched from the eTag. - func dataIfAvailable(_ statusCode: HTTPStatusCode) -> Data? { - if statusCode == .notModified { - return nil - } else { - return data - } - } + let statusCode: HTTPStatusCode = .init(rawValue: httpURLResponse.statusCode) - let statusCode = HTTPStatusCode(rawValue: httpURLResponse.statusCode) + // `nil` if status code is 304, since the response will be empty and fetched from the eTag. + let dataIfAvailable = statusCode == .notModified + ? nil + : data + return self.createVerifiedResponse(request: request, + urlRequest: urlRequest, + data: dataIfAvailable, + response: httpURLResponse) + } + + /// - Returns `Result, NetworkError>?` + private func createVerifiedResponse( + request: Request, + urlRequest: URLRequest, + data: Data?, + response httpURLResponse: HTTPURLResponse + ) -> VerifiedHTTPResponse.Result? { return Result - .success(dataIfAvailable(statusCode)) - .mapToResponse(response: httpURLResponse, - request: request.httpRequest, - signing: self.signing(for: request.httpRequest), - verificationMode: request.verificationMode) + .success(data) + .mapToResponse(response: httpURLResponse, request: request.httpRequest) + // Fetch from ETagManager if available .map { (response) -> HTTPResponse? in - guard let cachedResponse = self.eTagManager.httpResultFromCacheOrBackend( + return self.eTagManager.httpResultFromCacheOrBackend( with: response, request: urlRequest, retried: request.retried - ) else { - return nil + ) + } + // Verify response + .map { cachedResponse -> VerifiedHTTPResponse? in + return cachedResponse?.verify( + request: request.httpRequest, + publicKey: request.verificationMode.publicKey, + signing: self.signing(for: request.httpRequest) + ) + } + // Upgrade to error in enforced mode + .flatMap { response -> Result?, NetworkError> in + if let response = response, + response.verificationResult == .failed, + case .enforced = request.verificationMode { + return .failure(.signatureVerificationFailed(path: request.httpRequest.path, + code: response.statusCode)) + } else { + return .success(response) } - - return cachedResponse - .copy(with: .from(cache: cachedResponse.verificationResult, - response: response.verificationResult)) } .asOptionalResult? .convertUnsuccessfulResponseToError() @@ -407,7 +426,6 @@ private extension HTTPClient { if request.httpRequest.path.shouldSendEtag { let eTagHeader = self.eTagManager.eTagHeader( for: urlRequest, - withSignatureVerification: request.httpRequest.nonce != nil, refreshETag: request.retried ) return request.headers.merging(eTagHeader) @@ -499,34 +517,58 @@ private extension NetworkError { } -extension Result where Success == HTTPResponse, Failure == NetworkError { - - // Converts an unsuccessful response into a `Result.failure` - fileprivate func convertUnsuccessfulResponseToError() -> Self { - return self.flatMap { response in - response.statusCode.isSuccessfulResponse - ? .success(response) - : .failure(response.parseUnsuccessfulResponse()) +extension Result where Success == Data?, Failure == NetworkError { + + /// Converts a `Result` into `Result, NetworkError>` + func mapToResponse( + response: HTTPURLResponse, + request: HTTPRequest + ) -> Result, Failure> { + return self.flatMap { body in + return .success( + .init( + statusCode: .init(rawValue: response.statusCode), + responseHeaders: response.allHeaderFields, + body: body + ) + ) } } - // Parses a `Result>` to `Result>` - func parseResponse() -> HTTPResponse.Result { - return self.flatMap { response in // Convert the `Result` type - Result, Error> { // Create a new `Result` - try response.mapBody { data in // Convert the body of `HTTPResponse` from `Data` -> `Value` - try Value.create(with: data) // Decode `Data` into `Value` +} + +extension Result where Success == VerifiedHTTPResponse, Failure == NetworkError { + + // Parses a `Result>` to `Result>` + func parseResponse() -> VerifiedHTTPResponse.Result { + return self.flatMap { response in // Convert the `Result` type + Result, Error> { // Create a new `Result` + try response.mapBody { data in // Convert the from `Data` -> `Value` + try Value.create(with: data) // Decode `Data` into `Value` } - .copyWithNewRequestDate() // Update request date for 304 responses + .copyWithNewRequestDate() // Update request date for 304 responses } // Convert decoding errors into `NetworkError.decoding` - .mapError { NetworkError.decoding($0, response.body) } + .mapError { NetworkError.decoding($0, response.response.body) } + } + } + +} + +extension Result where Success == VerifiedHTTPResponse, Failure == NetworkError { + + // Converts an unsuccessful response into a `Result.failure` + fileprivate func convertUnsuccessfulResponseToError() -> Self { + return self.flatMap { + $0.response.statusCode.isSuccessfulResponse + ? .success($0) + : .failure($0.response.parseUnsuccessfulResponse()) } } } -private extension HTTPResponse { +private extension VerifiedHTTPResponse { func copyWithNewRequestDate() -> Self { // Update request time from server unless it failed verification. @@ -538,7 +580,7 @@ private extension HTTPResponse { } var isLoadShedder: Bool { - return self.value(forHeaderField: HTTPClient.ResponseHeader.isLoadShedder.rawValue) == "true" + return self.response.value(forHeaderField: HTTPClient.ResponseHeader.isLoadShedder.rawValue) == "true" } } diff --git a/Sources/Networking/HTTPClient/HTTPResponse.swift b/Sources/Networking/HTTPClient/HTTPResponse.swift index 13addad650..39fbac1044 100644 --- a/Sources/Networking/HTTPClient/HTTPResponse.swift +++ b/Sources/Networking/HTTPClient/HTTPResponse.swift @@ -25,7 +25,6 @@ struct HTTPResponse { var responseHeaders: HTTPClient.ResponseHeaders var body: Body var requestDate: Date? - var verificationResult: VerificationResult } @@ -43,7 +42,53 @@ extension HTTPResponse: CustomStringConvertible { return """ HTTPResponse( statusCode: \(self.statusCode.rawValue), - body: \(body), + body: \(body) + ) + """ + } + +} + +// MARK: - VerifiedHTTPResponse + +struct VerifiedHTTPResponse { + + typealias Result = Swift.Result + + var response: HTTPResponse + var verificationResult: VerificationResult + + init(response: HTTPResponse, verificationResult: VerificationResult) { + self.response = response + self.verificationResult = verificationResult + } + + init( + statusCode: HTTPStatusCode, + responseHeaders: HTTPClient.ResponseHeaders, + body: Body, + requestDate: Date? = nil, + verificationResult: VerificationResult + ) { + self.init( + response: .init( + statusCode: statusCode, + responseHeaders: responseHeaders, + body: body, + requestDate: requestDate + ), + verificationResult: verificationResult + ) + } + +} + +extension VerifiedHTTPResponse: CustomStringConvertible { + + var description: String { + return """ + VerifiedHTTPResponse( + response: \(self.response.description), verification: \(self.verificationResult) ) """ @@ -51,6 +96,8 @@ extension HTTPResponse: CustomStringConvertible { } +// MARK: - Extensions + extension HTTPResponse { /// Equivalent to `HTTPURLResponse.value(forHTTPHeaderField:)` @@ -88,8 +135,7 @@ extension HTTPResponse where Body: OptionalType, Body.Wrapped: HTTPResponseBody return .init(statusCode: self.statusCode, responseHeaders: self.responseHeaders, body: body, - requestDate: self.requestDate, - verificationResult: self.verificationResult) + requestDate: self.requestDate) } } @@ -100,19 +146,55 @@ extension HTTPResponse { return .init(statusCode: self.statusCode, responseHeaders: self.responseHeaders, body: try mapping(self.body), - requestDate: self.requestDate, - verificationResult: self.verificationResult) + requestDate: self.requestDate) + } + + func verified(with verificationResult: VerificationResult) -> VerifiedHTTPResponse { + return .init( + response: self, + verificationResult: verificationResult + ) + } + +} + +extension HTTPResponse { + + /// Creates an `HTTPResponse` extracting the `requestDate` from its headers + init( + statusCode: HTTPStatusCode, + responseHeaders: HTTPClient.ResponseHeaders, + body: Body + ) { + self.statusCode = statusCode + self.responseHeaders = responseHeaders + self.body = body + self.requestDate = Self.parseRequestDate(headers: responseHeaders) + } + + private static func parseRequestDate(headers: Self.Headers) -> Date? { + guard let stringValue = Self.value( + forCaseInsensitiveHeaderField: HTTPClient.ResponseHeader.requestDate.rawValue, + in: headers + ), + let intValue = UInt64(stringValue) else { return nil } + + return .init(millisecondsSince1970: intValue) } - func copy(with newVerificationResult: VerificationResult) -> Self { - guard newVerificationResult != self.verificationResult else { return self } +} + +extension VerifiedHTTPResponse { + + var statusCode: HTTPStatusCode { self.response.statusCode } + var responseHeaders: HTTPClient.ResponseHeaders { self.response.responseHeaders } + var body: Body { self.response.body } + var requestDate: Date? { self.response.requestDate } + func mapBody(_ mapping: (Body) throws -> NewBody) rethrows -> VerifiedHTTPResponse { return .init( - statusCode: self.statusCode, - responseHeaders: self.responseHeaders, - body: self.body, - requestDate: self.requestDate, - verificationResult: newVerificationResult + response: try self.response.mapBody(mapping), + verificationResult: self.verificationResult ) } diff --git a/Sources/Networking/InternalAPI.swift b/Sources/Networking/InternalAPI.swift index 466528a75b..0ebacb7009 100644 --- a/Sources/Networking/InternalAPI.swift +++ b/Sources/Networking/InternalAPI.swift @@ -88,12 +88,12 @@ private final class HealthOperation: CacheableNetworkOperation { self.httpClient.perform( request, with: self.verificationMode - ) { (response: HTTPResponse.Result) in + ) { (response: VerifiedHTTPResponse.Result) in self.finish(with: response, completion: completion) } } - private func finish(with response: HTTPResponse.Result, + private func finish(with response: VerifiedHTTPResponse.Result, completion: () -> Void) { self.callbackCache.performOnAllItemsAndRemoveFromCache(withCacheable: self) { callback in callback.completion( diff --git a/Sources/Networking/Operations/GetCustomerInfoOperation.swift b/Sources/Networking/Operations/GetCustomerInfoOperation.swift index 00f29b6bd9..ce71a881ac 100644 --- a/Sources/Networking/Operations/GetCustomerInfoOperation.swift +++ b/Sources/Networking/Operations/GetCustomerInfoOperation.swift @@ -82,7 +82,9 @@ private extension GetCustomerInfoOperation { let request = HTTPRequest(method: .get, path: .getCustomerInfo(appUserID: appUserID)) - self.httpClient.perform(request) { (response: HTTPResponse.Result) in + self.httpClient.perform( + request + ) { (response: VerifiedHTTPResponse.Result) in self.customerInfoResponseHandler.handle(customerInfoResponse: response) { result in self.customerInfoCallbackCache.performOnAllItemsAndRemoveFromCache(withCacheable: self) { callback in callback.completion(result) diff --git a/Sources/Networking/Operations/GetIntroEligibilityOperation.swift b/Sources/Networking/Operations/GetIntroEligibilityOperation.swift index 0ce4360111..022d9700d9 100644 --- a/Sources/Networking/Operations/GetIntroEligibilityOperation.swift +++ b/Sources/Networking/Operations/GetIntroEligibilityOperation.swift @@ -76,7 +76,7 @@ private extension GetIntroEligibilityOperation { httpClient.perform( request - ) { (response: HTTPResponse.Result) in + ) { (response: VerifiedHTTPResponse.Result) in self.handleIntroEligibility(result: response, productIdentifiers: self.productIdentifiers, completion: self.responseHandler) @@ -85,7 +85,7 @@ private extension GetIntroEligibilityOperation { } func handleIntroEligibility( - result: Result, NetworkError>, + result: VerifiedHTTPResponse.Result, productIdentifiers: [String], completion: OfferingsAPI.IntroEligibilityResponseHandler ) { diff --git a/Sources/Networking/Operations/GetOfferingsOperation.swift b/Sources/Networking/Operations/GetOfferingsOperation.swift index c617399d9b..cc6f110c92 100644 --- a/Sources/Networking/Operations/GetOfferingsOperation.swift +++ b/Sources/Networking/Operations/GetOfferingsOperation.swift @@ -61,7 +61,7 @@ private extension GetOfferingsOperation { let request = HTTPRequest(method: .get, path: .getOfferings(appUserID: appUserID)) - httpClient.perform(request) { (response: HTTPResponse.Result) in + httpClient.perform(request) { (response: VerifiedHTTPResponse.Result) in defer { completion() } diff --git a/Sources/Networking/Operations/GetProductEntitlementMappingOperation.swift b/Sources/Networking/Operations/GetProductEntitlementMappingOperation.swift index 2e7cfde1d2..f0a40919c2 100644 --- a/Sources/Networking/Operations/GetProductEntitlementMappingOperation.swift +++ b/Sources/Networking/Operations/GetProductEntitlementMappingOperation.swift @@ -49,7 +49,7 @@ private extension GetProductEntitlementMappingOperation { func getResponse(completion: @escaping () -> Void) { let request = HTTPRequest(method: .get, path: .getProductEntitlementMapping) - self.httpClient.perform(request) { (response: HTTPResponse.Result) in + self.httpClient.perform(request) { (response: VerifiedHTTPResponse.Result) in defer { completion() } diff --git a/Sources/Networking/Operations/Handling/CustomerInfoResponseHandler.swift b/Sources/Networking/Operations/Handling/CustomerInfoResponseHandler.swift index 61a1cab436..90dca64081 100644 --- a/Sources/Networking/Operations/Handling/CustomerInfoResponseHandler.swift +++ b/Sources/Networking/Operations/Handling/CustomerInfoResponseHandler.swift @@ -24,7 +24,7 @@ class CustomerInfoResponseHandler { self.userID = userID } - func handle(customerInfoResponse response: HTTPResponse.Result, + func handle(customerInfoResponse response: VerifiedHTTPResponse.Result, completion: @escaping CustomerAPI.CustomerInfoResponseHandler) { let result: Result = response .map { response in diff --git a/Sources/Networking/Operations/LogInOperation.swift b/Sources/Networking/Operations/LogInOperation.swift index b62d1e20d6..a34483788b 100644 --- a/Sources/Networking/Operations/LogInOperation.swift +++ b/Sources/Networking/Operations/LogInOperation.swift @@ -71,7 +71,7 @@ private extension LogInOperation { newAppUserID: newAppUserID)), path: .logIn) - self.httpClient.perform(request) { (response: HTTPResponse.Result) in + self.httpClient.perform(request) { (response: VerifiedHTTPResponse.Result) in self.loginCallbackCache.performOnAllItemsAndRemoveFromCache(withCacheable: self) { callbackObject in self.handleLogin(response, completion: callbackObject.completion) } @@ -80,7 +80,7 @@ private extension LogInOperation { } } - func handleLogin(_ result: HTTPResponse.Result, + func handleLogin(_ result: VerifiedHTTPResponse.Result, completion: IdentityAPI.LogInResponseHandler) { let result: Result<(info: CustomerInfo, created: Bool), BackendError> = result .map { response in diff --git a/Sources/Networking/Operations/PostAdServicesTokenOperation.swift b/Sources/Networking/Operations/PostAdServicesTokenOperation.swift index e212102905..8493f8787e 100644 --- a/Sources/Networking/Operations/PostAdServicesTokenOperation.swift +++ b/Sources/Networking/Operations/PostAdServicesTokenOperation.swift @@ -43,7 +43,7 @@ class PostAdServicesTokenOperation: NetworkOperation { let request = HTTPRequest(method: .post(Body(aadAttributionToken: self.token)), path: .postAdServicesToken(appUserID: appUserID)) - self.httpClient.perform(request) { (response: HTTPResponse.Result) in + self.httpClient.perform(request) { (response: VerifiedHTTPResponse.Result) in defer { completion() } diff --git a/Sources/Networking/Operations/PostAttributionDataOperation.swift b/Sources/Networking/Operations/PostAttributionDataOperation.swift index 4187547e99..213de16b02 100644 --- a/Sources/Networking/Operations/PostAttributionDataOperation.swift +++ b/Sources/Networking/Operations/PostAttributionDataOperation.swift @@ -47,7 +47,7 @@ class PostAttributionDataOperation: NetworkOperation { let request = HTTPRequest(method: .post(Body(network: self.network, attributionData: self.attributionData)), path: .postAttributionData(appUserID: appUserID)) - self.httpClient.perform(request) { (response: HTTPResponse.Result) in + self.httpClient.perform(request) { (response: VerifiedHTTPResponse.Result) in defer { completion() } diff --git a/Sources/Networking/Operations/PostOfferForSigningOperation.swift b/Sources/Networking/Operations/PostOfferForSigningOperation.swift index f4f0cf132d..82424fc1c4 100644 --- a/Sources/Networking/Operations/PostOfferForSigningOperation.swift +++ b/Sources/Networking/Operations/PostOfferForSigningOperation.swift @@ -50,7 +50,7 @@ class PostOfferForSigningOperation: NetworkOperation { path: .postOfferForSigning ) - self.httpClient.perform(request) { (response: HTTPResponse.Result) in + self.httpClient.perform(request) { (response: VerifiedHTTPResponse.Result) in let result: Result = response .mapError { error -> BackendError in if case .decoding = error { diff --git a/Sources/Networking/Operations/PostReceiptDataOperation.swift b/Sources/Networking/Operations/PostReceiptDataOperation.swift index c8b8f26cc5..63c4535f8d 100644 --- a/Sources/Networking/Operations/PostReceiptDataOperation.swift +++ b/Sources/Networking/Operations/PostReceiptDataOperation.swift @@ -112,7 +112,9 @@ final class PostReceiptDataOperation: CacheableNetworkOperation { private func post(completion: @escaping () -> Void) { let request = HTTPRequest(method: .post(self.postData), path: .postReceiptData) - self.httpClient.perform(request) { (response: HTTPResponse.Result) in + self.httpClient.perform( + request + ) { (response: VerifiedHTTPResponse.Result) in self.customerInfoResponseHandler.handle(customerInfoResponse: response) { result in self.customerInfoCallbackCache.performOnAllItemsAndRemoveFromCache( withCacheable: self diff --git a/Sources/Networking/Operations/PostSubscriberAttributesOperation.swift b/Sources/Networking/Operations/PostSubscriberAttributesOperation.swift index 80e3d336a8..0347fb9c18 100644 --- a/Sources/Networking/Operations/PostSubscriberAttributesOperation.swift +++ b/Sources/Networking/Operations/PostSubscriberAttributesOperation.swift @@ -51,7 +51,7 @@ class PostSubscriberAttributesOperation: NetworkOperation { let request = HTTPRequest(method: .post(Body(self.subscriberAttributes)), path: .postSubscriberAttributes(appUserID: appUserID)) - httpClient.perform(request) { (response: HTTPResponse.Result) in + self.httpClient.perform(request) { (response: VerifiedHTTPResponse.Result) in defer { completion() } diff --git a/Sources/Purchasing/Purchases/Purchases.swift b/Sources/Purchasing/Purchases/Purchases.swift index 704d3001a4..5c1112edbe 100644 --- a/Sources/Purchasing/Purchases/Purchases.swift +++ b/Sources/Purchasing/Purchases/Purchases.swift @@ -286,7 +286,7 @@ public typealias StartPurchaseBlock = (@escaping PurchaseCompletedBlock) -> Void } let receiptFetcher = ReceiptFetcher(requestFetcher: fetcher, systemInfo: systemInfo) - let eTagManager = ETagManager(verificationMode: systemInfo.responseVerificationMode) + let eTagManager = ETagManager() let attributionTypeFactory = AttributionTypeFactory() let attributionFetcher = AttributionFetcher(attributionFactory: attributionTypeFactory, systemInfo: systemInfo) let userDefaults = userDefaults ?? UserDefaults.computeDefault() diff --git a/Sources/Security/Signing+ResponseVerification.swift b/Sources/Security/Signing+ResponseVerification.swift index c2019ccb1f..5993068d40 100644 --- a/Sources/Security/Signing+ResponseVerification.swift +++ b/Sources/Security/Signing+ResponseVerification.swift @@ -15,31 +15,16 @@ import Foundation extension HTTPResponse where Body == Data { - static func create(with response: HTTPURLResponse, - body: Data, - request: HTTPRequest, - publicKey: Signing.PublicKey?, - signing: SigningType.Type = Signing.self) -> Self { - return Self.create(with: body, - statusCode: .init(rawValue: response.statusCode), - headers: response.allHeaderFields, - request: request, - publicKey: publicKey, - signing: signing) - } - - static func create(with body: Data, - statusCode: HTTPStatusCode, - headers: HTTPClient.ResponseHeaders, - request: HTTPRequest, - publicKey: Signing.PublicKey?, - signing: SigningType.Type = Signing.self) -> Self { - let requestDate = Self.parseRequestDate(headers: headers) + func verify( + request: HTTPRequest, + publicKey: Signing.PublicKey?, + signing: SigningType.Type = Signing.self + ) -> VerifiedHTTPResponse { let verificationResult = Self.verificationResult( - body: body, - statusCode: statusCode, - headers: headers, - requestDate: requestDate, + body: self.body, + statusCode: self.statusCode, + headers: self.responseHeaders, + requestDate: self.requestDate, request: request, publicKey: publicKey, signing: signing @@ -47,17 +32,16 @@ extension HTTPResponse where Body == Data { #if DEBUG if verificationResult == .failed, ProcessInfo.isRunningRevenueCatTests { - Logger.warn(Strings.signing.invalid_signature_data(request, body, headers, statusCode)) + Logger.warn(Strings.signing.invalid_signature_data( + request, + self.body, + self.responseHeaders, + statusCode + )) } #endif - return .init( - statusCode: statusCode, - responseHeaders: headers, - body: body, - requestDate: requestDate, - verificationResult: verificationResult - ) + return self.verified(with: verificationResult) } // swiftlint:disable:next function_parameter_count @@ -110,59 +94,3 @@ extension HTTPResponse where Body == Data { } } - -extension Result where Success == Data?, Failure == NetworkError { - - /// Converts a `Result` into `Result, NetworkError>` - func mapToResponse( - response: HTTPURLResponse, - request: HTTPRequest, - signing: SigningType.Type, - verificationMode: Signing.ResponseVerificationMode - ) -> Result, Failure> { - return self.flatMap { body in - let response = HTTPResponse.create( - with: response, - body: body ?? .init(), - request: request, - publicKey: verificationMode.publicKey, - signing: signing - ) - - if response.verificationResult == .failed, case .enforced = verificationMode { - return .failure(.signatureVerificationFailed(path: request.path, code: response.statusCode)) - } else { - return .success(response.mapBody(Optional.some)) - } - } - } - -} - -extension HTTPResponse { - - /// Creates an `HTTPResponse` extracting the `requestDate` from its headers - init( - statusCode: HTTPStatusCode, - responseHeaders: HTTPClient.ResponseHeaders, - body: Body, - verificationResult: VerificationResult - ) { - self.statusCode = statusCode - self.responseHeaders = responseHeaders - self.body = body - self.requestDate = Self.parseRequestDate(headers: responseHeaders) - self.verificationResult = verificationResult - } - - static func parseRequestDate(headers: Self.Headers) -> Date? { - guard let stringValue = Self.value( - forCaseInsensitiveHeaderField: HTTPClient.ResponseHeader.requestDate.rawValue, - in: headers - ), - let intValue = UInt64(stringValue) else { return nil } - - return .init(millisecondsSince1970: intValue) - } - -} diff --git a/Sources/Security/VerificationResult.swift b/Sources/Security/VerificationResult.swift index bea0b5271b..dbfad93498 100644 --- a/Sources/Security/VerificationResult.swift +++ b/Sources/Security/VerificationResult.swift @@ -87,32 +87,3 @@ extension VerificationResult: CustomDebugStringConvertible { } -extension VerificationResult { - - /// - Returns: the most restrictive ``VerificationResult`` based on the cached verification and - /// the response verification. - static func from(cache cachedResult: Self, response responseResult: Self) -> Self { - switch (cachedResult, responseResult) { - case (.notRequested, .notRequested), - (.verified, .verified), - (.verifiedOnDevice, .verifiedOnDevice), - (.failed, .failed): - return cachedResult - - case (.verified, .notRequested), (.verifiedOnDevice, .notRequested): return .notRequested - case (.verified, .failed), (.verifiedOnDevice, .failed): return .failed - - case (.notRequested, .verified), (.notRequested, .verifiedOnDevice): return responseResult - case (.notRequested, .failed): return .failed - - case (.failed, .notRequested): return .notRequested - // If the cache verification failed, the etag won't be used - // so the response would only be a 200 and not 304. - // Therefore the cache verification error can be ignored - case (.failed, .verified), (.failed, .verifiedOnDevice): return responseResult - - case (.verifiedOnDevice, .verified), (.verified, .verifiedOnDevice): return responseResult - } - } - -} diff --git a/Tests/UnitTests/Mocks/MockETagManager.swift b/Tests/UnitTests/Mocks/MockETagManager.swift index 41203158f8..8a6e89e522 100644 --- a/Tests/UnitTests/Mocks/MockETagManager.swift +++ b/Tests/UnitTests/Mocks/MockETagManager.swift @@ -14,12 +14,11 @@ import Foundation class MockETagManager: ETagManager { init() { - super.init(userDefaults: MockUserDefaults(), verificationMode: .default) + super.init(userDefaults: MockUserDefaults()) } struct ETagHeaderRequest { var urlRequest: URLRequest - var withSignatureVerification: Bool var refreshETag: Bool } @@ -38,12 +37,10 @@ class MockETagManager: ETagManager { override func eTagHeader( for urlRequest: URLRequest, - withSignatureVerification: Bool, refreshETag: Bool = false ) -> [String: String] { return self.lock.perform { let request: ETagHeaderRequest = .init(urlRequest: urlRequest, - withSignatureVerification: withSignatureVerification, refreshETag: refreshETag) self.invokedETagHeader = true @@ -81,10 +78,18 @@ class MockETagManager: ETagManager { ) self.invokedHTTPResultFromCacheOrBackendParameters = params self.invokedHTTPResultFromCacheOrBackendParametersList.append(params) + if self.shouldReturnResultFromBackend { return response.asOptionalResponse + } else { + // Mimic behavior from `ETagManager`, returning the cached response + // with the original headers and request date + var result = self.stubbedHTTPResultFromCacheOrBackendResult + result?.responseHeaders = response.responseHeaders + result?.requestDate = response.requestDate + + return result } - return self.stubbedHTTPResultFromCacheOrBackendResult } } diff --git a/Tests/UnitTests/Mocks/MockHTTPClient.swift b/Tests/UnitTests/Mocks/MockHTTPClient.swift index 83833ea5f0..90c1087ba6 100644 --- a/Tests/UnitTests/Mocks/MockHTTPClient.swift +++ b/Tests/UnitTests/Mocks/MockHTTPClient.swift @@ -15,10 +15,10 @@ class MockHTTPClient: HTTPClient { struct Response { - let response: HTTPResponse.Result + let response: VerifiedHTTPResponse.Result let delay: DispatchTimeInterval - private init(response: HTTPResponse.Result, delay: DispatchTimeInterval) { + private init(response: VerifiedHTTPResponse.Result, delay: DispatchTimeInterval) { self.response = response self.delay = delay } @@ -33,10 +33,12 @@ class MockHTTPClient: HTTPClient { // swiftlint:disable:next force_try let data = try! JSONSerialization.data(withJSONObject: response) - let response = HTTPResponse( - statusCode: statusCode, - responseHeaders: responseHeaders, - body: data, + let response = VerifiedHTTPResponse( + response: .init( + statusCode: statusCode, + responseHeaders: responseHeaders, + body: data + ), verificationResult: verificationResult ) @@ -92,7 +94,7 @@ class MockHTTPClient: HTTPClient { let mock = self.mocks[request.path] ?? .init(statusCode: .success) if let completionHandler = completionHandler { - let response: HTTPResponse.Result = mock.response.parseResponse() + let response: VerifiedHTTPResponse.Result = mock.response.parseResponse() if mock.delay != .never { DispatchQueue.main.asyncAfter(deadline: .now() + mock.delay) { diff --git a/Tests/UnitTests/Networking/ETagManagerTests.swift b/Tests/UnitTests/Networking/ETagManagerTests.swift index b6679a6030..2c5a13d790 100644 --- a/Tests/UnitTests/Networking/ETagManagerTests.swift +++ b/Tests/UnitTests/Networking/ETagManagerTests.swift @@ -13,7 +13,7 @@ class ETagManagerTests: TestCase { try super.setUpWithError() self.mockUserDefaults = MockUserDefaults() - self.eTagManager = try self.create() + self.eTagManager = .init(userDefaults: self.mockUserDefaults) } override func tearDown() { @@ -25,7 +25,7 @@ class ETagManagerTests: TestCase { func testETagIsEmptyIfThereIsNoETagSavedForThatRequest() { let request = URLRequest(url: Self.testURL) - let header = self.eTagManager.eTagHeader(for: request, withSignatureVerification: false) + let header = self.eTagManager.eTagHeader(for: request) let eTag = header[ETagManager.eTagRequestHeaderName] expect(eTag) == "" @@ -37,7 +37,7 @@ class ETagManagerTests: TestCase { try self.mockStoredETagResponse(for: request1) - let header = self.eTagManager.eTagHeader(for: request2, withSignatureVerification: false) + let header = self.eTagManager.eTagHeader(for: request2) expect(header[ETagManager.eTagRequestHeaderName]) == "" } @@ -45,7 +45,7 @@ class ETagManagerTests: TestCase { let request = URLRequest(url: Self.testURL) try self.mockStoredETagResponse(for: request) - let header = self.eTagManager.eTagHeader(for: request, withSignatureVerification: false) + let header = self.eTagManager.eTagHeader(for: request) expect(header[ETagManager.eTagRequestHeaderName]) == Self.testETag } @@ -175,7 +175,7 @@ class ETagManagerTests: TestCase { expect(response).to(beNil()) } - func testResponseIsStoredIfResponseCodeIs200AndVerificationWasNotRequested() throws { + func testResponseIsStoredIfResponseCodeIs200() throws { let request = URLRequest(url: Self.testURL) let cacheKey = try request.cacheKey @@ -185,39 +185,7 @@ class ETagManagerTests: TestCase { url: Self.testURL, body: responseObject, eTag: Self.testETag, - statusCode: .success, - verificationResult: .notRequested - ), - request: request, - retried: false - ) - - expect(response).toNot(beNil()) - expect(self.mockUserDefaults.setObjectForKeyCallCount) == 1 - - expect(self.mockUserDefaults.mockValues[try request.cacheKey]).toNot(beNil()) - let setData = try XCTUnwrap(self.mockUserDefaults.mockValues[cacheKey] as? Data) - - expect(setData).toNot(beNil()) - expect(self.mockUserDefaults.setObjectForKeyCalledValue) == cacheKey - - let eTagResponse = try ETagManager.Response.with(setData) - expect(eTagResponse.eTag) == Self.testETag - expect(eTagResponse.data) == responseObject - } - - func testResponseIsStoredIfResponseCodeIs200AndVerificationSucceded() throws { - let request = URLRequest(url: Self.testURL) - let cacheKey = try request.cacheKey - - let responseObject = try JSONSerialization.data(withJSONObject: ["a": "response"]) - let response = self.eTagManager.httpResultFromCacheOrBackend( - with: self.responseForTest( - url: Self.testURL, - body: responseObject, - eTag: Self.testETag, - statusCode: .success, - verificationResult: .verified + statusCode: .success ), request: request, retried: false @@ -262,57 +230,6 @@ class ETagManagerTests: TestCase { expect(self.mockUserDefaults.mockValues[try request.cacheKey]).to(beNil()) } - func testResponseIsNotStoredIfVerificationFailedWithInformationalMode() throws { - self.eTagManager = try self.create(with: .informational) - - let request = URLRequest(url: Self.testURL) - - let responseObject = Data() - - let response = self.eTagManager.httpResultFromCacheOrBackend( - with: self.responseForTest( - url: Self.testURL, - body: responseObject, - eTag: Self.testETag, - statusCode: .success, - verificationResult: .failed - ), - request: request, - retried: false - ) - - expect(response).toNot(beNil()) - expect(self.mockUserDefaults.setObjectForKeyCallCount) == 0 - expect(self.mockUserDefaults.mockValues[Self.testURL.absoluteString]).to(beNil()) - } - - func testResponseIsNotStoredIfVerificationFailedWithEnforcedMode() throws { - try self.setEnforcedETagManager() - - let request = URLRequest(url: Self.testURL) - - let responseObject = Data() - - let response = self.eTagManager.httpResultFromCacheOrBackend( - with: self.responseForTest( - url: Self.testURL, - body: responseObject, - eTag: Self.testETag, - statusCode: .success, - verificationResult: .failed - ), - request: request, - retried: false - ) - - expect(response).toNot(beNil()) - expect(response?.verificationResult) == .failed - - expect(self.mockUserDefaults.setObjectForKeyCallCount) == 0 - - expect(self.mockUserDefaults.mockValues[try request.cacheKey]).to(beNil()) - } - func testClearCachesWorks() throws { let request = URLRequest(url: Self.testURL) @@ -362,170 +279,19 @@ class ETagManagerTests: TestCase { expect(response).toNot(beNil()) expect(response?.statusCode) == .notModified expect(response?.body) == actualResponse - expect(response?.verificationResult) == .notRequested - } - - func testETagHeaderIsNotFoundIfItsMissingResponseVerificationAndVerificationEnforced() throws { - try self.setEnforcedETagManager() - - let request = URLRequest(url: Self.testURL) - - let actualResponse = "response".asData - - self.mockUserDefaults.mockValues[try request.cacheKey] = """ - { - "e_tag": "\(Self.testETag)", - "status_code": 200, - "data": "\(actualResponse.asFetchToken)" - } - """.asData - - let response = self.eTagManager.eTagHeader(for: request, - withSignatureVerification: true) - expect(response[ETagManager.eTagResponseHeaderName]).to(beEmpty()) - } - - func testETagHeaderIsNotFoundIfItsMissingResponseVerificationAndVerificationInformational() throws { - self.eTagManager = try self.create(with: .informational) - - let request = URLRequest(url: Self.testURL) - - let actualResponse = "response".asData - - self.mockUserDefaults.mockValues[try request.cacheKey] = """ - { - "e_tag": "\(Self.testETag)", - "status_code": 200, - "data": "\(actualResponse.asFetchToken)" - } - """.asData - - let response = self.eTagManager.eTagHeader(for: request, - withSignatureVerification: true) - expect(response[ETagManager.eTagResponseHeaderName]).to(beEmpty()) - } - - func testETagHeaderIsFoundIfItsMissingResponseVerificationAndVerificationIsDisabled() throws { - let request = URLRequest(url: Self.testURL) - - let actualResponse = "response".asData - - self.mockUserDefaults.mockValues[try request.cacheKey] = """ - { - "e_tag": "\(Self.testETag)", - "status_code": 200, - "data": "\(actualResponse.asFetchToken)" - } - """.asData - - let response = self.eTagManager.eTagHeader(for: request, - withSignatureVerification: false) - expect(response[ETagManager.eTagRequestHeaderName]) == Self.testETag - } - - func testETagHeaderIsIgnoredIfVerificationFailedAndModeEnforced() throws { - try self.setEnforcedETagManager() - - let request = URLRequest(url: Self.testURL) - - let actualResponse = "response".asData - - self.mockUserDefaults.mockValues[try request.cacheKey] = ETagManager.Response( - eTag: Self.testETag, - statusCode: .success, - data: actualResponse, - verificationResult: .failed - ).asData() - - let response = self.eTagManager.eTagHeader(for: request, withSignatureVerification: true) - expect(response[ETagManager.eTagResponseHeaderName]).to(beEmpty()) - } - - func testETagHeaderIsIgnoredIfVerificationFailedAndModeInformational() throws { - self.eTagManager = try self.create(with: .informational) - - let request = URLRequest(url: Self.testURL) - - let actualResponse = "response".asData - - self.mockUserDefaults.mockValues[try request.cacheKey] = ETagManager.Response( - eTag: Self.testETag, - statusCode: .success, - data: actualResponse, - verificationResult: .failed - ).asData() - - let response = self.eTagManager.eTagHeader(for: request, withSignatureVerification: true) - expect(response[ETagManager.eTagResponseHeaderName]).to(beEmpty()) } - func testETagHeaderIsIgnoredIfVerificationWasNotEnabledAndModeInformational() throws { - self.eTagManager = try self.create(with: .informational) - + func testETagHeaderIsReturned() throws { let request = URLRequest(url: Self.testURL) - let actualResponse = "response".asData self.mockUserDefaults.mockValues[try request.cacheKey] = ETagManager.Response( eTag: Self.testETag, statusCode: .success, - data: actualResponse, - verificationResult: .notRequested + data: actualResponse ).asData() - let response = self.eTagManager.eTagHeader(for: request, withSignatureVerification: true) - expect(response[ETagManager.eTagResponseHeaderName]).to(beEmpty()) - } - - func testETagHeaderIsIgnoredIfVerificationWasNotEnabledAndModeEnforced() throws { - try self.setEnforcedETagManager() - - let request = URLRequest(url: Self.testURL) - - let actualResponse = "response".asData - - self.mockUserDefaults.mockValues[try request.cacheKey] = ETagManager.Response( - eTag: Self.testETag, - statusCode: .success, - data: actualResponse, - verificationResult: .notRequested - ).asData() - - let response = self.eTagManager.eTagHeader(for: request, withSignatureVerification: true) - expect(response[ETagManager.eTagResponseHeaderName]).to(beEmpty()) - } - - func testETagHeaderIsReturnedIfVerificationSucceded() throws { - let request = URLRequest(url: Self.testURL) - - let actualResponse = "response".asData - - self.mockUserDefaults.mockValues[try request.cacheKey] = ETagManager.Response( - eTag: Self.testETag, - statusCode: .success, - data: actualResponse, - verificationResult: .verified - ).asData() - - let response = self.eTagManager.eTagHeader(for: request, - withSignatureVerification: false) - expect(response[ETagManager.eTagResponseHeaderName]) == Self.testETag - } - - func testETagHeaderIsReturnedIfVerificationWasNotRequested() throws { - let request = URLRequest(url: Self.testURL) - - let actualResponse = "response".asData - - self.mockUserDefaults.mockValues[try request.cacheKey] = ETagManager.Response( - eTag: Self.testETag, - statusCode: .success, - data: actualResponse, - verificationResult: .notRequested - ).asData() - - let response = self.eTagManager.eTagHeader(for: request, - withSignatureVerification: false) + let response = self.eTagManager.eTagHeader(for: request) expect(response[ETagManager.eTagResponseHeaderName]) == Self.testETag } @@ -539,11 +305,10 @@ class ETagManagerTests: TestCase { eTag: Self.testETag, statusCode: .success, data: actualResponse, - validationTime: validationTime, - verificationResult: .notRequested + validationTime: validationTime ).asData() - let response = self.eTagManager.eTagHeader(for: request, withSignatureVerification: false) + let response = self.eTagManager.eTagHeader(for: request) expect(response) == [ ETagManager.eTagRequestHeaderName: Self.testETag, ETagManager.eTagValidationTimeRequestHeaderName: validationTime.millisecondsSince1970.description @@ -559,11 +324,10 @@ class ETagManagerTests: TestCase { eTag: Self.testETag, statusCode: .success, data: actualResponse, - validationTime: nil, - verificationResult: .notRequested + validationTime: nil ).asData() - let response = self.eTagManager.eTagHeader(for: request, withSignatureVerification: false) + let response = self.eTagManager.eTagHeader(for: request) expect(response) == [ ETagManager.eTagRequestHeaderName: Self.testETag ] @@ -595,31 +359,6 @@ class ETagManagerTests: TestCase { expect(response?.requestDate) == requestDate } - func testCachedResponseWithNoVerificationResultIsNotIgnored() throws { - let request = URLRequest(url: Self.testURL) - - let actualResponse = "response".asData - - self.mockUserDefaults.mockValues[try request.cacheKey] = """ - { - "e_tag": "\(Self.testETag)", - "status_code": 200, - "data": "\(actualResponse.asFetchToken)" - } - """.asData - - let response = self.eTagManager.httpResultFromCacheOrBackend( - with: self.responseForTest(url: Self.testURL, - body: nil, - eTag: Self.testETag, - statusCode: .notModified), - request: request, - retried: false - ) - expect(response?.verificationResult) == .notRequested - expect(response?.body) == actualResponse - } - func testCachedResponseWithNoValidationTimeIsNotIgnored() throws { let request = URLRequest(url: Self.testURL) @@ -645,33 +384,7 @@ class ETagManagerTests: TestCase { expect(response?.body) == actualResponse } - func testCachedResponseIsFoundIfVerificationWasNotRequested() throws { - let request = URLRequest(url: Self.testURL) - - let actualResponse = "response".asData - - self.mockUserDefaults.mockValues[try request.cacheKey] = ETagManager.Response( - eTag: Self.testETag, - statusCode: .success, - data: actualResponse, - verificationResult: .notRequested - ).asData() - - let response = self.eTagManager.httpResultFromCacheOrBackend( - with: self.responseForTest(url: Self.testURL, - body: nil, - eTag: Self.testETag, - statusCode: .notModified), - request: request, - retried: true - ) - expect(response).toNot(beNil()) - expect(response?.statusCode) == .success - expect(response?.body) == actualResponse - expect(response?.verificationResult) == .notRequested - } - - func testCachedResponseIsFoundIfVerificationSucceeded() throws { + func testCachedResponseIsFound() throws { let request = URLRequest(url: Self.testURL) let actualResponse = "response".asData @@ -680,8 +393,7 @@ class ETagManagerTests: TestCase { { "e_tag": "\(Self.testETag)", "status_code": 200, - "data": "\(actualResponse.asFetchToken)", - "verification_result": \(VerificationResult.verified.rawValue) + "data": "\(actualResponse.asFetchToken)" } """.asData @@ -696,70 +408,12 @@ class ETagManagerTests: TestCase { expect(response).toNot(beNil()) expect(response?.statusCode) == .success expect(response?.body) == actualResponse - expect(response?.verificationResult) == .verified - } - - func testCachedResponseIsReturnedEvenIfVerificationFailed() throws { - // Technically, as tested by `testResponseIsNotStoredIfVerificationFailed` - // a response can't be stored if verification failed, but useful to test just in case. - - let request = URLRequest(url: Self.testURL) - - let actualResponse = "response".asData - - self.mockUserDefaults.mockValues[try request.cacheKey] = ETagManager.Response( - eTag: Self.testETag, - statusCode: .success, - data: actualResponse, - verificationResult: .failed - ).asData() - - let response = self.eTagManager.httpResultFromCacheOrBackend( - with: self.responseForTest(url: Self.testURL, - body: nil, - eTag: Self.testETag, - statusCode: .notModified), - request: request, - retried: true - ) - expect(response).toNot(beNil()) - expect(response?.statusCode) == .success - expect(response?.body) == actualResponse - expect(response?.verificationResult) == .failed } } private extension ETagManagerTests { - /// - Throws: `XCTSkip` prior to iOS 13 - func create(with verificationMode: Configuration.EntitlementVerificationMode = .disabled) throws -> ETagManager { - let mode: Signing.ResponseVerificationMode - - if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) { - mode = Signing.verificationMode(with: verificationMode) - } else if verificationMode == .disabled { - mode = .disabled - } else { - throw XCTSkip("Response verification not available") - } - - return self.create(mode) - } - - /// - Throws: `XCTSkip` prior to iOS 13 - func setEnforcedETagManager() throws { - if #available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) { - self.eTagManager = self.create(Signing.enforcedVerificationMode()) - } else { - throw XCTSkip("iOS 13 required for this test") - } - } - - private func create(_ mode: Signing.ResponseVerificationMode) -> ETagManager { - return .init(userDefaults: self.mockUserDefaults, verificationMode: mode) - } - func getHeaders(eTag: String?) -> [String: String] { return [ "Content-Type": "application/json", @@ -779,16 +433,14 @@ private extension ETagManagerTests { @discardableResult func mockStoredETagResponse(for request: URLRequest, statusCode: HTTPStatusCode = .success, - validationTime: Date? = nil, - verificationResult: RevenueCat.VerificationResult = .defaultValue) throws -> Data { + validationTime: Date? = nil) throws -> Data { let data = try JSONSerialization.data(withJSONObject: ["arg": "value"]) let etagAndResponse = ETagManager.Response( eTag: Self.testETag, statusCode: statusCode, data: data, - validationTime: validationTime, - verificationResult: verificationResult + validationTime: validationTime ) self.mockUserDefaults.mockValues[try request.cacheKey] = try XCTUnwrap(etagAndResponse.asData()) @@ -805,14 +457,12 @@ private extension ETagManagerTests { body: Data?, eTag: String?, statusCode: HTTPStatusCode, - requestDate: Date? = nil, - verificationResult: RevenueCat.VerificationResult = .defaultValue + requestDate: Date? = nil ) -> HTTPResponse { return .init(statusCode: statusCode, responseHeaders: self.getHeaders(eTag: eTag), body: body, - requestDate: requestDate, - verificationResult: verificationResult) + requestDate: requestDate) } private static let testURL = HTTPRequest.Path.getCustomerInfo(appUserID: "appUserID").url! diff --git a/Tests/UnitTests/Networking/HTTPClientTests.swift b/Tests/UnitTests/Networking/HTTPClientTests.swift index b7dae47b93..5f649878e9 100644 --- a/Tests/UnitTests/Networking/HTTPClientTests.swift +++ b/Tests/UnitTests/Networking/HTTPClientTests.swift @@ -15,7 +15,9 @@ import XCTest class BaseHTTPClientTests: TestCase { - typealias EmptyResponse = HTTPResponse.Result + typealias EmptyResponse = VerifiedHTTPResponse.Result + typealias DataResponse = VerifiedHTTPResponse.Result + typealias BodyWithDateResponse = VerifiedHTTPResponse.Result var systemInfo: MockSystemInfo! var client: HTTPClient! @@ -329,7 +331,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } let result = waitUntilValue { completion in - self.client.perform(request) { (response: HTTPResponse.Result) in + self.client.perform(request) { (response: DataResponse) in completion(response) } } @@ -365,7 +367,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } let result = waitUntilValue { completion in - self.client.perform(request) { (response: HTTPResponse.Result) in + self.client.perform(request) { (response: DataResponse) in completion(response) } } @@ -401,7 +403,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } let result = waitUntilValue { completion in - self.client.perform(request) { (response: HTTPResponse.Result) in + self.client.perform(request) { (response: DataResponse) in completion(response) } } @@ -438,7 +440,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } let result = waitUntilValue { completion in - self.client.perform(request) { (response: HTTPResponse.Result) in + self.client.perform(request) { (response: DataResponse) in completion(response) } } @@ -466,7 +468,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } let result = waitUntilValue { completion in - self.client.perform(request) { (response: HTTPResponse.Result) in + self.client.perform(request) { (response: VerifiedHTTPResponse.Result) in completion(response) } } @@ -496,7 +498,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } let result = waitUntilValue { completion in - self.client.perform(request) { (response: HTTPResponse.Result) in + self.client.perform(request) { (response: DataResponse) in completion(response) } } @@ -525,7 +527,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } let result = waitUntilValue { completion in - self.client.perform(request) { (response: HTTPResponse.Result) in + self.client.perform(request) { (response: DataResponse) in completion(response) } } @@ -555,7 +557,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } let result = waitUntilValue { completion in - self.client.perform(request) { (response: HTTPResponse.Result) in + self.client.perform(request) { (response: VerifiedHTTPResponse.Result) in completion(response) } } @@ -580,7 +582,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } waitUntil { completion in - self.client.perform(request) { (_: HTTPResponse.Result) in completion() } + self.client.perform(request) { (_: DataResponse) in completion() } } expect(headerPresent.value) == true @@ -597,7 +599,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } waitUntil { completion in - self.client.perform(request) { (_: HTTPResponse.Result) in completion() } + self.client.perform(request) { (_: DataResponse) in completion() } } expect(headerPresent.value) == false @@ -617,7 +619,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } waitUntil { completion in - self.client.perform(request) { (_: HTTPResponse.Result) in completion() } + self.client.perform(request) { (_: DataResponse) in completion() } } expect(headerPresent.value) == true @@ -636,7 +638,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } waitUntil { completion in - self.client.perform(request) { (_: HTTPResponse.Result) in completion() } + self.client.perform(request) { (_: DataResponse) in completion() } } expect(headerPresent.value) == true @@ -655,7 +657,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } waitUntil { completion in - self.client.perform(request) { (_: HTTPResponse.Result) in completion() } + self.client.perform(request) { (_: DataResponse) in completion() } } expect(headerPresent.value) == true @@ -675,7 +677,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } waitUntil { completion in - self.client.perform(request) { (_: HTTPResponse.Result) in completion() } + self.client.perform(request) { (_: DataResponse) in completion() } } expect(headerPresent.value) == true @@ -696,7 +698,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } waitUntil { completion in - self.client.perform(request) { (_: HTTPResponse.Result) in completion() } + self.client.perform(request) { (_: DataResponse) in completion() } } expect(headerPresent.value) == true @@ -723,7 +725,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } waitUntil { completion in - self.client.perform(request) { (_: HTTPResponse.Result) in completion() } + self.client.perform(request) { (_: DataResponse) in completion() } } expect(headerPresent.value) == true @@ -741,7 +743,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } waitUntil { completion in - self.client.perform(request) { (_: HTTPResponse.Result) in completion() } + self.client.perform(request) { (_: DataResponse) in completion() } } expect(headerPresent.value) == true @@ -763,7 +765,7 @@ final class HTTPClientTests: BaseHTTPClientTests { self.client = HTTPClient(apiKey: self.apiKey, systemInfo: systemInfo, eTagManager: self.eTagManager) waitUntil { completion in - self.client.perform(request) { (_: HTTPResponse.Result) in completion() } + self.client.perform(request) { (_: DataResponse) in completion() } } expect(headerPresent.value) == true @@ -784,7 +786,7 @@ final class HTTPClientTests: BaseHTTPClientTests { self.client = HTTPClient(apiKey: self.apiKey, systemInfo: systemInfo, eTagManager: self.eTagManager) waitUntil { completion in - self.client.perform(request) { (_: HTTPResponse.Result) in completion() } + self.client.perform(request) { (_: DataResponse) in completion() } } expect(headerPresent.value) == true @@ -803,7 +805,7 @@ final class HTTPClientTests: BaseHTTPClientTests { self.client = HTTPClient(apiKey: self.apiKey, systemInfo: systemInfo, eTagManager: self.eTagManager) waitUntil { completion in - self.client.perform(request) { (_: HTTPResponse.Result) in completion() } + self.client.perform(request) { (_: DataResponse) in completion() } } expect(headerPresent.value) == true @@ -825,7 +827,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } waitUntil { completion in - self.client.perform(request) { (_: HTTPResponse.Result) in completion() } + self.client.perform(request) { (_: DataResponse) in completion() } } expect(headerPresent) == true @@ -846,7 +848,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } waitUntil { completion in - self.client.perform(request) { (_: HTTPResponse.Result) in completion() } + self.client.perform(request) { (_: DataResponse) in completion() } } expect(headerPresent) == false @@ -865,7 +867,7 @@ final class HTTPClientTests: BaseHTTPClientTests { self.client = HTTPClient(apiKey: self.apiKey, systemInfo: systemInfo, eTagManager: self.eTagManager) waitUntil { completion in - self.client.perform(request) { (_: HTTPResponse.Result) in completion() } + self.client.perform(request) { (_: DataResponse) in completion() } } expect(headerPresent.value) == true @@ -891,7 +893,7 @@ final class HTTPClientTests: BaseHTTPClientTests { for requestNumber in 0...Result) in + client.perform(.init(method: .requestNumber(requestNumber), path: path)) { (_: DataResponse) in completionCallCount.value += 1 expectation.fulfill() } @@ -926,12 +928,12 @@ final class HTTPClientTests: BaseHTTPClientTests { self.expectation(description: "Request 2") ] - self.client.perform(.init(method: .requestNumber(1), path: path)) { (_: HTTPResponse.Result) in + self.client.perform(.init(method: .requestNumber(1), path: path)) { (_: DataResponse) in firstRequestFinished.value = true expectations[0].fulfill() } - self.client.perform(.init(method: .requestNumber(2), path: path)) { (_: HTTPResponse.Result) in + self.client.perform(.init(method: .requestNumber(2), path: path)) { (_: DataResponse) in secondRequestFinished.value = true expectations[1].fulfill() } @@ -978,17 +980,17 @@ final class HTTPClientTests: BaseHTTPClientTests { self.expectation(description: "Request 3") ] - self.client.perform(.init(method: .requestNumber(1), path: path)) { (_: HTTPResponse.Result) in + self.client.perform(.init(method: .requestNumber(1), path: path)) { (_: DataResponse) in firstRequestFinished.value = true expectations[0].fulfill() } - self.client.perform(.init(method: .requestNumber(2), path: path)) { (_: HTTPResponse.Result) in + self.client.perform(.init(method: .requestNumber(2), path: path)) { (_: DataResponse) in secondRequestFinished.value = true expectations[1].fulfill() } - self.client.perform(.init(method: .requestNumber(3), path: path)) { (_: HTTPResponse.Result) in + self.client.perform(.init(method: .requestNumber(3), path: path)) { (_: DataResponse) in thirdRequestFinished.value = true expectations[2].fulfill() } @@ -1002,7 +1004,7 @@ final class HTTPClientTests: BaseHTTPClientTests { func testPerformRequestExitsWithErrorIfBodyCouldntBeParsedIntoJSON() throws { let response = waitUntilValue { completion in - self.client.perform(.init(method: .invalidBody(), path: .mockPath)) { (result: HTTPResponse.Result) in + self.client.perform(.init(method: .invalidBody(), path: .mockPath)) { (result: DataResponse) in completion(result) } } @@ -1022,7 +1024,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } waitUntil { completion in - self.client.perform(.init(method: .invalidBody(), path: path)) { (_: HTTPResponse.Result) in + self.client.perform(.init(method: .invalidBody(), path: path)) { (_: DataResponse) in completion() } } @@ -1048,7 +1050,7 @@ final class HTTPClientTests: BaseHTTPClientTests { self.eTagManager.shouldReturnResultFromBackend = false self.eTagManager.stubbedHTTPResultFromCacheOrBackendResult = nil - let result: HTTPResponse.Result? = waitUntilValue { completion in + let result: DataResponse? = waitUntilValue { completion in self.client.perform(.init(method: .get, path: path)) { completion($0) } @@ -1069,7 +1071,8 @@ final class HTTPClientTests: BaseHTTPClientTests { let headers: [String: String] = [ HTTPClient.ResponseHeader.contentType.rawValue: "application/json", - HTTPClient.ResponseHeader.signature.rawValue: UUID().uuidString + HTTPClient.ResponseHeader.signature.rawValue: UUID().uuidString, + HTTPClient.ResponseHeader.requestDate.rawValue: String(requestDate.millisecondsSince1970) ] self.eTagManager.stubResponseEtag(eTag) @@ -1077,9 +1080,7 @@ final class HTTPClientTests: BaseHTTPClientTests { self.eTagManager.stubbedHTTPResultFromCacheOrBackendResult = .init( statusCode: .success, responseHeaders: headers, - body: mockedCachedResponse, - requestDate: requestDate, - verificationResult: .notRequested + body: mockedCachedResponse ) stub(condition: isPath(path)) { response in @@ -1090,8 +1091,8 @@ final class HTTPClientTests: BaseHTTPClientTests { headers: headers) } - let response: HTTPResponse.Result? = waitUntilValue { completion in - self.client.perform(.init(method: .get, path: path)) { (result: HTTPResponse.Result) in + let response: DataResponse? = waitUntilValue { completion in + self.client.perform(.init(method: .get, path: path)) { (result: DataResponse) in completion(result) } } @@ -1099,12 +1100,11 @@ final class HTTPClientTests: BaseHTTPClientTests { expect(response).toNot(beNil()) expect(response?.value?.statusCode) == .success expect(response?.value?.body) == mockedCachedResponse - expect(response?.value?.requestDate) == requestDate + expect(response?.value?.requestDate).to(beCloseToDate(requestDate)) expect(response?.value?.verificationResult) == .notRequested - expect(response?.value?.responseHeaders).to(haveCount(headers.count)) + expect(response?.value?.responseHeaders.keys).to(contain(Array(headers.keys.map(AnyHashable.init)))) expect(self.eTagManager.invokedETagHeaderParametersList).to(haveCount(1)) - expect(self.eTagManager.invokedETagHeaderParameters?.withSignatureVerification) == false } func testDNSCheckerIsCalledWhenGETRequestFailedWithUnknownError() { @@ -1120,7 +1120,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } waitUntil { completion in - self.client.perform(.init(method: .get, path: path)) { (_: HTTPResponse.Result) in + self.client.perform(.init(method: .get, path: path)) { (_: DataResponse) in completion() } } @@ -1142,7 +1142,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } waitUntil { completion in - self.client.perform(.init(method: .post([:]), path: path)) { (_: HTTPResponse.Result) in + self.client.perform(.init(method: .post([:]), path: path)) { (_: DataResponse) in completion() } } @@ -1168,7 +1168,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } waitUntil { completion in - self.client.perform(.init(method: .post([:]), path: path)) { (_: HTTPResponse.Result) in + self.client.perform(.init(method: .post([:]), path: path)) { (_: DataResponse) in completion() } } @@ -1193,7 +1193,7 @@ final class HTTPClientTests: BaseHTTPClientTests { return response } waitUntil { completion in - self.client.perform(.init(method: .get, path: path)) { (_: HTTPResponse.Result) in + self.client.perform(.init(method: .get, path: path)) { (_: DataResponse) in completion() } } @@ -1214,7 +1214,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } let obtainedError: NetworkError? = waitUntilValue { completion in - self.client.perform(.init(method: .get, path: path)) { (result: HTTPResponse.Result) in + self.client.perform(.init(method: .get, path: path)) { (result: DataResponse) in completion(result.error) } } @@ -1244,7 +1244,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } let obtainedError: NetworkError? = waitUntilValue { completion in - self.client.perform(.init(method: .get, path: path)) { (result: HTTPResponse.Result) in + self.client.perform(.init(method: .get, path: path)) { (result: DataResponse) in completion(result.error) } } @@ -1275,7 +1275,7 @@ final class HTTPClientTests: BaseHTTPClientTests { } waitUntil { completion in - self.client.perform(.init(method: .get, path: path)) { (_: HTTPResponse.Result) in + self.client.perform(.init(method: .get, path: path)) { (_: DataResponse) in completion() } } @@ -1302,7 +1302,7 @@ final class HTTPClientTests: BaseHTTPClientTests { ) } - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: BodyWithDateResponse? = waitUntilValue { completion in self.client.perform(.init(method: .get, path: path), completionHandler: completion) } @@ -1323,8 +1323,7 @@ final class HTTPClientTests: BaseHTTPClientTests { statusCode: .success, responseHeaders: [:], body: encodedResponse, - requestDate: requestDate, - verificationResult: .notRequested + requestDate: requestDate ) stub(condition: isPath(path)) { _ in @@ -1337,7 +1336,7 @@ final class HTTPClientTests: BaseHTTPClientTests { ) } - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: BodyWithDateResponse? = waitUntilValue { completion in self.client.perform(.init(method: .get, path: path), completionHandler: completion) } @@ -1366,7 +1365,7 @@ final class HTTPClientTests: BaseHTTPClientTests { ) self.client = self.createClient() - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: BodyWithDateResponse? = waitUntilValue { completion in self.client.perform(.init(method: .get, path: path), completionHandler: completion) } @@ -1406,7 +1405,7 @@ final class HTTPClientTests: BaseHTTPClientTests { let logger = TestLogHandler() - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: DataResponse? = waitUntilValue { completion in self.client.perform(.init(method: .get, path: pathA), completionHandler: completion) } @@ -1432,7 +1431,7 @@ final class HTTPClientTests: BaseHTTPClientTests { let logger = TestLogHandler() - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: DataResponse? = waitUntilValue { completion in self.client.perform(.init(method: .get, path: path), completionHandler: completion) } expect(response).to(beSuccess()) @@ -1455,7 +1454,7 @@ final class HTTPClientTests: BaseHTTPClientTests { let logger = TestLogHandler() - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: DataResponse? = waitUntilValue { completion in self.client.perform(.init(method: .get, path: path), completionHandler: completion) } expect(response).to(beSuccess()) diff --git a/Tests/UnitTests/Networking/HTTPResponseTests.swift b/Tests/UnitTests/Networking/HTTPResponseTests.swift index f161e02bcc..78ed51ec1f 100644 --- a/Tests/UnitTests/Networking/HTTPResponseTests.swift +++ b/Tests/UnitTests/Networking/HTTPResponseTests.swift @@ -21,13 +21,14 @@ class HTTPResponseTests: TestCase { func testResponseVerificationNotRequestedWithNoPublicKey() { let request = HTTPRequest(method: .get, path: .health) - let response = HTTPResponse.create(with: Data(), - statusCode: .success, - headers: [:], - request: request, - publicKey: nil) + let response = HTTPResponse( + statusCode: .success, + responseHeaders: [:], + body: Data() + ) + let verifiedResponse = response.verify(request: request, publicKey: nil) - expect(response.verificationResult) == .notRequested + expect(verifiedResponse.verificationResult) == .notRequested } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) @@ -37,13 +38,14 @@ class HTTPResponseTests: TestCase { let key = Curve25519.Signing.PrivateKey().publicKey let request = HTTPRequest(method: .get, path: .health) - let response = HTTPResponse.create(with: Data(), - statusCode: .success, - headers: [:], - request: request, - publicKey: key) + let response = HTTPResponse( + statusCode: .success, + responseHeaders: [:], + body: Data() + ) + let verifiedResponse = response.verify(request: request, publicKey: key) - expect(response.verificationResult) == .notRequested + expect(verifiedResponse.verificationResult) == .notRequested } func testValueForHeaderFieldWithNonExistingField() { @@ -77,40 +79,8 @@ class HTTPResponseTests: TestCase { expect(response.requestDate).to(beNil()) } - func testCopyWithSameVerificationResult() throws { - self.verifyCopy(of: try Self.sampleResponse.copy(with: .verified), - onlyModifiesEntitlementVerification: .verified) - } - - func testCopyWithVerificationResultVerified() throws { - self.verifyCopy(of: try Self.sampleResponse, - onlyModifiesEntitlementVerification: .verified) - } - - func testCopyWithVerificationResultFailedVerified() throws { - self.verifyCopy(of: try Self.sampleResponse, - onlyModifiesEntitlementVerification: .failed) - } - - func testCopyWithVerificationResultNotRequested() throws { - self.verifyCopy(of: try Self.sampleResponse.copy(with: .verified), - onlyModifiesEntitlementVerification: .notRequested) - } - // MARK: - - private func verifyCopy( - of response: HTTPResponse, - onlyModifiesEntitlementVerification newVerification: VerificationResult - ) { - let copy = response.copy(with: newVerification) - expect(copy.verificationResult) == newVerification - expect(copy.statusCode) == response.statusCode - expect(copy.responseHeaders).to(haveCount(response.responseHeaders.count)) - expect(copy.body) == response.body - expect(copy.requestDate) == response.requestDate - } - private static var sampleResponse: HTTPResponse { get throws { return .create( @@ -121,6 +91,11 @@ class HTTPResponseTests: TestCase { ) } } + private static var sampleVerifiedResponse: VerifiedHTTPResponse { + get throws { + return .init(response: try Self.sampleResponse, verificationResult: .notRequested) + } + } } @@ -129,8 +104,7 @@ private extension HTTPResponse where Body == HTTPEmptyResponseBody { static func create(_ headers: HTTPResponse.Headers) -> Self { return .init(statusCode: .success, responseHeaders: headers, - body: .init(), - verificationResult: .notRequested) + body: .init()) } } @@ -141,8 +115,7 @@ private extension HTTPResponse where Body == Data { return .init(statusCode: .success, responseHeaders: headers, body: body, - requestDate: Date(), - verificationResult: .notRequested) + requestDate: Date()) } } diff --git a/Tests/UnitTests/Networking/SignatureVerificationHTTPClientTests.swift b/Tests/UnitTests/Networking/SignatureVerificationHTTPClientTests.swift index d59e8c7481..7be9ced60f 100644 --- a/Tests/UnitTests/Networking/SignatureVerificationHTTPClientTests.swift +++ b/Tests/UnitTests/Networking/SignatureVerificationHTTPClientTests.swift @@ -85,7 +85,7 @@ final class SignatureVerificationHTTPClientTests: BaseSignatureVerificationHTTPC MockSigning.stubbedVerificationResult = true let request: HTTPRequest = .createWithResponseVerification(method: .get, path: Self.path) - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: DataResponse? = waitUntilValue { completion in self.client.perform(request, completionHandler: completion) } @@ -115,7 +115,7 @@ final class SignatureVerificationHTTPClientTests: BaseSignatureVerificationHTTPC let request: HTTPRequest = .createWithResponseVerification(method: .get, path: Self.path) - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: DataResponse? = waitUntilValue { completion in self.client.perform(request, completionHandler: completion) } @@ -127,7 +127,7 @@ final class SignatureVerificationHTTPClientTests: BaseSignatureVerificationHTTPC try self.changeClient(.disabled) self.mockResponse() - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: DataResponse? = waitUntilValue { completion in self.client.perform(.init(method: .get, path: Self.path), completionHandler: completion) } @@ -137,19 +137,18 @@ final class SignatureVerificationHTTPClientTests: BaseSignatureVerificationHTTPC expect(MockSigning.requests).to(beEmpty()) } - func testVerifiedCachedResponseWithNotRequestedVerificationResponse() throws { + func testCachedResponseWithNotRequestedVerificationResponse() throws { try self.changeClient(.disabled) let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1) try self.mockETagCache( response: cachedResponse, - requestDate: Self.date1, - verificationResult: .verified + requestDate: Self.date1 ) self.mockPath(statusCode: .notModified, requestDate: Self.date2) - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: BodyWithDateResponse? = waitUntilValue { completion in self.client.perform(.createWithResponseVerification(method: .get, path: Self.path), completionHandler: completion) } @@ -158,7 +157,7 @@ final class SignatureVerificationHTTPClientTests: BaseSignatureVerificationHTTPC expect(response).to(beSuccess()) expect(response?.value?.body.data) == cachedResponse.data - expect(response?.value?.body.requestDate).to(beCloseTo(Self.date1, within: 1)) + expect(response?.value?.body.requestDate).to(beCloseToDate(Self.date2)) expect(response?.value?.verificationResult) == .notRequested } @@ -183,7 +182,7 @@ final class InformationalSignatureVerificationHTTPClientTests: BaseSignatureVeri let request: HTTPRequest = .createWithResponseVerification(method: .get, path: Self.path) - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: DataResponse? = waitUntilValue { completion in self.client.perform(request, completionHandler: completion) } @@ -205,7 +204,7 @@ final class InformationalSignatureVerificationHTTPClientTests: BaseSignatureVeri MockSigning.stubbedVerificationResult = false - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: DataResponse? = waitUntilValue { completion in self.client.perform(.createWithResponseVerification(method: .get, path: .logIn), with: Signing.verificationMode(with: .informational), completionHandler: completion) @@ -216,8 +215,6 @@ final class InformationalSignatureVerificationHTTPClientTests: BaseSignatureVeri } func testValidSignatureWithETagResponse() throws { - XCTExpectFailure("Not yet implemented") - let body = "body".asData self.mockPath(statusCode: .notModified, requestDate: Self.date1) @@ -227,13 +224,12 @@ final class InformationalSignatureVerificationHTTPClientTests: BaseSignatureVeri self.eTagManager.stubbedHTTPResultFromCacheOrBackendResult = .init( statusCode: .success, responseHeaders: [:], - body: body, - verificationResult: .verified + body: body ) let request: HTTPRequest = .createWithResponseVerification(method: .get, path: Self.path) - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: DataResponse? = waitUntilValue { completion in self.client.perform(request, completionHandler: completion) } @@ -256,7 +252,7 @@ final class InformationalSignatureVerificationHTTPClientTests: BaseSignatureVeri MockSigning.stubbedVerificationResult = false - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: DataResponse? = waitUntilValue { completion in self.client.perform(.createWithResponseVerification(method: .get, path: Self.path), completionHandler: completion) } @@ -266,49 +262,15 @@ final class InformationalSignatureVerificationHTTPClientTests: BaseSignatureVeri expect(MockSigning.requests).to(haveCount(1)) } - func testIgnoresResponseFromETagManagerIfItHadNotBeenVerified() throws { - MockSigning.stubbedVerificationResult = true - - stub(condition: isPath(Self.path)) { request in - expect(request.allHTTPHeaderFields?.keys).toNot(contain(ETagManager.eTagResponseHeaderName)) - - return .init(data: Data(), - statusCode: .success, - headers: [ - HTTPClient.ResponseHeader.signature.rawValue: Self.sampleSignature, - HTTPClient.ResponseHeader.requestDate.rawValue: String(Self.date1.millisecondsSince1970) - ]) - } - - self.eTagManager.shouldReturnResultFromBackend = true - - let response: HTTPResponse.Result? = waitUntilValue { completion in - self.client.perform( - .createWithResponseVerification(method: .get, path: Self.path) - ) { (result: HTTPResponse.Result) in - completion(result) - } - } - - expect(self.eTagManager.invokedETagHeaderParametersList).to(haveCount(1)) - expect(self.eTagManager.invokedETagHeaderParameters?.withSignatureVerification) == true - expect(self.eTagManager.invokedETagHeaderParameters?.refreshETag) == false - - expect(response).toNot(beNil()) - expect(response?.value?.statusCode) == .success - expect(response?.value?.verificationResult) == .verified - } - func testCachedResponseDoesNotUpdateRequestDateIfNewResponseVerificationFails() throws { let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1) try self.mockETagCache(response: cachedResponse, - requestDate: Self.date1, - verificationResult: .notRequested) + requestDate: Self.date1) self.mockPath(statusCode: .notModified, requestDate: Self.date2) MockSigning.stubbedVerificationResult = false - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: BodyWithDateResponse? = waitUntilValue { completion in self.client.perform(.createWithResponseVerification(method: .get, path: Self.path), completionHandler: completion) } @@ -319,16 +281,15 @@ final class InformationalSignatureVerificationHTTPClientTests: BaseSignatureVeri expect(response?.value?.verificationResult) == .failed } - func testCachedResponseWithoutVerificationAndVerifiedResponse() throws { + func testCachedResponseWithVerifiedResponse() throws { let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1) try self.mockETagCache(response: cachedResponse, - requestDate: Self.date2, - verificationResult: .notRequested) + requestDate: Self.date2) self.mockPath(statusCode: .notModified, requestDate: Self.date2) MockSigning.stubbedVerificationResult = true - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: BodyWithDateResponse? = waitUntilValue { completion in self.client.perform(.createWithResponseVerification(method: .get, path: Self.path), completionHandler: completion) } @@ -343,13 +304,12 @@ final class InformationalSignatureVerificationHTTPClientTests: BaseSignatureVeri let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1) try self.mockETagCache(response: cachedResponse, - requestDate: Self.date2, - verificationResult: .verified) + requestDate: Self.date2) self.mockPath(statusCode: .notModified, requestDate: Self.date2) MockSigning.stubbedVerificationResult = false - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: BodyWithDateResponse? = waitUntilValue { completion in self.client.perform(.createWithResponseVerification(method: .get, path: Self.path), completionHandler: completion) } @@ -365,11 +325,10 @@ final class InformationalSignatureVerificationHTTPClientTests: BaseSignatureVeri MockSigning.stubbedVerificationResult = true try self.mockETagCache(response: cachedResponse, - requestDate: Self.date2, - verificationResult: .verified) + requestDate: Self.date2) self.mockPath(statusCode: .notModified, requestDate: Self.date2) - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: BodyWithDateResponse? = waitUntilValue { completion in self.client.perform(.createWithResponseVerification(method: .get, path: Self.path), completionHandler: completion) } @@ -379,57 +338,15 @@ final class InformationalSignatureVerificationHTTPClientTests: BaseSignatureVeri expect(response?.value?.verificationResult) == .verified } - func testCachedResponseWithFailedVerificationAndNotRequestedVerification() throws { - let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1) - - try self.mockETagCache(response: cachedResponse, - requestDate: Self.date2, - verificationResult: .failed) - self.mockPath(statusCode: .notModified, requestDate: Self.date2) - - let response: HTTPResponse.Result? = waitUntilValue { completion in - self.client.perform(.init(method: .get, path: .getOfferings(appUserID: "user")), - completionHandler: completion) - } - - expect(MockSigning.requests).to(beEmpty()) - expect(response).to(beSuccess()) - expect(response?.value?.body.requestDate).to(beCloseTo(Self.date2, within: 1)) - expect(response?.value?.verificationResult) == .notRequested - } - - func testCachedResponseWithFailedVerificationAndVerifiedResponse() throws { - // This won't happen in practice because the ETag won't be used if its verification failed. - - let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1) - - try self.mockETagCache(response: cachedResponse, - requestDate: Self.date1, - verificationResult: .failed) - self.mockPath(statusCode: .notModified, requestDate: Self.date2) - MockSigning.stubbedVerificationResult = true - - let response: HTTPResponse.Result? = waitUntilValue { completion in - self.client.perform(.init(method: .get, path: Self.path), - completionHandler: completion) - } - - expect(MockSigning.requests).to(haveCount(1)) - expect(response).to(beSuccess()) - expect(response?.value?.body.requestDate).to(beCloseTo(Self.date1, within: 1)) - expect(response?.value?.verificationResult) == .verified - } - - func testCachedResponseWithFailedVerificationAndFailedResponse() throws { + func testCachedResponseWithFailedResponseVerification() throws { let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1) try self.mockETagCache(response: cachedResponse, - requestDate: Self.date2, - verificationResult: .failed) + requestDate: Self.date2) self.mockPath(statusCode: .notModified, requestDate: Self.date2) MockSigning.stubbedVerificationResult = false - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: BodyWithDateResponse? = waitUntilValue { completion in self.client.perform(.init(method: .get, path: Self.path), completionHandler: completion) } @@ -440,35 +357,15 @@ final class InformationalSignatureVerificationHTTPClientTests: BaseSignatureVeri expect(response?.value?.verificationResult) == .failed } - func testIgnoredCachedResponseAndNotVerifiedResponse() throws { - let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1) - - try self.mockETagCache(response: cachedResponse, - requestDate: Self.date2, - verificationResult: .failed) - self.mockPath(statusCode: .success, requestDate: Self.date2) - - let response: HTTPResponse.Result? = waitUntilValue { completion in - self.client.perform(.init(method: .get, path: .getOfferings(appUserID: "user")), - completionHandler: completion) - } - - expect(MockSigning.requests).to(beEmpty()) - expect(response).to(beSuccess()) - expect(response?.value?.body.requestDate).to(beCloseTo(Self.date2, within: 1)) - expect(response?.value?.verificationResult) == .notRequested - } - func testIgnoredCachedResponseAndVerifiedResponse() throws { let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1) try self.mockETagCache(response: cachedResponse, - requestDate: Self.date2, - verificationResult: .failed) + requestDate: Self.date2) self.mockPath(statusCode: .success, requestDate: Self.date2) MockSigning.stubbedVerificationResult = true - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: BodyWithDateResponse? = waitUntilValue { completion in self.client.perform(.init(method: .get, path: Self.path), completionHandler: completion) } @@ -483,12 +380,11 @@ final class InformationalSignatureVerificationHTTPClientTests: BaseSignatureVeri let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1) try self.mockETagCache(response: cachedResponse, - requestDate: Self.date2, - verificationResult: .failed) + requestDate: Self.date2) self.mockPath(statusCode: .success, requestDate: Self.date2) MockSigning.stubbedVerificationResult = false - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: BodyWithDateResponse? = waitUntilValue { completion in self.client.perform(.init(method: .get, path: Self.path), completionHandler: completion) } @@ -498,6 +394,7 @@ final class InformationalSignatureVerificationHTTPClientTests: BaseSignatureVeri expect(response?.value?.body.requestDate).to(beCloseTo(Self.date1, within: 1)) expect(response?.value?.verificationResult) == .failed } + } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *) @@ -514,7 +411,7 @@ final class EnforcedSignatureVerificationHTTPClientTests: BaseSignatureVerificat MockSigning.stubbedVerificationResult = true - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: DataResponse? = waitUntilValue { completion in self.client.perform(.createWithResponseVerification(method: .get, path: Self.path), completionHandler: completion) } @@ -529,7 +426,7 @@ final class EnforcedSignatureVerificationHTTPClientTests: BaseSignatureVerificat MockSigning.stubbedVerificationResult = false - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: DataResponse? = waitUntilValue { completion in self.client.perform(.createWithResponseVerification(method: .get, path: Self.path), completionHandler: completion) } @@ -544,7 +441,7 @@ final class EnforcedSignatureVerificationHTTPClientTests: BaseSignatureVerificat MockSigning.stubbedVerificationResult = false - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: DataResponse? = waitUntilValue { completion in self.client.perform(.createWithResponseVerification(method: .get, path: .logIn), with: Signing.enforcedVerificationMode(), completionHandler: completion) @@ -560,7 +457,7 @@ final class EnforcedSignatureVerificationHTTPClientTests: BaseSignatureVerificat MockSigning.stubbedVerificationResult = false - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: DataResponse? = waitUntilValue { completion in self.client.perform(.createWithResponseVerification(method: .get, path: Self.path), with: .disabled, completionHandler: completion) @@ -575,7 +472,7 @@ final class EnforcedSignatureVerificationHTTPClientTests: BaseSignatureVerificat try self.changeClientToEnforced(forceSignatureFailures: true) - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: EmptyResponse? = waitUntilValue { completion in self.client.perform(.init(method: .get, path: Self.path), completionHandler: completion) } @@ -589,7 +486,7 @@ final class EnforcedSignatureVerificationHTTPClientTests: BaseSignatureVerificat try self.changeClient(.informational, forceSignatureFailures: true) - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: EmptyResponse? = waitUntilValue { completion in self.client.perform(.init(method: .get, path: Self.path), completionHandler: completion) } @@ -603,7 +500,7 @@ final class EnforcedSignatureVerificationHTTPClientTests: BaseSignatureVerificat try self.changeClient(.disabled, forceSignatureFailures: true) - let response: HTTPResponse.Result? = waitUntilValue { completion in + let response: EmptyResponse? = waitUntilValue { completion in self.client.perform(.init(method: .get, path: Self.path), completionHandler: completion) } @@ -659,25 +556,20 @@ private extension BaseSignatureVerificationHTTPClientTests { statusCode: HTTPStatusCode = .success ) { stub(condition: isPath(Self.path)) { _ in - if let signature = signature, let requestDate = requestDate { - let headers: [String: String?] = [ - HTTPClient.ResponseHeader.signature.rawValue: signature, - HTTPClient.ResponseHeader.eTag.rawValue: eTag, - HTTPClient.ResponseHeader.requestDate.rawValue: String(requestDate.millisecondsSince1970) - ] - - return .init(data: body, - statusCode: statusCode, - headers: headers.compactMapValues { $0 }) - } else { - return .emptySuccessResponse() - } + let headers: [String: String?] = [ + HTTPClient.ResponseHeader.signature.rawValue: signature, + HTTPClient.ResponseHeader.eTag.rawValue: eTag, + HTTPClient.ResponseHeader.requestDate.rawValue: requestDate.map { String($0.millisecondsSince1970) } + ] + + return .init(data: body, + statusCode: statusCode, + headers: headers.compactMapValues { $0 }) } } final func mockETagCache(response: BodyWithDate, - requestDate: Date, - verificationResult: VerificationResult) throws { + requestDate: Date) throws { self.eTagManager.stubResponseEtag(Self.eTag) self.eTagManager.shouldReturnResultFromBackend = false self.eTagManager.stubbedHTTPResultFromCacheOrBackendResult = .init( @@ -685,14 +577,17 @@ private extension BaseSignatureVerificationHTTPClientTests { responseHeaders: [ HTTPClient.ResponseHeader.requestDate.rawValue: String(requestDate.millisecondsSince1970) ], - body: try response.jsonEncodedData, - verificationResult: verificationResult + body: try response.jsonEncodedData ) } - func mockPath(statusCode: HTTPStatusCode, requestDate: Date) { + func mockPath( + statusCode: HTTPStatusCode, + requestDate: Date, + signature: String? = BaseSignatureVerificationHTTPClientTests.sampleSignature + ) { self.mockResponse( - signature: Self.sampleSignature, + signature: signature, requestDate: requestDate, eTag: Self.eTag, body: .init(), diff --git a/Tests/UnitTests/OfflineEntitlements/CustomerInfoResponseHandlerTests.swift b/Tests/UnitTests/OfflineEntitlements/CustomerInfoResponseHandlerTests.swift index 7129df03c9..2124758567 100644 --- a/Tests/UnitTests/OfflineEntitlements/CustomerInfoResponseHandlerTests.swift +++ b/Tests/UnitTests/OfflineEntitlements/CustomerInfoResponseHandlerTests.swift @@ -266,7 +266,7 @@ private extension BaseCustomerInfoResponseHandlerTests { } func handle( - _ response: HTTPResponse.Result, + _ response: VerifiedHTTPResponse.Result, _ mapping: ProductEntitlementMapping? ) async -> Result { let handler = self.create(mapping) diff --git a/Tests/UnitTests/Security/SigningTests.swift b/Tests/UnitTests/Security/SigningTests.swift index 6066f8a80f..e852ced910 100644 --- a/Tests/UnitTests/Security/SigningTests.swift +++ b/Tests/UnitTests/Security/SigningTests.swift @@ -277,26 +277,20 @@ class SigningTests: TestCase { func testResponseVerificationWithNoProvidedKey() throws { let request = HTTPRequest.createWithResponseVerification(method: .get, path: .health) - let response = HTTPResponse.create(with: Data(), - statusCode: .success, - headers: [:], - request: request, - publicKey: nil) + let response = HTTPResponse(statusCode: .success, responseHeaders: [:], body: Data()) + let verifiedResponse = response.verify(request: request, publicKey: nil) - expect(response.verificationResult) == .notRequested + expect(verifiedResponse.verificationResult) == .notRequested } func testResponseVerificationWithNoSignatureInResponse() throws { let request = HTTPRequest.createWithResponseVerification(method: .get, path: .health) let logger = TestLogHandler() - let response = HTTPResponse.create(with: Data(), - statusCode: .success, - headers: [:], - request: request, - publicKey: self.publicKey) + let response = HTTPResponse(statusCode: .success, responseHeaders: [:], body: Data()) + let verifiedResponse = response.verify(request: request, publicKey: self.publicKey) - expect(response.verificationResult) == .failed + expect(verifiedResponse.verificationResult) == .failed logger.verifyMessageWasLogged(Strings.signing.signature_was_requested_but_not_provided(request), level: .warn) @@ -304,17 +298,16 @@ class SigningTests: TestCase { func testResponseVerificationWithInvalidSignature() throws { let request = HTTPRequest.createWithResponseVerification(method: .get, path: .health) - let response = HTTPResponse.create( - with: Data(), + let response = HTTPResponse( statusCode: .success, - headers: [ + responseHeaders: [ HTTPClient.ResponseHeader.signature.rawValue: "invalid_signature" ], - request: request, - publicKey: self.publicKey + body: Data() ) + let verifiedResponse = response.verify(request: request, publicKey: self.publicKey) - expect(response.verificationResult) == .failed + expect(verifiedResponse.verificationResult) == .failed } func testResponseVerificationWithNonceWithValidSignature() throws { @@ -336,18 +329,17 @@ class SigningTests: TestCase { ) let request = HTTPRequest(method: .get, path: .health, nonce: nonce.asData) - let response = HTTPResponse.create( - with: message.asData, + let response = HTTPResponse( statusCode: .success, - headers: [ + responseHeaders: [ HTTPClient.ResponseHeader.signature.rawValue: fullSignature.base64EncodedString(), HTTPClient.ResponseHeader.requestDate.rawValue: String(requestDate) ], - request: request, - publicKey: self.publicKey + body: message.asData ) + let verifiedResponse = response.verify(request: request, publicKey: self.publicKey) - expect(response.verificationResult) == .verified + expect(verifiedResponse.verificationResult) == .verified } func testResponseVerificationWithoutNonceWithValidSignature() throws { @@ -368,18 +360,17 @@ class SigningTests: TestCase { ) let request = HTTPRequest(method: .get, path: .health, nonce: nil) - let response = HTTPResponse.create( - with: message.asData, + let response = HTTPResponse( statusCode: .success, - headers: [ + responseHeaders: [ HTTPClient.ResponseHeader.signature.rawValue: fullSignature.base64EncodedString(), HTTPClient.ResponseHeader.requestDate.rawValue: String(requestDate) ], - request: request, - publicKey: self.publicKey + body: message.asData ) + let verifiedResponse = response.verify(request: request, publicKey: self.publicKey) - expect(response.verificationResult) == .verified + expect(verifiedResponse.verificationResult) == .verified } func testResponseVerificationWithoutNonceAndNoSignatureReturnsNotRequested() throws { @@ -389,58 +380,21 @@ class SigningTests: TestCase { let logger = TestLogHandler() let request = HTTPRequest(method: .get, path: .health, nonce: nil) - let response = HTTPResponse.create( - with: message.asData, + let response = HTTPResponse( statusCode: .success, - headers: [ + responseHeaders: [ HTTPClient.ResponseHeader.requestDate.rawValue: String(requestDate) ], - request: request, - publicKey: self.publicKey + body: message.asData ) + let verifiedResponse = response.verify(request: request, publicKey: self.publicKey) - expect(response.verificationResult) == .notRequested + expect(verifiedResponse.verificationResult) == .notRequested logger.verifyMessageWasNotLogged(Strings.signing.signature_was_requested_but_not_provided(request), allowNoMessages: true) } - func testVerificationResultWithSameCachedAndResponseResult() { - expect(VerificationResult.from(cache: .notRequested, response: .notRequested)) == .notRequested - expect(VerificationResult.from(cache: .verified, response: .verified)) == .verified - expect(VerificationResult.from(cache: .verifiedOnDevice, response: .verifiedOnDevice)) == .verifiedOnDevice - expect(VerificationResult.from(cache: .failed, response: .failed)) == .failed - } - - func testVerificationNotRequestedCachedResult() { - expect(VerificationResult.from(cache: .notRequested, - response: .verified)) == .verified - expect(VerificationResult.from(cache: .notRequested, - response: .verifiedOnDevice)) == .verifiedOnDevice - expect(VerificationResult.from(cache: .notRequested, - response: .failed)) == .failed - } - - func testVerifiedCachedResult() { - expect(VerificationResult.from(cache: .verified, - response: .notRequested)) == .notRequested - expect(VerificationResult.from(cache: .verifiedOnDevice, - response: .notRequested)) == .notRequested - expect(VerificationResult.from(cache: .verified, - response: .failed)) == .failed - expect(VerificationResult.from(cache: .verifiedOnDevice, - response: .failed)) == .failed - } - - func testFailedVerificationCachedResult() { - expect(VerificationResult.from(cache: .failed, - response: .notRequested)) == .notRequested - expect(VerificationResult.from(cache: .failed, - response: .verified)) == .verified - expect(VerificationResult.from(cache: .failed, - response: .verifiedOnDevice)) == .verifiedOnDevice - } - } @available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)