mirror of
https://github.com/RevenueCat/purchases-ios
synced 2026-11-11 22:55:55 +05:45
250 lines
8.9 KiB
Swift
250 lines
8.9 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
|
|
//
|
|
// BaseBackendIntegrationTests.swift
|
|
//
|
|
// Created by Nacho Soto on 4/1/22.
|
|
|
|
import Nimble
|
|
import XCTest
|
|
|
|
#if ENABLE_CUSTOM_ENTITLEMENT_COMPUTATION
|
|
@testable import RevenueCat_CustomEntitlementComputation
|
|
#else
|
|
@testable import RevenueCat
|
|
#endif
|
|
|
|
final class TestPurchaseDelegate: NSObject, PurchasesDelegate, Sendable {
|
|
|
|
private let _customerInfo: Atomic<CustomerInfo?> = nil
|
|
private let _customerInfoUpdateCount: Atomic<Int> = .init(0)
|
|
|
|
func purchases(_ purchases: Purchases, receivedUpdated customerInfo: CustomerInfo) {
|
|
self._customerInfo.value = customerInfo
|
|
self._customerInfoUpdateCount.value += 1
|
|
}
|
|
|
|
var customerInfo: CustomerInfo? { return self._customerInfo.value }
|
|
var customerInfoUpdateCount: Int { return self._customerInfoUpdateCount.value }
|
|
|
|
}
|
|
|
|
@MainActor
|
|
class BaseBackendIntegrationTests: TestCase {
|
|
|
|
private var userDefaults: UserDefaults!
|
|
private var applicationSupportDirectory: URL!
|
|
private var testUUID: UUID!
|
|
|
|
// swiftlint:disable:next weak_delegate
|
|
private(set) var purchasesDelegate: TestPurchaseDelegate!
|
|
|
|
private var mainThreadMonitor: MainThreadMonitor!
|
|
|
|
fileprivate var serverIsDown: Bool = false
|
|
|
|
static var isSandbox: Bool = true {
|
|
didSet {
|
|
BundleSandboxEnvironmentDetector.default = MockSandboxEnvironmentDetector(isSandbox: Self.isSandbox)
|
|
}
|
|
}
|
|
|
|
// MARK: - Overridable configuration
|
|
|
|
class var storeKitVersion: StoreKitVersion { return .default }
|
|
class var observerMode: Bool { return false }
|
|
class var responseVerificationMode: Signing.ResponseVerificationMode {
|
|
return .enforced(Signing.loadPublicKey())
|
|
}
|
|
var enableReceiptFetchRetry: Bool = true
|
|
|
|
var apiKey: String { return Constants.apiKey }
|
|
var proxyURL: String? { return Constants.proxyURL }
|
|
|
|
func configurePurchases() {
|
|
Purchases.proxyURL = self.proxyURL.flatMap(URL.init(string:))
|
|
Purchases.logLevel = .verbose
|
|
|
|
Purchases.configure(withAPIKey: self.apiKey,
|
|
appUserID: nil,
|
|
observerMode: Self.observerMode,
|
|
userDefaults: self.userDefaults,
|
|
applicationSupportDirectory: self.applicationSupportDirectory,
|
|
platformInfo: nil,
|
|
responseVerificationMode: Self.responseVerificationMode,
|
|
storeKitVersion: Self.storeKitVersion,
|
|
storeKitTimeout: Configuration.storeKitRequestTimeoutDefault,
|
|
networkTimeout: Configuration.networkTimeoutDefault,
|
|
dangerousSettings: self.dangerousSettings,
|
|
showStoreMessagesAutomatically: true,
|
|
diagnosticsEnabled: false)
|
|
}
|
|
|
|
// MARK: -
|
|
|
|
@MainActor
|
|
override func setUp() async throws {
|
|
try await super.setUp()
|
|
|
|
// Avoid continuing with potentially bad data after a failed assertion
|
|
self.continueAfterFailure = false
|
|
|
|
guard self.apiKey != "REVENUECAT_API_KEY",
|
|
self.apiKey != "REVENUECAT_LOAD_SHEDDER_API_KEY",
|
|
self.proxyURL != "REVENUECAT_PROXY_URL" else {
|
|
throw ErrorUtils.configurationError(message: "Must set configuration in `Constants.swift`")
|
|
}
|
|
|
|
Self.isSandbox = true
|
|
|
|
self.mainThreadMonitor = .init()
|
|
self.mainThreadMonitor.run()
|
|
|
|
self.createUserDefaults()
|
|
|
|
self.applicationSupportDirectory = URL
|
|
.cachesDirectory
|
|
.appendingPathComponent(UUID().uuidString, conformingTo: .directory)
|
|
|
|
// We use a different identifier for each test to ensure the backend
|
|
// doesn't produce conflicts when producing similar receipts across
|
|
// separate test invocations.
|
|
self.testUUID = UUID()
|
|
|
|
self.clearReceiptIfExists()
|
|
await self.createPurchases()
|
|
self.verifyPurchasesDoesNotLeak()
|
|
}
|
|
|
|
override func tearDown() {
|
|
super.tearDown()
|
|
|
|
self.mainThreadMonitor = nil
|
|
}
|
|
|
|
/// Simulates closing the app and re-opening with a fresh instance of `Purchases`.
|
|
final func resetSingleton() async {
|
|
Logger.warn(TestMessage.resetting_purchases_singleton)
|
|
|
|
Purchases.clearSingleton()
|
|
await self.createPurchases()
|
|
}
|
|
|
|
/// - Returns: `Purchases.shared` if it's currently configured
|
|
/// - Throws: `ErrorCode` if it's not
|
|
/// - Note: This is the recomended way of accessing `Purchases.shared`, as it won't make the test crash.
|
|
/// If an expectation fails in an `async` fails, sometimes `XCTest` seems to continue execution of the test despite
|
|
/// having started tearing down the test (and therefore clearing `Purchases.shared`, which will lead to a crash
|
|
/// and will prevent the test from being retried.
|
|
final var purchases: Purchases {
|
|
get throws {
|
|
if Purchases.isConfigured {
|
|
return Purchases.shared
|
|
} else {
|
|
throw ErrorCode.configurationError
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
private extension BaseBackendIntegrationTests {
|
|
|
|
func clearReceiptIfExists() {
|
|
let manager = FileManager.default
|
|
|
|
guard let url = Bundle.main.appStoreReceiptURL, manager.fileExists(atPath: url.path) else { return }
|
|
|
|
do {
|
|
Logger.info(TestMessage.removing_receipt(url))
|
|
try manager.removeItem(at: url)
|
|
} catch {
|
|
Logger.appleWarning(TestMessage.error_removing_url(url, error))
|
|
}
|
|
}
|
|
|
|
func createUserDefaults() {
|
|
self.userDefaults = UserDefaults(suiteName: Constants.userDefaultsSuiteName)
|
|
self.userDefaults.removePersistentDomain(forName: Constants.userDefaultsSuiteName)
|
|
// See also `SynchronizedUserDefaults`.
|
|
// While Apple states `this method is unnecessary and shouldn't be used`, it's still
|
|
// necessary to call `synchronize` in order for the `removePersistentDomain` changes to take effect.
|
|
self.userDefaults.synchronize()
|
|
|
|
// Verify that user defaults is indeed empty.
|
|
// Reusing users across tests would lead to flaky failures.
|
|
expect(self.userDefaults.value(forKey: DeviceCache.CacheKeys.appUserDefaults.rawValue))
|
|
.to(
|
|
beNil(),
|
|
description: "Found existing user after clearing UserDefaults"
|
|
)
|
|
}
|
|
|
|
func createPurchases() async {
|
|
self.purchasesDelegate = TestPurchaseDelegate()
|
|
self.configurePurchases()
|
|
self.simulateForegroundingApp()
|
|
|
|
Purchases.shared.delegate = self.purchasesDelegate
|
|
|
|
await self.waitForAnonymousUser()
|
|
}
|
|
|
|
func verifyPurchasesDoesNotLeak() {
|
|
weak var purchases = Purchases.shared
|
|
|
|
// See `addTeardownBlock` docs:
|
|
// - These run *before* `tearDown`.
|
|
// - They run in LIFO order.
|
|
self.addTeardownBlock {
|
|
Purchases.clearSingleton()
|
|
|
|
// Note: this captures the boolean to avoid race conditions when Nimble tries
|
|
// to print `purchases` while it's being deallocated.
|
|
expect { purchases == nil }.toEventually(beTrue(), description: "Purchases has leaked")
|
|
}
|
|
}
|
|
|
|
func waitForAnonymousUser() async {
|
|
// SDK initialization begins with an initial request to offerings,
|
|
// which results in a get-create of the initial anonymous user.
|
|
// To avoid race conditions with when this request finishes and make all tests deterministic
|
|
// this waits for that request to finish.
|
|
//
|
|
// This ignores errors because this class does not set up `SKTestSession`,
|
|
// so subclasses would fail to load offerings if they don't set one up.
|
|
// However, it still serves the purpose of waiting for the anonymous user.
|
|
// If there is something broken when loading offerings, there is a dedicated test that would fail instead.
|
|
_ = try? await Purchases.shared.offerings()
|
|
}
|
|
|
|
func simulateForegroundingApp() {
|
|
// This notification is posted automatically on app launch
|
|
NotificationCenter.default.post(name: SystemInfo.applicationWillEnterForegroundNotification, object: nil)
|
|
}
|
|
|
|
private var dangerousSettings: DangerousSettings {
|
|
return .init(autoSyncPurchases: true,
|
|
internalSettings: self)
|
|
}
|
|
|
|
}
|
|
|
|
extension BaseBackendIntegrationTests: InternalDangerousSettingsType {
|
|
|
|
var forceServerErrors: Bool { return self.serverIsDown }
|
|
var forceSignatureFailures: Bool { return false }
|
|
var disableHeaderSignatureVerification: Bool { return false }
|
|
var testReceiptIdentifier: String? { return self.testUUID.uuidString }
|
|
|
|
final func serverDown() { self.serverIsDown = true }
|
|
final func serverUp() { self.serverIsDown = false }
|
|
|
|
}
|