mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-11 22:55:55 +05:45
When compiling with Xcode 16, we started throwing a bunch of new warnings. Here's a list and explanation of each: - The most common one was needing to explicitly restate the @unchecked Sendable conformance from a parent class. - There were a couple switches where a new entry was added in tvOS but only for iOS 18, and needed to add it to make it exhaustive. - There we a couple of places where we were adding a protocol conformance to a class from a different module e.g. extension DispatchTimeInterval: Comparable. This is considered unsafe now because the owners of the module could add the conformance later. Solved by removing the conformance which wasn't being used widely in the codebase, only in tests. - Finally there was a class where we were capturing SKRequest and SKProductsRequest instances in a sendable block, but only to either log something, or use them as dictionary key, so I just made the classes Sendable (with the new @retroactice attribute because they belong to a different module)
208 lines
6.3 KiB
Swift
208 lines
6.3 KiB
Swift
@testable import RevenueCat
|
|
|
|
import Nimble
|
|
import SnapshotTesting
|
|
import XCTest
|
|
|
|
class MockHTTPClient: HTTPClient {
|
|
|
|
struct Call {
|
|
|
|
let request: HTTPRequest
|
|
let headers: RequestHeaders
|
|
|
|
}
|
|
|
|
struct Response {
|
|
|
|
let response: VerifiedHTTPResponse<Data>.Result
|
|
let delay: DispatchTimeInterval
|
|
|
|
private init(response: VerifiedHTTPResponse<Data>.Result, delay: DispatchTimeInterval) {
|
|
self.response = response
|
|
self.delay = delay
|
|
}
|
|
|
|
init(
|
|
statusCode: HTTPStatusCode,
|
|
response: [String: Any] = [:],
|
|
responseHeaders: HTTPResponse.Headers = [:],
|
|
verificationResult: VerificationResult = .defaultValue,
|
|
delay: DispatchTimeInterval = .never
|
|
) {
|
|
// swiftlint:disable:next force_try
|
|
let data = try! JSONSerialization.data(withJSONObject: response)
|
|
|
|
let response = VerifiedHTTPResponse(
|
|
response: .init(
|
|
httpStatusCode: statusCode,
|
|
responseHeaders: responseHeaders,
|
|
body: data
|
|
),
|
|
verificationResult: verificationResult
|
|
)
|
|
|
|
self.init(response: .success(response), delay: delay)
|
|
}
|
|
|
|
init(error: NetworkError, delay: DispatchTimeInterval = .never) {
|
|
self.init(response: .failure(error), delay: delay)
|
|
}
|
|
|
|
}
|
|
|
|
var mocks: [URL: Response] = [:]
|
|
var calls: [Call] = []
|
|
|
|
init(apiKey: String,
|
|
systemInfo: SystemInfo,
|
|
eTagManager: ETagManager,
|
|
diagnosticsTracker: DiagnosticsTrackerType?,
|
|
dnsChecker: DNSCheckerType.Type = DNSChecker.self,
|
|
requestTimeout: TimeInterval = 7,
|
|
sourceTestFile: StaticString = #file) {
|
|
self.sourceTestFile = sourceTestFile
|
|
|
|
super.init(apiKey: apiKey,
|
|
systemInfo: systemInfo,
|
|
eTagManager: eTagManager,
|
|
signing: FakeSigning.default,
|
|
diagnosticsTracker: diagnosticsTracker,
|
|
dnsChecker: dnsChecker,
|
|
requestTimeout: requestTimeout,
|
|
operationDispatcher: MockOperationDispatcher())
|
|
}
|
|
|
|
private let sourceTestFile: StaticString
|
|
|
|
override func perform<Value: HTTPResponseBody>(
|
|
_ request: HTTPRequest,
|
|
with verificationMode: Signing.ResponseVerificationMode? = nil,
|
|
completionHandler: Completion<Value>?
|
|
) {
|
|
let verificationMode = verificationMode ?? self.systemInfo.responseVerificationMode
|
|
|
|
let request = request
|
|
.requestAddingNonceIfRequired(with: verificationMode)
|
|
.withHardcodedNonce
|
|
|
|
let call = Call(request: request,
|
|
headers: request.headers(with: self.authHeaders,
|
|
defaultHeaders: self.defaultHeaders,
|
|
verificationMode: verificationMode,
|
|
internalSettings: self.systemInfo.dangerousSettings.internalSettings))
|
|
|
|
DispatchQueue.main.async {
|
|
self.calls.append(call)
|
|
|
|
assertSnapshot(matching: call,
|
|
as: .formattedJson,
|
|
file: self.sourceTestFile,
|
|
testName: CurrentTestCaseTracker.osVersionAndTestName)
|
|
|
|
let mock = self.mocks[request.path.url!] ?? .init(statusCode: .success)
|
|
|
|
if let completionHandler = completionHandler {
|
|
let response: VerifiedHTTPResponse<Value>.Result = mock.response.parseResponse()
|
|
|
|
if mock.delay != .never {
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + mock.delay) {
|
|
completionHandler(response)
|
|
}
|
|
} else {
|
|
completionHandler(response)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
func mock(requestPath: HTTPRequest.Path, response: Response) {
|
|
self.mock(path: requestPath, response: response)
|
|
}
|
|
|
|
func mock(requestPath: HTTPRequest.DiagnosticsPath, response: Response) {
|
|
self.mock(path: requestPath, response: response)
|
|
}
|
|
|
|
private func mock(path: HTTPRequestPath, response: Response) {
|
|
self.mocks[path.url!] = response
|
|
}
|
|
|
|
/// Override headers that depend on the environment to make them stable.
|
|
override var defaultHeaders: RequestHeaders {
|
|
var result = super.defaultHeaders
|
|
result["X-Version"] = "4.0.0"
|
|
// Snapshots are shared across platforms so we need this to be stable.
|
|
result["X-Platform"] = "iOS"
|
|
result["X-Client-Build-Version"] = "12345"
|
|
result["X-Client-Version"] = "17.0.0"
|
|
result["X-Platform-Version"] = "Version 17.0.0 (Build 21A342)"
|
|
|
|
if result.keys.contains("X-Apple-Device-Identifier") {
|
|
result["X-Apple-Device-Identifier"] = "5D7C0074-07E4-4564-AAA4-4008D0640881"
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
}
|
|
|
|
extension MockHTTPClient: @unchecked Sendable {}
|
|
|
|
// MARK: - MockHTTPClient.Call Encodable
|
|
|
|
extension RevenueCat.HTTPRequest: Swift.Encodable {
|
|
|
|
enum CodingKeys: String, CodingKey {
|
|
|
|
case method
|
|
case body
|
|
case url
|
|
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var container = encoder.container(keyedBy: CodingKeys.self)
|
|
|
|
try container.encode(self.path.url, forKey: .url)
|
|
try container.encode(self.method.httpMethod, forKey: .method)
|
|
|
|
if let body = self.requestBody {
|
|
try body.encode(inContainer: &container, forKey: .body)
|
|
} else {
|
|
try container.encodeNil(forKey: .body)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
extension MockHTTPClient.Call: Encodable { }
|
|
|
|
// MARK: -
|
|
|
|
private extension HTTPRequest {
|
|
|
|
/// Creates a copy of the request replacing the `nonce` with a fixed value
|
|
/// to make snapshot tests deterministic
|
|
var withHardcodedNonce: Self {
|
|
if self.nonce == nil {
|
|
return self
|
|
} else {
|
|
var copy = self
|
|
copy.nonce = "1234567890ab".asData
|
|
|
|
return copy
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private extension Encodable {
|
|
func encode<Container: KeyedEncodingContainerProtocol>(
|
|
inContainer container: inout Container,
|
|
forKey key: Container.Key
|
|
) throws {
|
|
try container.encode(self, forKey: key)
|
|
}
|
|
}
|