Skip to content

Commit

Permalink
Improved error handling and added tests
Browse files Browse the repository at this point in the history
  • Loading branch information
NachoSoto committed Dec 9, 2022
1 parent 09c1d91 commit a36c1b6
Show file tree
Hide file tree
Showing 5 changed files with 95 additions and 17 deletions.
2 changes: 2 additions & 0 deletions Sources/Error Handling/ErrorUtils.swift
Original file line number Diff line number Diff line change
Expand Up @@ -408,9 +408,11 @@ enum ErrorUtils {
*/
static func invalidPromotionalOfferError(
error: Error? = nil,
message: String? = nil,
fileName: String = #fileID, functionName: String = #function, line: UInt = #line
) -> PurchasesError {
return ErrorUtils.error(with: .invalidPromotionalOfferError,
message: message,
underlyingError: error)
}

Expand Down
5 changes: 4 additions & 1 deletion Sources/Purchasing/Purchases/PurchasesOrchestrator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ final class PurchasesOrchestrator {
Logger.debug(
Strings.storeKit.sk2_purchasing_added_promotional_offer_option(signedData.identifier)
)
options.insert(signedData.sk2PurchaseOption)
options.insert(try signedData.sk2PurchaseOption)
}

return try await sk2Product.purchase(options: options)
Expand All @@ -446,6 +446,9 @@ final class PurchasesOrchestrator {
fetchPolicy: .cachedOrFetched),
userCancelled: true
)
} catch let error as PromotionalOffer.SignedData.Error {
throw ErrorUtils.invalidPromotionalOfferError(error: error,
message: error.localizedDescription)
} catch {
throw ErrorUtils.purchasesError(withStoreKitError: error)
}
Expand Down
49 changes: 35 additions & 14 deletions Sources/Purchasing/StoreKitAbstractions/PromotionalOffer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,13 @@ extension PromotionalOffer.SignedData: @unchecked Sendable {}

extension PromotionalOffer.SignedData {

enum Error: Swift.Error {

/// The signature generated by the backend could not be decoded.
case failedToDecodeSignature(String)

}

@available(iOS 12.2, macOS 10.14.4, watchOS 6.2, macCatalyst 13.0, tvOS 12.2, *)
convenience init(sk1PaymentDiscount discount: SKPaymentDiscount) {
self.init(identifier: discount.identifier,
Expand All @@ -122,22 +129,36 @@ extension PromotionalOffer.SignedData {

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
var sk2PurchaseOption: Product.PurchaseOption {
let signature: Data

if let decoded = Data(base64Encoded: self.signature) {
signature = decoded
} else {
// TODO: log warning
signature = .init()
get throws {
let signature: Data

if let decoded = Data(base64Encoded: self.signature) {
signature = decoded
} else {
throw Error.failedToDecodeSignature(self.signature)
}

return .promotionalOffer(
offerID: self.identifier,
keyID: self.keyIdentifier,
nonce: self.nonce,
signature: signature,
timestamp: self.timestamp
)
}
}

return .promotionalOffer(
offerID: self.identifier,
keyID: self.keyIdentifier,
nonce: self.nonce,
signature: signature,
timestamp: self.timestamp
)
}

// MARK: -

extension PromotionalOffer.SignedData.Error: LocalizedError {

var errorDescription: String? {
switch self {
case let .failedToDecodeSignature(signature):
return "The signature generated by RevenueCat could not be decoded: \(signature)"
}
}

}
27 changes: 27 additions & 0 deletions Tests/StoreKitUnitTests/PurchasesOrchestratorTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,33 @@ class PurchasesOrchestratorTests: StoreKitConfigTestCase {
}
}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func testPurchaseSK2PackageWithInvalidPromotionalOfferSignatureThrowsError() async throws {
try AvailabilityChecks.iOS15APIAvailableOrSkipTest()

self.customerInfoManager.stubbedCachedCustomerInfoResult = self.mockCustomerInfo
self.backend.stubbedPostReceiptResult = .success(self.mockCustomerInfo)

let product = try await self.fetchSk2Product()
let offer = PromotionalOffer.SignedData(
identifier: "identifier \(Int.random(in: 0..<1000))",
keyIdentifier: "key identifier \(Int.random(in: 0..<1000))",
nonce: .init(),
// This should be base64
signature: "signature \(Int.random(in: 0..<1000))",
timestamp: Int.random(in: 0..<1000)
)

do {
_ = try await orchestrator.purchase(sk2Product: product, promotionalOffer: offer)
XCTFail("Expected error")
} catch {
expect(error).to(matchError(ErrorCode.invalidPromotionalOfferError))
expect(error.localizedDescription)
.to(contain("The signature generated by RevenueCat could not be decoded: \(offer.signature)"))
}
}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func testPurchaseSK2PackageReturnsCustomerInfoForFailedTransaction() async throws {
try AvailabilityChecks.iOS15APIAvailableOrSkipTest()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,25 +32,50 @@ class PromotionalOfferTests: TestCase {
func testSK2PurchaseOption() throws {
try AvailabilityChecks.iOS15APIAvailableOrSkipTest()

let option = Self.randomOffer.sk2PurchaseOption
let option = try Self.randomOffer.sk2PurchaseOption
let expected: Product.PurchaseOption = .promotionalOffer(
offerID: Self.randomOffer.identifier,
keyID: Self.randomOffer.keyIdentifier,
nonce: Self.randomOffer.nonce,
signature: Self.randomOffer.signature.asData,
// `Product.PurchaseOption` conforms to Equatable but it does not compare this
// The only way to validate this is correct is integration tests.
signature: Data(),
timestamp: Self.randomOffer.timestamp
)

expect(option) == expected
}

@available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)
func testSK2PurchaseOptionWithInvalidSignatureThrows() throws {
try AvailabilityChecks.iOS15APIAvailableOrSkipTest()

do {
_ = try Self.invalidOffer.sk2PurchaseOption
fail("Expected error")
} catch {
expect(error).to(matchError(
PromotionalOffer.SignedData.Error
.failedToDecodeSignature(Self.invalidOffer.signature)
))
}
}

}

// MARK: - Private

private extension PromotionalOfferTests {

static let randomOffer: PromotionalOffer.SignedData = .init(
identifier: "identifier \(Int.random(in: 0..<1000))",
keyIdentifier: "key identifier \(Int.random(in: 0..<1000))",
nonce: .init(),
signature: "signature \(Int.random(in: 0..<1000))".asData.base64EncodedString(),
timestamp: Int.random(in: 0..<1000)
)

static let invalidOffer: PromotionalOffer.SignedData = .init(
identifier: "identifier \(Int.random(in: 0..<1000))",
keyIdentifier: "key identifier \(Int.random(in: 0..<1000))",
nonce: .init(),
Expand Down

0 comments on commit a36c1b6

Please sign in to comment.