From c799d67d2e8c1a13aa53a622098fc711683a4a57 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Tue, 6 Jun 2023 09:05:12 -0700 Subject: [PATCH 1/5] `StoreKitObserverModeIntegrationTests`: added test for posting renewals --- .../BaseStoreKitIntegrationTests.swift | 6 ++++-- ...StoreKitObserverModeIntegrationTests.swift | 21 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/Tests/BackendIntegrationTests/BaseStoreKitIntegrationTests.swift b/Tests/BackendIntegrationTests/BaseStoreKitIntegrationTests.swift index c1e0e8b4d9..8e78540c9c 100644 --- a/Tests/BackendIntegrationTests/BaseStoreKitIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/BaseStoreKitIntegrationTests.swift @@ -315,8 +315,10 @@ extension BaseStoreKitIntegrationTests { /// Purchases a product directly with StoreKit. @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) @discardableResult - func purchaseProductFromStoreKit() async throws -> Product.PurchaseResult { - let products = try await StoreKit.Product.products(for: [Self.monthlyNoIntroProductID]) + func purchaseProductFromStoreKit( + productIdentifier: String = BaseStoreKitIntegrationTests.monthlyNoIntroProductID + ) async throws -> Product.PurchaseResult { + let products = try await StoreKit.Product.products(for: [productIdentifier]) let product = try XCTUnwrap(products.onlyElement) return try await product.purchase() diff --git a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift index 730d08ed5a..a6720ae557 100644 --- a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift @@ -59,6 +59,27 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes ) } + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + func testRenewalsPostReceipt() async throws { + let productID = Self.monthlyNoIntroProductID + + try await self.purchaseProductFromStoreKit(productIdentifier: productID) + try self.testSession.forceRenewalOfSubscription(productIdentifier: productID) + + try await asyncWait( + until: { + let entitlement = await self.purchasesDelegate + .customerInfo? + .entitlements[Self.entitlementIdentifier] + + return entitlement?.isActive == true + }, + timeout: .seconds(10), + pollInterval: .milliseconds(500), + description: "Entitlement didn't become active" + ) + } + } class StoreKit1ObserverModeIntegrationTests: BaseStoreKitObserverModeIntegrationTests { From f163a555fd5fa4ea8fb84dff4911d8a7a482bd31 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Tue, 6 Jun 2023 18:06:17 -0700 Subject: [PATCH 2/5] Fixed test? --- .../StoreKitObserverModeIntegrationTests.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift index a6720ae557..7620fb3875 100644 --- a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift @@ -64,6 +64,8 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes let productID = Self.monthlyNoIntroProductID try await self.purchaseProductFromStoreKit(productIdentifier: productID) + + self.testSession.timeRate = .realTime try self.testSession.forceRenewalOfSubscription(productIdentifier: productID) try await asyncWait( @@ -74,8 +76,8 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes return entitlement?.isActive == true }, - timeout: .seconds(10), - pollInterval: .milliseconds(500), + timeout: .seconds(5), + pollInterval: .milliseconds(100), description: "Entitlement didn't become active" ) } From 67dcb00027ac21269ba06fba8d2de15e0b744f5f Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Wed, 7 Jun 2023 09:24:49 -0700 Subject: [PATCH 3/5] Testing --- .../StoreKitObserverModeIntegrationTests.swift | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift index 7620fb3875..266ff16096 100644 --- a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift @@ -66,7 +66,11 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes try await self.purchaseProductFromStoreKit(productIdentifier: productID) self.testSession.timeRate = .realTime - try self.testSession.forceRenewalOfSubscription(productIdentifier: productID) + do { + try self.testSession.forceRenewalOfSubscription(productIdentifier: productID) + } catch let error as NSError { + Logger.appleError("\(error.description) \(error.localizedDescription) \(error.userInfo)") + } try await asyncWait( until: { From e7bd6e385d2dba0c55af22267b0f8958b30ea736 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Mon, 12 Jun 2023 09:17:24 -0700 Subject: [PATCH 4/5] Finish transaction --- .../StoreKitObserverModeIntegrationTests.swift | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift index 266ff16096..df5b98f084 100644 --- a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift @@ -63,14 +63,11 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes func testRenewalsPostReceipt() async throws { let productID = Self.monthlyNoIntroProductID - try await self.purchaseProductFromStoreKit(productIdentifier: productID) + let result = try await self.purchaseProductFromStoreKit(productIdentifier: productID) + let transaction = try XCTUnwrap(result.verificationResult?.underlyingTransaction) + await transaction.finish() - self.testSession.timeRate = .realTime - do { - try self.testSession.forceRenewalOfSubscription(productIdentifier: productID) - } catch let error as NSError { - Logger.appleError("\(error.description) \(error.localizedDescription) \(error.userInfo)") - } + try? self.testSession.forceRenewalOfSubscription(productIdentifier: productID) try await asyncWait( until: { From 8b07e55db32cc21774ed13a62dcf71e1c994e933 Mon Sep 17 00:00:00 2001 From: NachoSoto Date: Mon, 12 Jun 2023 15:51:32 -0700 Subject: [PATCH 5/5] Updated tests --- Sources/Logging/Strings/PurchaseStrings.swift | 4 ++ .../Purchases/TransactionPoster.swift | 4 ++ .../BaseStoreKitIntegrationTests.swift | 11 ++++- ...StoreKitObserverModeIntegrationTests.swift | 47 +++++++++++++------ 4 files changed, 50 insertions(+), 16 deletions(-) diff --git a/Sources/Logging/Strings/PurchaseStrings.swift b/Sources/Logging/Strings/PurchaseStrings.swift index 7a30aa5b63..bcd1aeac35 100644 --- a/Sources/Logging/Strings/PurchaseStrings.swift +++ b/Sources/Logging/Strings/PurchaseStrings.swift @@ -75,6 +75,7 @@ enum PurchaseStrings { case begin_refund_customer_info_error(entitlementID: String?) case missing_cached_customer_info case sk2_transactions_update_received_transaction(productID: String) + case transaction_poster_handling_transaction(productID: String) case sk1_purchase_too_slow case sk2_purchase_too_slow case payment_queue_wrapper_delegate_call_sk1_enabled @@ -285,6 +286,9 @@ extension PurchaseStrings: CustomStringConvertible { case let .sk2_transactions_update_received_transaction(productID): return "StoreKit.Transaction.updates: received transaction for product '\(productID)'" + case let .transaction_poster_handling_transaction(productID): + return "TransactionPoster: handling transaction for product '\(productID)'" + case .sk1_purchase_too_slow: return "StoreKit 1 purchase took longer than expected" diff --git a/Sources/Purchasing/Purchases/TransactionPoster.swift b/Sources/Purchasing/Purchases/TransactionPoster.swift index 9c7af23684..a0755e8950 100644 --- a/Sources/Purchasing/Purchases/TransactionPoster.swift +++ b/Sources/Purchasing/Purchases/TransactionPoster.swift @@ -81,6 +81,10 @@ final class TransactionPoster: TransactionPosterType { func handlePurchasedTransaction(_ transaction: StoreTransactionType, data: PurchasedTransactionData, completion: @escaping CustomerAPI.CustomerInfoResponseHandler) { + Logger.debug(Strings.purchase.transaction_poster_handling_transaction( + productID: transaction.productIdentifier + )) + self.receiptFetcher.receiptData( refreshPolicy: self.refreshRequestPolicy(forProductIdentifier: transaction.productIdentifier) ) { receiptData, receiptURL in diff --git a/Tests/BackendIntegrationTests/BaseStoreKitIntegrationTests.swift b/Tests/BackendIntegrationTests/BaseStoreKitIntegrationTests.swift index 8e78540c9c..b9a0be7098 100644 --- a/Tests/BackendIntegrationTests/BaseStoreKitIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/BaseStoreKitIntegrationTests.swift @@ -316,12 +316,19 @@ extension BaseStoreKitIntegrationTests { @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) @discardableResult func purchaseProductFromStoreKit( - productIdentifier: String = BaseStoreKitIntegrationTests.monthlyNoIntroProductID + productIdentifier: String = BaseStoreKitIntegrationTests.monthlyNoIntroProductID, + finishTransaction: Bool = false ) async throws -> Product.PurchaseResult { let products = try await StoreKit.Product.products(for: [productIdentifier]) let product = try XCTUnwrap(products.onlyElement) - return try await product.purchase() + let result = try await product.purchase() + + if finishTransaction { + await result.verificationResult?.underlyingTransaction.finish() + } + + return result } } diff --git a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift index df5b98f084..114859f0de 100644 --- a/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift +++ b/Tests/BackendIntegrationTests/StoreKitObserverModeIntegrationTests.swift @@ -61,25 +61,20 @@ class StoreKit2ObserverModeIntegrationTests: StoreKit1ObserverModeIntegrationTes @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) func testRenewalsPostReceipt() async throws { + self.testSession.timeRate = .realTime + let productID = Self.monthlyNoIntroProductID - let result = try await self.purchaseProductFromStoreKit(productIdentifier: productID) - let transaction = try XCTUnwrap(result.verificationResult?.underlyingTransaction) - await transaction.finish() + try await self.purchaseProductFromStoreKit(productIdentifier: productID, finishTransaction: true) - try? self.testSession.forceRenewalOfSubscription(productIdentifier: productID) + let logger = TestLogHandler() - try await asyncWait( - until: { - let entitlement = await self.purchasesDelegate - .customerInfo? - .entitlements[Self.entitlementIdentifier] + try self.testSession.forceRenewalOfSubscription(productIdentifier: productID) - return entitlement?.isActive == true - }, - timeout: .seconds(5), - pollInterval: .milliseconds(100), - description: "Entitlement didn't become active" + try await logger.verifyMessageIsEventuallyLogged( + Strings.network.operation_state(PostReceiptDataOperation.self, state: "Finished").description, + timeout: .seconds(3), + pollInterval: .milliseconds(100) ) } @@ -96,6 +91,30 @@ class StoreKit1ObserverModeIntegrationTests: BaseStoreKitObserverModeIntegration try await self.verifyEntitlementWentThrough(info) } + @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *) + func testSK2RenewalsPostReceiptOnlyOnceWhenSK1IsEnabled() async throws { + try XCTSkipIf(Self.storeKit2Setting.isEnabledAndAvailable, "Test only for SK1") + + // `StoreKit2TransactionListener` is always enabled even in SK1 mode. + // This test ensures that we don't end up posting receipts multiple times when renewals come through. + + self.testSession.timeRate = .realTime + + let productID = Self.monthlyNoIntroProductID + + try await self.purchaseProductFromStoreKit(productIdentifier: productID, finishTransaction: true) + + let logger = TestLogHandler() + + try? self.testSession.forceRenewalOfSubscription(productIdentifier: productID) + + try await logger.verifyMessageIsEventuallyLogged( + "Network operation 'PostReceiptDataOperation' found with the same cache key", + timeout: .seconds(4), + pollInterval: .milliseconds(100) + ) + } + } @available(iOS 15.0, tvOS 15.0, watchOS 8.0, macOS 12.0, *)