Files
purchases-ios/Tests/UnitTests/Networking/SignatureVerificationHTTPClientTests.swift
T

948 lines
35 KiB
Swift

//
// Copyright RevenueCat Inc. All Rights Reserved.
//
// Licensed under the MIT License (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://opensource.org/licenses/MIT
//
// SignatureVerificationHTTPClientTests.swift
//
// Created by Nacho Soto on 3/13/23.
import Nimble
import OHHTTPStubs
import OHHTTPStubsSwift
import XCTest
@testable import RevenueCat
// swiftlint:disable type_name
class BaseSignatureVerificationHTTPClientTests: BaseHTTPClientTests<ETagManager> {
private var userDefaultsSuiteName: String!
fileprivate var userDefaults: UserDefaults!
override func setUpWithError() throws {
// Note: these tests use the real `ETagManager`
self.userDefaultsSuiteName = UUID().uuidString
self.userDefaults = .init(suiteName: self.userDefaultsSuiteName)!
self.eTagManager = ETagManager(userDefaults: self.userDefaults)
try super.setUpWithError()
}
override func tearDown() {
// Clean up to avoid leaving leftover data in the simulator
if let defaults = self.userDefaults, let suiteName = self.userDefaultsSuiteName {
defaults.removePersistentDomain(forName: suiteName)
}
super.tearDown()
}
fileprivate static let path: HTTPRequest.Path = .mockPath
fileprivate static let eTag = "etag"
fileprivate static let sampleSignature = "signature"
fileprivate static let date1 = Date().addingTimeInterval(-30000000)
fileprivate static let date2 = Date().addingTimeInterval(-1000000)
}
final class SignatureVerificationHTTPClientTests: BaseSignatureVerificationHTTPClientTests {
func testAutomaticallyAddsNonceIfRequired() {
self.changeClient(.informational)
let request = HTTPRequest(method: .get, path: .getCustomerInfo(appUserID: "user"))
let headers: [String: String]? = waitUntilValue { completion in
stub(condition: isPath(request.path)) { request in
completion(request.allHTTPHeaderFields)
return .emptySuccessResponse()
}
self.client.perform(request) { (_: EmptyResponse) in }
}
expect(headers).toNot(beEmpty())
expect(headers?.keys).to(contain(HTTPClient.RequestHeader.nonce.rawValue))
expect(headers?[HTTPClient.RequestHeader.nonce.rawValue]).toNot(beNil())
}
func testRequestIncludesRandomNonce() throws {
let request = HTTPRequest.createWithResponseVerification(method: .get, path: Self.path)
let headers: [String: String]? = waitUntilValue { completion in
stub(condition: isPath(request.path)) { request in
completion(request.allHTTPHeaderFields)
return .emptySuccessResponse()
}
self.client.perform(request) { (_: EmptyResponse) in }
}
expect(headers).toNot(beEmpty())
expect(headers?.keys).to(contain(HTTPClient.RequestHeader.nonce.rawValue))
expect(headers?[HTTPClient.RequestHeader.nonce.rawValue]) == request.nonce?.base64EncodedString()
}
func testFailedVerificationIfResponseContainsNoSignature() {
self.changeClient(.informational)
self.mockResponse()
self.signing.stubbedVerificationResult = true
let request: HTTPRequest = .createWithResponseVerification(method: .get, path: Self.path)
let response: DataResponse? = waitUntilValue { completion in
self.client.perform(request, completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.verificationResult) == .failed
expect(self.signing.requests).to(beEmpty())
self.logger.verifyMessageWasLogged(
Strings.signing.signature_was_requested_but_not_provided(request),
level: .warn
)
}
func testFailedVerificationIfResponseContainsNoSignatureForEndpointWithStaticSignature() {
// This test relies on a path with static signatures
let path: HTTPRequest.Path = .getProductEntitlementMapping
expect(path.supportsSignatureVerification) == true
expect(path.needsNonceForSigning) == false
self.changeClient(.informational)
self.mockResponse(path: path,
signature: nil,
requestDate: nil)
self.signing.stubbedVerificationResult = true
let request: HTTPRequest = .init(method: .get, path: path)
let response: DataResponse? = waitUntilValue { completion in
self.client.perform(request, completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.verificationResult) == .failed
expect(self.signing.requests).to(beEmpty())
self.logger.verifyMessageWasLogged(
Strings.signing.signature_was_requested_but_not_provided(request),
level: .warn
)
}
func testHeadersAreCaseInsensitive() {
self.changeClient(.informational)
stub(condition: isPath(Self.path)) { _ in
return .init(data: Data(),
statusCode: .success,
headers: [
"x-signature": "signature",
"x-revenuecat-request-time": String(Date().millisecondsSince1970)
])
}
self.signing.stubbedVerificationResult = true
let request: HTTPRequest = .createWithResponseVerification(method: .get, path: Self.path)
let response: DataResponse? = waitUntilValue { completion in
self.client.perform(request, completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.verificationResult) == .verified
}
func testSignatureNotRequested() {
self.changeClient(.disabled)
self.mockResponse()
let response: DataResponse? = waitUntilValue { completion in
self.client.perform(.init(method: .get, path: Self.path), completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.verificationResult) == .notRequested
expect(self.signing.requests).to(beEmpty())
}
func testVerifiedCachedResponseWithNotRequestedVerificationResponse() throws {
self.changeClient(.disabled)
let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1)
self.mockPath(
statusCode: .notModified,
requestDate: Self.date2,
eTagResponse: .init(
eTag: Self.eTag,
statusCode: .success,
data: try cachedResponse.jsonEncodedData,
validationTime: Self.date1,
verificationResult: .verified
)
)
let response: BodyWithDateResponse? = waitUntilValue { completion in
self.client.perform(.createWithResponseVerification(method: .get, path: Self.path),
completionHandler: completion)
}
expect(self.signing.requests).to(beEmpty())
expect(response).to(beSuccess())
expect(response?.value?.body.data) == cachedResponse.data
expect(response?.value?.body.requestDate).to(beCloseToDate(Self.date2))
expect(response?.value?.verificationResult) == .notRequested
}
func testPostRequestWithPostParametersHeader() throws {
self.changeClient(.informational)
let body = BodyWithSignature(key1: "a", key2: "b")
let request = HTTPRequest(method: .post(body), path: .mockPath)
let headers: [String: String]? = waitUntilValue { completion in
stub(condition: isPath(request.path)) { request in
completion(request.allHTTPHeaderFields)
return .emptySuccessResponse()
}
self.client.perform(request) { (_: EmptyResponse) in }
}
let header = try XCTUnwrap(headers?[HTTPClient.RequestHeader.postParameters.rawValue] as? String)
expect(header) == "key1,key2:sha256:59b271ae1bbcb1d31d41929817f4b16fb439eb4f31520b5ad1d5ce98920a7138"
}
func testRequestWithHeaderParametersHash() throws {
self.changeClient(.informational)
self.systemInfo.stubbedIsSandbox = true
let request = HTTPRequest(method: .get, path: .mockPath)
let headers: [String: String]? = waitUntilValue { completion in
stub(condition: isPath(request.path)) { request in
completion(request.allHTTPHeaderFields)
return .emptySuccessResponse()
}
self.client.perform(request) { (_: EmptyResponse) in }
}
let header = try XCTUnwrap(headers?[HTTPClient.RequestHeader.headerParametersForSignature.rawValue] as? String)
expect(header) == "X-Is-Sandbox:sha256:b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b"
}
func testEndpointsWithStaticSignatureDoNotIncludeHeaderParameterHash() throws {
self.changeClient(.informational)
let request = HTTPRequest(method: .get, path: .getOfferings(appUserID: "test"))
let result: [String: String]? = waitUntilValue { completion in
stub(condition: isPath(request.path)) { request in
completion(request.allHTTPHeaderFields)
return .emptySuccessResponse()
}
self.client.perform(request) { (_: EmptyResponse) in }
}
let headers = try XCTUnwrap(result)
expect(headers.keys).toNot(contain(HTTPClient.RequestHeader.headerParametersForSignature.rawValue))
}
func testPostRequestContainsBothHashHeaders() throws {
self.changeClient(.informational)
self.systemInfo.stubbedIsSandbox = true
let body = BodyWithSignature(key1: "a", key2: "b")
let request = HTTPRequest(method: .post(body), path: .mockPath)
let headers: [String: String]? = waitUntilValue { completion in
stub(condition: isPath(request.path)) { request in
completion(request.allHTTPHeaderFields)
return .emptySuccessResponse()
}
self.client.perform(request) { (_: EmptyResponse) in }
}
let post = try XCTUnwrap(headers?[HTTPClient.RequestHeader.postParameters.rawValue] as? String)
let header = try XCTUnwrap(headers?[HTTPClient.RequestHeader.headerParametersForSignature.rawValue] as? String)
expect(post) == "key1,key2:sha256:59b271ae1bbcb1d31d41929817f4b16fb439eb4f31520b5ad1d5ce98920a7138"
expect(header) == "X-Is-Sandbox:sha256:b5bea41b6c623f7c09f1bf24dcae58ebab3c0cdd90ad966bc43a45b44867e12b"
}
func testDisableHeaderSignatureVerification() throws {
self.changeClient(.informational, disableHeaderSignatureVerification: true)
let request = HTTPRequest(method: .get, path: .mockPath)
let headers: [String: String]? = waitUntilValue { completion in
stub(condition: isPath(request.path)) { request in
completion(request.allHTTPHeaderFields)
return .emptySuccessResponse()
}
self.client.perform(request) { (_: EmptyResponse) in }
}
expect(try XCTUnwrap(headers).keys)
.toNot(contain(HTTPClient.RequestHeader.headerParametersForSignature.rawValue))
}
}
final class InformationalSignatureVerificationHTTPClientTests: BaseSignatureVerificationHTTPClientTests {
override func setUpWithError() throws {
try super.setUpWithError()
self.changeClient(.informational)
}
func testValidSignature() throws {
let body = "body".asData
self.mockResponse(signature: Self.sampleSignature,
requestDate: Self.date2,
body: body)
self.signing.stubbedVerificationResult = true
let request: HTTPRequest = .createWithResponseVerification(method: .get, path: Self.path)
let response: DataResponse? = waitUntilValue { completion in
self.client.perform(request, completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.verificationResult) == .verified
expect(self.signing.requests).to(haveCount(1))
let signingRequest = try XCTUnwrap(self.signing.requests.onlyElement)
expect(signingRequest.parameters.message) == body
expect(signingRequest.parameters.nonce) == request.nonce
expect(signingRequest.parameters.requestDate) == Self.date2.millisecondsSince1970
expect(signingRequest.signature) == Self.sampleSignature
expect(signingRequest.publicKey).toNot(beNil())
}
func testPerformRequestOverridesVerificationMode() throws {
self.mockPath(statusCode: .success, requestDate: Self.date1)
self.signing.stubbedVerificationResult = false
let response: DataResponse? = waitUntilValue { completion in
self.client.perform(.createWithResponseVerification(method: .get, path: .logIn),
with: Signing.verificationMode(with: .informational),
completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.verificationResult) == .failed
}
func testValidSignatureWithETagResponse() throws {
let body = "body".asData
self.mockPath(
statusCode: .notModified,
requestDate: Self.date1,
eTagResponse: .init(
eTag: Self.eTag,
statusCode: .success,
data: body,
verificationResult: .verified
)
)
self.signing.stubbedVerificationResult = true
let request: HTTPRequest = .createWithResponseVerification(method: .get, path: Self.path)
let response: DataResponse? = waitUntilValue { completion in
self.client.perform(request, completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.verificationResult) == .verified
expect(response?.value?.body) == body
expect(self.signing.requests).to(haveCount(1))
let signingRequest = try XCTUnwrap(self.signing.requests.onlyElement)
expect(signingRequest.parameters.message).to(beNil())
expect(signingRequest.parameters.nonce) == request.nonce
expect(signingRequest.parameters.requestDate) == Self.date1.millisecondsSince1970
expect(signingRequest.parameters.requestHeaders.keys).to(contain(HTTPRequest.headersToSign.map(\.rawValue)))
expect(signingRequest.signature) == Self.sampleSignature
expect(signingRequest.publicKey).toNot(beNil())
}
func testValidSignatureForPostRequest() throws {
let responseContent = "response".asData
let requestBody = BodyWithSignature(key1: "a", key2: "b")
self.mockResponse(
signature: Self.sampleSignature,
requestDate: Self.date1,
body: responseContent,
statusCode: .success
)
self.signing.stubbedVerificationResult = true
let request: HTTPRequest = .createWithResponseVerification(method: .post(requestBody),
path: Self.path)
let response: DataResponse? = waitUntilValue { completion in
self.client.perform(request, completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.verificationResult) == .verified
expect(response?.value?.body) == responseContent
expect(self.signing.requests).to(haveCount(1))
let signingRequest = try XCTUnwrap(self.signing.requests.onlyElement)
expect(signingRequest.parameters.message) == responseContent
expect(signingRequest.parameters.nonce) == request.nonce
expect(signingRequest.parameters.requestDate) == Self.date1.millisecondsSince1970
expect(signingRequest.parameters.requestBody as? BodyWithSignature) == requestBody
expect(signingRequest.parameters.requestHeaders.keys).to(contain(HTTPRequest.headersToSign.map(\.rawValue)))
expect(signingRequest.signature) == Self.sampleSignature
expect(signingRequest.publicKey).toNot(beNil())
}
func testDisabledHeaderSignatureVerification() throws {
self.changeClient(.informational, disableHeaderSignatureVerification: true)
let responseContent = "response".asData
let requestBody = BodyWithSignature(key1: "a", key2: "b")
self.mockResponse(
signature: Self.sampleSignature,
requestDate: Self.date1,
body: responseContent,
statusCode: .success
)
self.signing.stubbedVerificationResult = true
let request: HTTPRequest = .createWithResponseVerification(method: .post(requestBody),
path: Self.path)
let response: DataResponse? = waitUntilValue { completion in
self.client.perform(request, completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.verificationResult) == .verified
expect(response?.value?.body) == responseContent
expect(self.signing.requests).to(haveCount(1))
let signingRequest = try XCTUnwrap(self.signing.requests.onlyElement)
expect(signingRequest.parameters.requestHeaders).to(beEmpty())
}
func testIncorrectSignatureReturnsResponse() throws {
self.mockPath(statusCode: .success, requestDate: Self.date1)
self.signing.stubbedVerificationResult = false
let response: DataResponse? = waitUntilValue { completion in
self.client.perform(.createWithResponseVerification(method: .get, path: Self.path),
completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.verificationResult) == .failed
expect(self.signing.requests).to(haveCount(1))
}
func testIncorrectSignatureLogsError() throws {
let request: HTTPRequest = .createWithResponseVerification(method: .get, path: Self.path)
self.mockPath(request.path, statusCode: .success, requestDate: Self.date1)
self.signing.stubbedVerificationResult = false
let _: DataResponse? = waitUntilValue { completion in
self.client.perform(request, completionHandler: completion)
}
self.logger.verifyMessageWasLogged(
Strings.signing.request_failed_verification(request),
level: .error,
expectedCount: 1
)
}
func testIgnoresResponseFromETagManagerIfItHadNotBeenVerified() throws {
self.signing.stubbedVerificationResult = true
self.mockPath(
statusCode: .success,
requestDate: Self.date1,
signature: Self.sampleSignature,
eTagResponse: .init(
eTag: Self.eTag,
statusCode: .success,
data: .init(),
verificationResult: .notRequested
)
)
let response: VerifiedHTTPResponse<Data>.Result? = waitUntilValue { completion in
self.client.perform(
.createWithResponseVerification(method: .get, path: Self.path),
completionHandler: completion
)
}
expect(response).toNot(beNil())
expect(response?.value?.httpStatusCode) == .success
expect(response?.value?.verificationResult) == .verified
}
func testCachedResponseDoesNotUpdateRequestDateIfNewResponseVerificationFails() throws {
let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1)
self.mockPath(
statusCode: .notModified,
requestDate: Self.date2,
eTagResponse: try .init(
eTag: Self.eTag,
statusCode: .success,
data: cachedResponse,
verificationResult: .verified
)
)
self.signing.stubbedVerificationResult = false
let response: BodyWithDateResponse? = waitUntilValue { completion in
self.client.perform(.createWithResponseVerification(method: .get, path: Self.path),
completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.body.data) == cachedResponse.data
expect(response?.value?.body.requestDate).to(beCloseToDate(cachedResponse.requestDate))
expect(response?.value?.verificationResult) == .failed
}
func testCachedResponseWithVerifiedResponse() throws {
let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1)
self.mockPath(
statusCode: .notModified,
requestDate: Self.date2,
eTagResponse: try .init(
eTag: Self.eTag,
statusCode: .success,
data: cachedResponse,
validationTime: Self.date1,
verificationResult: .verified
)
)
self.signing.stubbedVerificationResult = true
let response: BodyWithDateResponse? = waitUntilValue { completion in
self.client.perform(.createWithResponseVerification(method: .get, path: Self.path),
completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.body.data) == cachedResponse.data
expect(response?.value?.body.requestDate).to(beCloseToDate(Self.date2))
expect(response?.value?.verificationResult) == .verified
}
func testNotModifiedResponseFailedVerification() throws {
let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1)
self.mockPath(
statusCode: .notModified,
requestDate: Self.date2,
eTagResponse: try .init(
eTag: Self.eTag,
statusCode: .success,
data: cachedResponse,
validationTime: Self.date1,
verificationResult: .verified
)
)
self.signing.stubbedVerificationResult = false
let response: BodyWithDateResponse? = waitUntilValue { completion in
self.client.perform(.createWithResponseVerification(method: .get, path: Self.path),
completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.body.data) == cachedResponse.data
expect(response?.value?.body.requestDate).to(beCloseToDate(cachedResponse.requestDate))
expect(response?.value?.verificationResult) == .failed
}
func testCachedResponseUpdatesRequestDateIfNewResponseIsVerified() throws {
let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1)
self.signing.stubbedVerificationResult = true
self.mockPath(
statusCode: .notModified,
requestDate: Self.date2,
eTagResponse: try .init(
eTag: Self.eTag,
statusCode: .success,
data: cachedResponse,
validationTime: Self.date1,
verificationResult: .verified
)
)
let response: BodyWithDateResponse? = waitUntilValue { completion in
self.client.perform(.createWithResponseVerification(method: .get, path: Self.path),
completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.body.requestDate).to(beCloseToDate(Self.date2))
expect(response?.value?.verificationResult) == .verified
}
func testCachedResponseWithFailedVerificationAndFailedResponse() throws {
let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1)
self.mockPath(
statusCode: .notModified,
requestDate: Self.date2,
eTagResponse: try .init(
eTag: Self.eTag,
statusCode: .success,
data: cachedResponse,
validationTime: Self.date1,
verificationResult: .failed
)
)
self.signing.stubbedVerificationResult = false
let response: BodyWithDateResponse? = waitUntilValue { completion in
self.client.perform(.init(method: .get, path: Self.path),
completionHandler: completion)
}
expect(self.signing.requests).to(haveCount(1))
expect(response).to(beSuccess())
expect(response?.value?.body.requestDate).to(beCloseToDate(Self.date1))
expect(response?.value?.verificationResult) == .failed
}
func testNoCachedResponseAndNotVerifiedResponse() throws {
let path: HTTPRequest.Path = .getCustomerInfo(appUserID: "user")
self.mockPath(path, statusCode: .success, requestDate: Self.date2, signature: nil)
let response: DataResponse? = waitUntilValue { completion in
self.client.perform(.init(method: .get, path: path),
completionHandler: completion)
}
expect(self.signing.requests).to(beEmpty())
expect(response).to(beSuccess())
expect(response?.value?.requestDate).to(beCloseToDate(Self.date2))
expect(response?.value?.verificationResult) == .failed
}
func testNoCachedResponseAndVerifiedResponse() throws {
self.mockPath(statusCode: .success, requestDate: Self.date2)
self.signing.stubbedVerificationResult = true
let response: DataResponse? = waitUntilValue { completion in
self.client.perform(.init(method: .get, path: Self.path),
completionHandler: completion)
}
expect(self.signing.requests).to(haveCount(1))
expect(response).to(beSuccess())
expect(response?.value?.requestDate).to(beCloseToDate(Self.date2))
expect(response?.value?.verificationResult) == .verified
}
func testIgnoredCachedResponseWithFailedVerificationAndFailedResponse() throws {
let cachedResponse = BodyWithDate(data: "test", requestDate: Self.date1)
self.mockResponse(
path: Self.path,
signature: Self.sampleSignature,
requestDate: Self.date2,
body: try cachedResponse.jsonEncodedData,
statusCode: .success,
eTagResponse: try .init(
eTag: Self.eTag,
statusCode: .success,
data: cachedResponse,
validationTime: Self.date1,
verificationResult: .verified
)
)
self.signing.stubbedVerificationResult = false
let response: BodyWithDateResponse? = waitUntilValue { completion in
self.client.perform(.init(method: .get, path: Self.path),
completionHandler: completion)
}
expect(self.signing.requests).to(haveCount(1))
expect(response).to(beSuccess())
expect(response?.value?.body.requestDate).to(beCloseToDate(Self.date1))
expect(response?.value?.verificationResult) == .failed
}
}
final class EnforcedSignatureVerificationHTTPClientTests: BaseSignatureVerificationHTTPClientTests {
override func setUpWithError() throws {
try super.setUpWithError()
self.changeClientToEnforced()
}
func testValidSignature() {
self.mockResponse(signature: Self.sampleSignature, requestDate: Self.date1)
self.signing.stubbedVerificationResult = true
let response: DataResponse? = waitUntilValue { completion in
self.client.perform(.createWithResponseVerification(method: .get, path: Self.path),
completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.verificationResult) == .verified
expect(self.signing.requests).to(haveCount(1))
}
func testIncorrectSignatureReturnsError() {
self.mockResponse(signature: Self.sampleSignature, requestDate: Self.date1)
self.signing.stubbedVerificationResult = false
let response: DataResponse? = waitUntilValue { completion in
self.client.perform(.createWithResponseVerification(method: .get, path: Self.path),
completionHandler: completion)
}
expect(response).to(beFailure())
expect(response?.error)
.to(matchError(NetworkError.signatureVerificationFailed(path: Self.path, code: .success)))
}
func testPerformRequestOverridesIt() {
self.mockResponse(signature: Self.sampleSignature, requestDate: Self.date1)
self.signing.stubbedVerificationResult = false
let response: DataResponse? = waitUntilValue { completion in
self.client.perform(.createWithResponseVerification(method: .get, path: .logIn),
with: Signing.enforcedVerificationMode(),
completionHandler: completion)
}
expect(response).to(beFailure())
expect(response?.error)
.to(matchError(NetworkError.signatureVerificationFailed(path: Self.path, code: .success)))
}
func testPerformRequestWithDisabledModeOverridesIt() {
self.mockResponse(signature: Self.sampleSignature, requestDate: Self.date1)
self.signing.stubbedVerificationResult = false
let response: DataResponse? = waitUntilValue { completion in
self.client.perform(.createWithResponseVerification(method: .get, path: Self.path),
with: .disabled,
completionHandler: completion)
}
expect(response).to(beSuccess())
}
func testFakeSignatureFailuresInEnforcedMode() {
self.mockResponse(signature: Self.sampleSignature, requestDate: Self.date1)
self.signing.stubbedVerificationResult = true
self.changeClientToEnforced(forceSignatureFailures: true)
let response: EmptyResponse? = waitUntilValue { completion in
self.client.perform(.init(method: .get, path: Self.path), completionHandler: completion)
}
expect(response).to(beFailure())
expect(response?.error) == NetworkError.signatureVerificationFailed(path: Self.path, code: .success)
}
func testFakeSignatureFailuresInInformationalMode() {
self.mockResponse(signature: Self.sampleSignature, requestDate: Self.date1)
self.signing.stubbedVerificationResult = true
self.changeClient(.informational, forceSignatureFailures: true)
let response: EmptyResponse? = waitUntilValue { completion in
self.client.perform(.init(method: .get, path: Self.path), completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.verificationResult) == .failed
}
func testFakeSignatureFailuresWithDisabledVerification() {
self.mockResponse(signature: Self.sampleSignature, requestDate: Self.date1)
self.signing.stubbedVerificationResult = true
self.changeClient(.disabled, forceSignatureFailures: true)
let response: EmptyResponse? = waitUntilValue { completion in
self.client.perform(.init(method: .get, path: Self.path), completionHandler: completion)
}
expect(response).to(beSuccess())
expect(response?.value?.verificationResult) == .notRequested
}
}
// MARK: - Private
private extension BaseSignatureVerificationHTTPClientTests {
final func changeClient(
_ verificationMode: Configuration.EntitlementVerificationMode,
forceSignatureFailures: Bool = false,
disableHeaderSignatureVerification: Bool = false
) {
self.createClient(Signing.verificationMode(with: verificationMode),
forceSignatureFailures: forceSignatureFailures,
disableHeaderSignatureVerification: disableHeaderSignatureVerification)
}
final func changeClientToEnforced(forceSignatureFailures: Bool = false) {
self.createClient(Signing.enforcedVerificationMode(),
forceSignatureFailures: forceSignatureFailures)
}
private final func createClient(
_ mode: Signing.ResponseVerificationMode,
forceSignatureFailures: Bool = false,
disableHeaderSignatureVerification: Bool = false
) {
self.systemInfo = MockSystemInfo(
platformInfo: nil,
finishTransactions: false,
responseVerificationMode: mode,
dangerousSettings: .init(
autoSyncPurchases: true,
internalSettings: DangerousSettings.Internal(
forceSignatureFailures: forceSignatureFailures,
disableHeaderSignatureVerification: disableHeaderSignatureVerification
)
)
)
self.client = self.createClient()
}
final func mockResponse() {
self.mockResponse(signature: nil, requestDate: nil)
}
final func mockResponse(
path: HTTPRequestPath = BaseSignatureVerificationHTTPClientTests.path,
signature: String?,
requestDate: Date?,
eTag: String? = nil,
body: Data = .init(),
statusCode: HTTPStatusCode = .success,
eTagResponse: ETagManager.Response? = nil
) {
stub(condition: isPath(path)) { [weak self] request in
let headers: [String: String?] = [
HTTPClient.ResponseHeader.signature.rawValue: signature,
HTTPClient.ResponseHeader.eTag.rawValue: eTag,
HTTPClient.ResponseHeader.requestDate.rawValue: requestDate.map { String($0.millisecondsSince1970) }
]
if let eTagResponse = eTagResponse {
// swiftlint:disable:next force_try
try! self?.setETagCache(eTagResponse, for: request)
}
return .init(data: body,
statusCode: statusCode,
headers: headers.compactMapValues { $0 })
}
}
final func mockPath(
_ path: HTTPRequestPath = BaseSignatureVerificationHTTPClientTests.path,
statusCode: HTTPStatusCode,
requestDate: Date,
signature: String? = BaseSignatureVerificationHTTPClientTests.sampleSignature,
eTagResponse: ETagManager.Response? = nil
) {
self.mockResponse(
path: path,
signature: signature,
requestDate: requestDate,
eTag: Self.eTag,
body: .init(),
statusCode: statusCode,
eTagResponse: eTagResponse
)
}
private func setETagCache(_ response: ETagManager.Response, for request: URLRequest) throws {
self.userDefaults.set(try response.jsonEncodedData,
forKey: try XCTUnwrap(ETagManager.cacheKey(for: request)))
}
}
private extension ETagManager.Response {
init(
eTag: String,
statusCode: HTTPStatusCode,
data: Encodable,
validationTime: Date? = nil,
verificationResult: VerificationResult
) throws {
self.init(eTag: eTag,
statusCode: statusCode,
data: try data.jsonEncodedData,
validationTime: validationTime,
verificationResult: verificationResult
)
}
}