Skip to content

Commit

Permalink
Introduced SDKTester to help diagnose SDK configuration errors
Browse files Browse the repository at this point in the history
Finishes [CSDK-451].
This new small public API allows users to quickly figure out if everything in the SDK is correctly configured in a simple way:
```swift
let tester = SDKTester.default
do {
  try await tester.test()
} catch {
  print(error)
}
```

The specific underlying errors will provide information about what failed.

We can continue growing this to check for more specific things, but for now it does 4 things:
- Verify API connectivity: networking issues, firewalling, etc.
- Verify API key is correct
- Verify `Offerings` are configured correctly
- Verify that all products in `Offerings` are configured correctly and found in `StoreKit`

This new API is covered by:
- API testers
- Unit tests
- Integration tests (both on `SK1` and `SK2`)

- #1970
- #1971
- #1973
- #1974
- #1975
- #1976
  • Loading branch information
NachoSoto committed Oct 11, 2022
1 parent 5896d72 commit 02f7a98
Show file tree
Hide file tree
Showing 17 changed files with 888 additions and 4 deletions.
16 changes: 16 additions & 0 deletions RevenueCat.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -274,8 +274,12 @@
5774F9BE2805E71100997128 /* Fixtures in Resources */ = {isa = PBXBuildFile; fileRef = 5774F9BD2805E71100997128 /* Fixtures */; };
5774F9C12805EA3000997128 /* BaseHTTPResponseTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5774F9C02805EA3000997128 /* BaseHTTPResponseTest.swift */; };
5774F9C22805EA6900997128 /* CustomerInfoDecodingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5774F9B92805E6E200997128 /* CustomerInfoDecodingTests.swift */; };
578C5F2C28DB82DD00A56F02 /* SDKTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = 578C5F2B28DB82DD00A56F02 /* SDKTester.swift */; };
578FB10E27ADDA8000F70709 /* AvailabilityChecks.swift in Sources */ = {isa = PBXBuildFile; fileRef = B3BE0263275942D500915B4C /* AvailabilityChecks.swift */; };
579189B728F4747700BF4963 /* EitherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 579189B628F4747700BF4963 /* EitherTests.swift */; };
579189E928F47E8D00BF4963 /* SDKTesterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 579189E828F47E8D00BF4963 /* SDKTesterTests.swift */; };
579189EB28F47F0F00BF4963 /* MockPurchases.swift in Sources */ = {isa = PBXBuildFile; fileRef = 579189EA28F47F0F00BF4963 /* MockPurchases.swift */; };
579189FD28F4966500BF4963 /* OtherIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 579189FC28F4966500BF4963 /* OtherIntegrationTests.swift */; };
5791A1C82767FC9400C972AA /* ManageSubscriptionsHelperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5791A1C72767FC9400C972AA /* ManageSubscriptionsHelperTests.swift */; };
5791CE80273F26A000E50C4B /* base64encoded_sandboxReceipt.txt in Resources */ = {isa = PBXBuildFile; fileRef = 5791CE7F273F26A000E50C4B /* base64encoded_sandboxReceipt.txt */; };
579234E327F7788900B39C68 /* BaseBackendIntegrationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 579234E127F777EE00B39C68 /* BaseBackendIntegrationTests.swift */; };
Expand Down Expand Up @@ -791,7 +795,11 @@
5774F9B92805E6E200997128 /* CustomerInfoDecodingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomerInfoDecodingTests.swift; sourceTree = "<group>"; };
5774F9BD2805E71100997128 /* Fixtures */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = folder; name = Fixtures; path = Tests/UnitTests/Networking/Responses/Fixtures; sourceTree = SOURCE_ROOT; };
5774F9C02805EA3000997128 /* BaseHTTPResponseTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseHTTPResponseTest.swift; sourceTree = "<group>"; };
578C5F2B28DB82DD00A56F02 /* SDKTester.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKTester.swift; sourceTree = "<group>"; };
579189B628F4747700BF4963 /* EitherTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EitherTests.swift; sourceTree = "<group>"; };
579189E828F47E8D00BF4963 /* SDKTesterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SDKTesterTests.swift; sourceTree = "<group>"; };
579189EA28F47F0F00BF4963 /* MockPurchases.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockPurchases.swift; sourceTree = "<group>"; };
579189FC28F4966500BF4963 /* OtherIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OtherIntegrationTests.swift; sourceTree = "<group>"; };
5791A1C72767FC9400C972AA /* ManageSubscriptionsHelperTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ManageSubscriptionsHelperTests.swift; sourceTree = "<group>"; };
5791CE7F273F26A000E50C4B /* base64encoded_sandboxReceipt.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = base64encoded_sandboxReceipt.txt; sourceTree = "<group>"; };
579234E127F777EE00B39C68 /* BaseBackendIntegrationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseBackendIntegrationTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1397,6 +1405,7 @@
37E35C9439E087F63ECC4F59 /* MockProductsManager.swift */,
37E35B08709090FBBFB16EBD /* MockProductsRequest.swift */,
37E35F783903362B65FB7AF3 /* MockProductsRequestFactory.swift */,
579189EA28F47F0F00BF4963 /* MockPurchases.swift */,
351B515D26D44B9900BD2BD7 /* MockPurchasesDelegate.swift */,
351B515126D44AF000BD2BD7 /* MockReceiptFetcher.swift */,
37E354B13440508B46C9A530 /* MockReceiptParser.swift */,
Expand Down Expand Up @@ -1425,6 +1434,7 @@
isa = PBXGroup;
children = (
579234E127F777EE00B39C68 /* BaseBackendIntegrationTests.swift */,
579189FC28F4966500BF4963 /* OtherIntegrationTests.swift */,
2DE20B6E264087FB004C597D /* StoreKitIntegrationTests.swift */,
579234E427F779FE00B39C68 /* SubscriberAttributesManagerIntegrationTests.swift */,
2CD2C541278CE0E0005D1CC2 /* RevenueCat_IntegrationPurchaseTesterConfiguration.storekit */,
Expand Down Expand Up @@ -1506,6 +1516,7 @@
5766AA59283D4CAB00FA6091 /* IgnoreHashableTests.swift */,
57ACB12328174B9F000DCC9F /* CustomerInfo+TestExtensions.swift */,
57FDAABD28493A29009A48F1 /* SandboxEnvironmentDetectorTests.swift */,
579189E828F47E8D00BF4963 /* SDKTesterTests.swift */,
57032ABE28C13CE4004FF47A /* StoreKit2SettingTests.swift */,
);
path = Misc;
Expand Down Expand Up @@ -1646,6 +1657,7 @@
children = (
A5F0104D2717B3150090732D /* BeginRefundRequestHelper.swift */,
35E840C5270FB47C00899AE2 /* ManageSubscriptionsHelper.swift */,
578C5F2B28DB82DD00A56F02 /* SDKTester.swift */,
);
path = Support;
sourceTree = "<group>";
Expand Down Expand Up @@ -2424,6 +2436,7 @@
5751379527F4C4D80064AB2C /* Optional+Extensions.swift in Sources */,
B3852FA026C1ED1F005384F8 /* IdentityManager.swift in Sources */,
9A65E03625918B0500DE00B0 /* ConfigureStrings.swift in Sources */,
578C5F2C28DB82DD00A56F02 /* SDKTester.swift in Sources */,
B34605C9279A6E380031CA74 /* GetIntroEligibilityOperation.swift in Sources */,
354895D6267BEDE3001DC5B1 /* ReservedSubscriberAttributes.swift in Sources */,
570FAF4B2864EC2300D3C769 /* NonSubscriptionTransaction.swift in Sources */,
Expand Down Expand Up @@ -2676,9 +2689,11 @@
5766AAE5283E9E9C00FA6091 /* PurchasesGetOfferingsTests.swift in Sources */,
2DDF41DA24F6F4DB005BC22D /* ReceiptParserTests.swift in Sources */,
2D1015E2275A67E40086173F /* SubscriptionPeriodTests.swift in Sources */,
579189EB28F47F0F00BF4963 /* MockPurchases.swift in Sources */,
574A2EE9282C403800150D40 /* AnyDecodableTests.swift in Sources */,
351B519F26D4508A00BD2BD7 /* DeviceCacheTests.swift in Sources */,
2D22BF6826F3CC6D001AE2F9 /* XCTestCase+Extensions.swift in Sources */,
579189E928F47E8D00BF4963 /* SDKTesterTests.swift in Sources */,
351B51B626D450E800BD2BD7 /* ReceiptFetcherTests.swift in Sources */,
5796A3C027D7D64500653165 /* ResultExtensionsTests.swift in Sources */,
351B51A726D450D400BD2BD7 /* SystemInfoTests.swift in Sources */,
Expand Down Expand Up @@ -2771,6 +2786,7 @@
579234E527F779FE00B39C68 /* SubscriberAttributesManagerIntegrationTests.swift in Sources */,
2D3BFAD226DEA46600370B11 /* MockProductsRequest.swift in Sources */,
2D1015DB275A4EAE0086173F /* AvailabilityChecks.swift in Sources */,
579189FD28F4966500BF4963 /* OtherIntegrationTests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Most features require configuring the SDK before using it.
### Configuring the SDK
- ``Purchases/configure(withAPIKey:)``
- ``Purchases/configure(with:)-6oipy``
- ``SDKTester``

### Displaying Products
- ``Purchases/offerings()``
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Or browse our iOS sample apps:
### Configuring the SDK
- ``Purchases/configure(withAPIKey:)``
- ``Purchases/configure(with:)-6oipy``
- ``SDKTester``

### Displaying Products
- ``Offerings``
Expand Down
14 changes: 14 additions & 0 deletions Sources/Networking/Backend.swift
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,20 @@ class Backend {

}

extension Backend {

/// - Throws: `NetworkError`
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
func healthRequest() async throws {
try await Async.call { completion in
self.internalAPI.healthRequest { error in
completion(.init(error))
}
}
}

}

// @unchecked because:
// - Class is not `final` (it's mocked). This implicitly makes subclasses `Sendable` even if they're not thread-safe.
extension Backend: @unchecked Sendable {}
Expand Down
20 changes: 16 additions & 4 deletions Sources/Purchasing/Purchases/Purchases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1054,14 +1054,28 @@ extension Purchases: @unchecked Sendable {}

// MARK: Internal

extension Purchases: InternalPurchasesType {

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
internal func healthRequest() async throws {
do {
try await self.backend.healthRequest()
} catch {
throw NewErrorUtils.purchasesError(withUntypedError: error)
}
}

}

/// Necessary because `ErrorUtils` inside of `Purchases` finds the obsoleted type.
private typealias NewErrorUtils = ErrorUtils

internal extension Purchases {

var isStoreKit1Configured: Bool {
return self.paymentQueueWrapper.sk1Wrapper != nil
}

#if DEBUG

/// - Returns: the parsed `AppleReceipt`
///
/// - Warning: this is only meant for integration tests, as a way to debug purchase failures.
Expand All @@ -1072,8 +1086,6 @@ internal extension Purchases {
return try receipt.map { try ReceiptParser.default.parse(from: $0) }
}

#endif

/// - Parameter syncedAttribute: will be called for every attribute that is updated
/// - Parameter completion: will be called once all attributes have completed syncing
/// - Returns: the number of attributes that will be synced
Expand Down
15 changes: 15 additions & 0 deletions Sources/Purchasing/Purchases/PurchasesType.swift
Original file line number Diff line number Diff line change
Expand Up @@ -941,3 +941,18 @@ public protocol PurchasesSwiftType: AnyObject {
#endif

}

// MARK: -

/// Interface for ``Purchases``'s internal-only methods.
internal protocol InternalPurchasesType: AnyObject {

/// Performs an unauthenticated request to the API to verify connectivity.
/// - Throws: `PublicError` if request failed.
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
func healthRequest() async throws

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
func offerings(fetchPolicy: OfferingsManager.FetchPolicy) async throws -> Offerings

}
170 changes: 170 additions & 0 deletions Sources/Support/SDKTester.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
//
// 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
//
// SDKTester.swift
//
// Created by Nacho Soto on 9/21/22.

import Foundation

/// `SDKTester` allows you to ensure that the SDK is set up correctly by diagnosing configuration errors.
/// To run the test, simply call ``SDKTester/test()``.
///
/// #### Example:
/// ```swift
/// let tester = SDKTester.default
/// do {
/// try await tester.test()
/// } catch {
/// print("SDKTester failed: \(error.localizedDescription)")
/// }
/// ```
@objc(RCSDKTester)
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
public final class SDKTester: NSObject {

typealias SDK = PurchasesType & InternalPurchasesType

private let purchases: SDK

init(purchases: SDK) {
self.purchases = purchases
}

/// Default instance of `SDKTester`.
/// Note: you must call ``Purchases/configure(with:)-6oipy`` before using this.
@objc
public static let `default`: SDKTester = .init(purchases: Purchases.shared)
}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
extension SDKTester {

/// An error that represents a failing step in ``SDKTester``
public enum Error: Swift.Error {

/// Connection to the API failed
case failedConectingToAPI(Swift.Error)

/// API key is invalid
case invalidAPIKey

/// Fetching offerings failed due to the underlying error
case failedFetchingOfferings(Swift.Error)

/// Any other not identifier error. You can check the undelying error for details.
case unknown(Swift.Error)

}

}

// MARK: - Implementation

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
extension SDKTester {

/// Perform tests to ensure SDK is configured correctly.
/// - `Throws`: ``SDKTester/Error`` if any step fails
@objc(testWithCompletion:)
public func test() async throws {
do {
try await self.unauthenticatedRequest()
try await self.authenticatedRequest()
try await self.offeringsRequest()
} catch let error as Error {
throw error
} catch let error {
// Catch every other error to ensure that we only throw `Error`s from here.
throw Error.unknown(error)
}
}

}

// MARK: - Private

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
private extension SDKTester {

/// Makes a request to the backend, to verify connectivity, firewalls, or anything blocking network traffic.
func unauthenticatedRequest() async throws {
do {
try await self.purchases.healthRequest()
} catch {
throw Error.failedConectingToAPI(error)
}
}

func authenticatedRequest() async throws {
do {
_ = try await self.purchases.customerInfo()
} catch let error as ErrorCode {
throw self.convert(error)
} catch {
throw Error.unknown(error)
}
}

func offeringsRequest() async throws {
do {
_ = try await self.purchases.offerings(fetchPolicy: .failIfProductsAreMissing)
} catch {
throw Error.failedFetchingOfferings(error)
}
}

func convert(_ error: ErrorCode) -> Error {
switch error {
case .unknownError:
return Error.unknown(error)

case .offlineConnectionError:
return Error.failedConectingToAPI(error)

case .invalidCredentialsError:
return Error.invalidAPIKey

default:
return Error.unknown(error)
}
}

}

@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.2, *)
extension SDKTester.Error: CustomNSError {

// swiftlint:disable:next missing_docs
public var errorUserInfo: [String: Any] {
return [
NSUnderlyingErrorKey: self.underlyingError as NSError? ?? NSNull(),
NSLocalizedDescriptionKey: self.localizedDescription
]
}

var localizedDescription: String {
switch self {
case let .unknown(error): return "Unknown error: \(error.localizedDescription)"
case let .failedConectingToAPI(error): return "Error connecting to API: \(error.localizedDescription)"
case let .failedFetchingOfferings(error): return "Failed fetching offerings: \(error.localizedDescription)"
case .invalidAPIKey: return "API key is not valid"
}
}

private var underlyingError: Swift.Error? {
switch self {
case let .unknown(error): return error
case let .failedConectingToAPI(error): return error
case let .failedFetchingOfferings(error): return error
case .invalidAPIKey: return nil
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
575885A52748274E00CA2169 /* RevenueCat.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 575885A32748274E00CA2169 /* RevenueCat.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
5758EE5227864A8500B3B703 /* RCStoreProductAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 5758EE5127864A8500B3B703 /* RCStoreProductAPI.m */; };
5758EE5B2786548D00B3B703 /* RCStoreTransactionAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 5758EE5A2786548D00B3B703 /* RCStoreTransactionAPI.m */; };
57918A1628F4C58300BF4963 /* RCSDKTesterAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 57918A1528F4C58300BF4963 /* RCSDKTesterAPI.m */; };
57DE807828074E59008D6C6F /* RCStorefrontAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = 57DE807728074E59008D6C6F /* RCStorefrontAPI.m */; };
A513AD32272B328800E0C1BA /* RCRefundRequestStatusAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = A513AD30272B327A00E0C1BA /* RCRefundRequestStatusAPI.m */; };
B32554452825E74000DA62EA /* RCConfigurationAPI.m in Sources */ = {isa = PBXBuildFile; fileRef = B32554442825E74000DA62EA /* RCConfigurationAPI.m */; };
Expand Down Expand Up @@ -65,6 +66,8 @@
5758EE5127864A8500B3B703 /* RCStoreProductAPI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCStoreProductAPI.m; sourceTree = "<group>"; };
5758EE592786548D00B3B703 /* RCStoreTransactionAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCStoreTransactionAPI.h; sourceTree = "<group>"; };
5758EE5A2786548D00B3B703 /* RCStoreTransactionAPI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCStoreTransactionAPI.m; sourceTree = "<group>"; };
57918A1428F4C58300BF4963 /* RCSDKTesterAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCSDKTesterAPI.h; sourceTree = "<group>"; };
57918A1528F4C58300BF4963 /* RCSDKTesterAPI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCSDKTesterAPI.m; sourceTree = "<group>"; };
57DE807628074E59008D6C6F /* RCStorefrontAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCStorefrontAPI.h; sourceTree = "<group>"; };
57DE807728074E59008D6C6F /* RCStorefrontAPI.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RCStorefrontAPI.m; sourceTree = "<group>"; };
A513AD2F272B327A00E0C1BA /* RCRefundRequestStatusAPI.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RCRefundRequestStatusAPI.h; sourceTree = "<group>"; };
Expand Down Expand Up @@ -164,6 +167,8 @@
A5D614EE26EBE84F007DDB75 /* RCPurchasesErrorCodeAPI.m */,
A513AD2F272B327A00E0C1BA /* RCRefundRequestStatusAPI.h */,
A513AD30272B327A00E0C1BA /* RCRefundRequestStatusAPI.m */,
57918A1428F4C58300BF4963 /* RCSDKTesterAPI.h */,
57918A1528F4C58300BF4963 /* RCSDKTesterAPI.m */,
57DE807628074E59008D6C6F /* RCStorefrontAPI.h */,
57DE807728074E59008D6C6F /* RCStorefrontAPI.m */,
5758EE5027864A8500B3B703 /* RCStoreProductAPI.h */,
Expand Down Expand Up @@ -279,6 +284,7 @@
2DD7790E270E23870079CBD4 /* RCIntroEligibilityAPI.m in Sources */,
5738F42D278674710096D623 /* RCSubscriptionPeriodAPI.m in Sources */,
2DD77916270E23870079CBD4 /* RCTransactionAPI.m in Sources */,
57918A1628F4C58300BF4963 /* RCSDKTesterAPI.m in Sources */,
570FAF502864ECB000D3C769 /* RCNonSubscriptionTransactionAPI.m in Sources */,
2DD7790F270E23870079CBD4 /* RCPurchasesAPI.m in Sources */,
B378153D2857A750000A7B93 /* RCAttributionAPI.m in Sources */,
Expand Down
Loading

0 comments on commit 02f7a98

Please sign in to comment.