diff --git a/Networking/NetworkingTests/Mapper/InAppPurchasesProductsMapperTests.swift b/Networking/NetworkingTests/Mapper/InAppPurchasesProductsMapperTests.swift index b1ae97d65a7..fc616f6c5ab 100644 --- a/Networking/NetworkingTests/Mapper/InAppPurchasesProductsMapperTests.swift +++ b/Networking/NetworkingTests/Mapper/InAppPurchasesProductsMapperTests.swift @@ -6,8 +6,8 @@ final class InAppPurchasesProductsMapperTests: XCTestCase { // Given let jsonData = try XCTUnwrap(Loader.contentsOf("iap-products")) let expectedProductIdentifiers = [ - "woocommerce_entry_monthly", - "woocommerce_entry_yearly"] + "debug.woocommerce.ecommerce.monthly" + ] // When let products = try InAppPurchasesProductMapper().map(response: jsonData) diff --git a/Networking/NetworkingTests/Remote/InAppPurchasesRemoteTests.swift b/Networking/NetworkingTests/Remote/InAppPurchasesRemoteTests.swift index 06c4c16881b..39ea2dfd124 100644 --- a/Networking/NetworkingTests/Remote/InAppPurchasesRemoteTests.swift +++ b/Networking/NetworkingTests/Remote/InAppPurchasesRemoteTests.swift @@ -44,7 +44,7 @@ class InAppPurchasesRemoteTests: XCTestCase { // Then let identifiers = try XCTUnwrap(result?.get()) - XCTAssert(identifiers.count == 2) + XCTAssert(identifiers.count == 1) } func test_purchase_product_returns_created_order() throws { diff --git a/Networking/NetworkingTests/Responses/iap-products.json b/Networking/NetworkingTests/Responses/iap-products.json index 96982014360..808cc10854f 100644 --- a/Networking/NetworkingTests/Responses/iap-products.json +++ b/Networking/NetworkingTests/Responses/iap-products.json @@ -1,4 +1,3 @@ [ - "woocommerce_entry_monthly", - "woocommerce_entry_yearly" + "debug.woocommerce.ecommerce.monthly" ] diff --git a/WooCommerce/Resources/WooCommerceTest.storekit b/WooCommerce/Resources/WooCommerceTest.storekit new file mode 100644 index 00000000000..989c7a52d10 --- /dev/null +++ b/WooCommerce/Resources/WooCommerceTest.storekit @@ -0,0 +1,52 @@ +{ + "identifier" : "28D2E099", + "nonRenewingSubscriptions" : [ + + ], + "products" : [ + + ], + "settings" : { + + }, + "subscriptionGroups" : [ + { + "id" : "21032762", + "localizations" : [ + + ], + "name" : "test_subscription_group", + "subscriptions" : [ + { + "adHocOffers" : [ + + ], + "codeOffers" : [ + + ], + "displayPrice" : "69.99", + "familyShareable" : false, + "groupNumber" : 1, + "internalID" : "1650562345", + "introductoryOffer" : null, + "localizations" : [ + { + "description" : "1 Month of Debug Woo", + "displayName" : "Debug Monthly", + "locale" : "en_US" + } + ], + "productID" : "debug.woocommerce.ecommerce.monthly", + "recurringSubscriptionPeriod" : "P1M", + "referenceName" : "Debug Monthly", + "subscriptionGroupID" : "21032762", + "type" : "RecurringSubscription" + } + ] + } + ], + "version" : { + "major" : 2, + "minor" : 0 + } +} diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index df86aa9a927..439f0f23276 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -1827,8 +1827,10 @@ E16715CD2666543000326230 /* CardPresentModalSuccessWithoutEmailTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E16715CC2666543000326230 /* CardPresentModalSuccessWithoutEmailTests.swift */; }; E17E3BF9266917C10009D977 /* CardPresentModalScanningFailedTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17E3BF8266917C10009D977 /* CardPresentModalScanningFailedTests.swift */; }; E17E3BFB266917E20009D977 /* CardPresentModalBluetoothRequiredTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E17E3BFA266917E20009D977 /* CardPresentModalBluetoothRequiredTests.swift */; }; + E181CDCC291BB2E1002DA3C6 /* InAppPurchaseStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E181CDCB291BB2E1002DA3C6 /* InAppPurchaseStoreTests.swift */; }; E1906E9A26C4126300CA6819 /* InPersonPaymentsMenuViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1906E9926C4126300CA6819 /* InPersonPaymentsMenuViewController.swift */; }; E1ABAEF728479E0300F40BB2 /* InPersonPaymentsSelectPluginView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1ABAEF628479E0300F40BB2 /* InPersonPaymentsSelectPluginView.swift */; }; + E1B0839B291BC5E3001D99C8 /* WooCommerceTest.storekit in Resources */ = {isa = PBXBuildFile; fileRef = E1B0839A291BC5DD001D99C8 /* WooCommerceTest.storekit */; }; E1BAAEA026BBECEF00F2C037 /* ButtonStyles.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BAAE9F26BBECEF00F2C037 /* ButtonStyles.swift */; }; E1BE703A265E6F47006CA4D9 /* CardPresentModalScanningFailed.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1BE7039265E6F47006CA4D9 /* CardPresentModalScanningFailed.swift */; }; E1C47209267A1ECC00D06DA1 /* CrashLoggingStack.swift in Sources */ = {isa = PBXBuildFile; fileRef = E1C47208267A1ECC00D06DA1 /* CrashLoggingStack.swift */; }; @@ -3797,8 +3799,10 @@ E16715CC2666543000326230 /* CardPresentModalSuccessWithoutEmailTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalSuccessWithoutEmailTests.swift; sourceTree = ""; }; E17E3BF8266917C10009D977 /* CardPresentModalScanningFailedTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalScanningFailedTests.swift; sourceTree = ""; }; E17E3BFA266917E20009D977 /* CardPresentModalBluetoothRequiredTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalBluetoothRequiredTests.swift; sourceTree = ""; }; + E181CDCB291BB2E1002DA3C6 /* InAppPurchaseStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InAppPurchaseStoreTests.swift; sourceTree = ""; }; E1906E9926C4126300CA6819 /* InPersonPaymentsMenuViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsMenuViewController.swift; sourceTree = ""; }; E1ABAEF628479E0300F40BB2 /* InPersonPaymentsSelectPluginView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InPersonPaymentsSelectPluginView.swift; sourceTree = ""; }; + E1B0839A291BC5DD001D99C8 /* WooCommerceTest.storekit */ = {isa = PBXFileReference; lastKnownFileType = text; path = WooCommerceTest.storekit; sourceTree = ""; }; E1BAAE9F26BBECEF00F2C037 /* ButtonStyles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ButtonStyles.swift; sourceTree = ""; }; E1BE7039265E6F47006CA4D9 /* CardPresentModalScanningFailed.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardPresentModalScanningFailed.swift; sourceTree = ""; }; E1C47208267A1ECC00D06DA1 /* CrashLoggingStack.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CrashLoggingStack.swift; sourceTree = ""; }; @@ -6989,6 +6993,7 @@ B573B19D219DC2690081C78C /* Localizable.strings */, 3F587028281B9C19004F7556 /* InfoPlist.strings */, B56DB3D82049BFAA00D4AA8E /* Info.plist */, + E1B0839A291BC5DD001D99C8 /* WooCommerceTest.storekit */, B56C721921B5F65E00E5E85B /* Woo-Debug.entitlements */, 03180BE72763AA9000B938A8 /* Woo-Debug-macOS.entitlements */, B56C721A21B5F65E00E5E85B /* Woo-Release.entitlements */, @@ -7225,6 +7230,7 @@ isa = PBXGroup; children = ( B5DBF3C220E1484400B53AED /* StoresManagerTests.swift */, + E181CDCB291BB2E1002DA3C6 /* InAppPurchaseStoreTests.swift */, ); path = Yosemite; sourceTree = ""; @@ -9318,6 +9324,7 @@ 9379E1A5225536AD006A6BE4 /* TestAssets.xcassets in Resources */, 9379E1A32255365F006A6BE4 /* TestingMode.storyboard in Resources */, B5F571A921BEECA50010D1B8 /* Responses in Resources */, + E1B0839B291BC5E3001D99C8 /* WooCommerceTest.storekit in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -10851,6 +10858,7 @@ 0271125D2887D4E900FCD13C /* LoggedOutAppSettingsTests.swift in Sources */, 74F3015A2200EC0800931B9E /* NSDecimalNumberWooTests.swift in Sources */, D85136CD231E15B800DD0539 /* MockReviews.swift in Sources */, + E181CDCC291BB2E1002DA3C6 /* InAppPurchaseStoreTests.swift in Sources */, 2655905B27863D1300BB8457 /* MockCollectOrderPaymentUseCase.swift in Sources */, D8053BCE231F98DA00CE60C2 /* ReviewAgeTests.swift in Sources */, A650BE862578E76600C655E0 /* MockStorageManager+Sample.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/Tools/DefaultConnectivityObserver.swift b/WooCommerce/WooCommerceTests/Tools/DefaultConnectivityObserver.swift index 8af3f64fbaa..f1e71c41db9 100644 --- a/WooCommerce/WooCommerceTests/Tools/DefaultConnectivityObserver.swift +++ b/WooCommerce/WooCommerceTests/Tools/DefaultConnectivityObserver.swift @@ -97,7 +97,7 @@ final class MockNetworkMonitor: NetworkMonitoring { } } -struct MockNetwork: NetworkMonitorable { +private struct MockNetwork: NetworkMonitorable { let status: NWPath.Status private let currentInterface: NWInterface.InterfaceType diff --git a/WooCommerce/WooCommerceTests/Yosemite/InAppPurchaseStoreTests.swift b/WooCommerce/WooCommerceTests/Yosemite/InAppPurchaseStoreTests.swift new file mode 100644 index 00000000000..a08b98033d1 --- /dev/null +++ b/WooCommerce/WooCommerceTests/Yosemite/InAppPurchaseStoreTests.swift @@ -0,0 +1,210 @@ +import XCTest +import TestKit +import StoreKitTest + +@testable import Yosemite +@testable import Networking + +final class InAppPurchaseStoreTests: XCTestCase { + + /// Mock Network: Allows us to inject predefined responses! + /// + private var network: MockNetwork! + + /// Mock Storage: InMemory + /// + private var storageManager: MockStorageManager! + + private var storeKitSession = try! SKTestSession(configurationFileNamed: "WooCommerceTest") + + /// Testing SiteID + /// + private let sampleSiteID: Int64 = 123 + + /// Testing Product ID + /// Should match the product ID in WooCommerce.storekit + /// + private let sampleProductID: String = "debug.woocommerce.ecommerce.monthly" + + /// Testing Order ID + /// Should match the order ID in iap-order-create.json + /// + private let sampleOrderID: Int64 = 12345 + + var store: InAppPurchaseStore! + + + override func setUp() { + network = MockNetwork(useResponseQueue: true) + storageManager = MockStorageManager() + store = InAppPurchaseStore(dispatcher: Dispatcher(), storageManager: storageManager, network: network) + storeKitSession.disableDialogs = true + } + + override func tearDown() { + storeKitSession.resetToDefaultState() + storeKitSession.clearTransactions() + } + + func test_iap_supported_in_us() throws { + // Given + storeKitSession.storefront = "USA" + + // When + let result = waitFor { promise in + let action = InAppPurchaseAction.inAppPurchasesAreSupported { result in + promise(result) + } + self.store.onAction(action) + } + + // Then + XCTAssertTrue(result) + } + + func test_iap_supported_in_canada() throws { + // Given + storeKitSession.storefront = "CAN" + + // When + let result = waitFor { promise in + let action = InAppPurchaseAction.inAppPurchasesAreSupported { result in + promise(result) + } + self.store.onAction(action) + } + + // Then + XCTAssertFalse(result) + } + + func test_load_products_loads_products_response() throws { + // Given + network.simulateResponse(requestUrlSuffix: "iap/products", filename: "iap-products") + + // When + let result = waitFor { promise in + let action = InAppPurchaseAction.loadProducts { result in + promise(result) + } + self.store.onAction(action) + } + + // Then + let products = try XCTUnwrap(result.get()) + XCTAssertFalse(products.isEmpty) + XCTAssertEqual(products.first?.id, sampleProductID) + } + + func test_load_products_fails_if_iap_unsupported() throws { + // Given + storeKitSession.storefront = "CAN" + network.simulateResponse(requestUrlSuffix: "iap/products", filename: "iap-products") + + // When + let result = waitFor { promise in + let action = InAppPurchaseAction.loadProducts { result in + promise(result) + } + self.store.onAction(action) + } + + // Then + XCTAssert(result.isFailure) + } + + func test_purchase_product_completes_purchase() throws { + // Given + network.simulateResponse(requestUrlSuffix: "iap/orders", filename: "iap-order-create") + + // When + let result = waitFor { promise in + let action = InAppPurchaseAction.purchaseProduct(siteID: self.sampleSiteID, productID: self.sampleProductID) { result in + promise(result) + } + self.store.onAction(action) + } + + // Then + let purchaseResult = try XCTUnwrap(result.get()) + guard case let .success(verificationResult) = purchaseResult, + case let .verified(transaction) = verificationResult else { + return XCTFail() + } + XCTAssertEqual(transaction.productID, sampleProductID) + XCTAssertNotNil(transaction.appAccountToken) + } + + @available(iOS 16.0, *) + func test_purchase_product_ensure_xcode_environment() throws { + // Given + network.simulateResponse(requestUrlSuffix: "iap/orders", filename: "iap-order-create") + + // When + let result = waitFor { promise in + let action = InAppPurchaseAction.purchaseProduct(siteID: self.sampleSiteID, productID: self.sampleProductID) { result in + promise(result) + } + self.store.onAction(action) + } + + // Then + let purchaseResult = try XCTUnwrap(result.get()) + guard case let .success(verificationResult) = purchaseResult, + case let .verified(transaction) = verificationResult else { + return XCTFail() + } + XCTAssertEqual(transaction.environment, .xcode) + } + + func test_purchase_product_handles_api_errors() throws { + // Given + network.simulateResponse(requestUrlSuffix: "iap/orders", filename: "error-wp-rest-forbidden") + + // When + let result = waitFor { promise in + let action = InAppPurchaseAction.purchaseProduct(siteID: self.sampleSiteID, productID: self.sampleProductID) { result in + promise(result) + } + self.store.onAction(action) + } + + // Then + XCTAssert(result.isFailure) + let error = try XCTUnwrap(result.failure) + XCTAssert(error is WordPressApiError) + } + + func test_user_is_entitled_to_product_returns_false_when_not_entitled() throws { + // Given + + // When + let result = waitFor { promise in + let action = InAppPurchaseAction.userIsEntitledToProduct(productID: self.sampleProductID) { result in + promise(result) + } + self.store.onAction(action) + } + + // Then + let isEntitled = try XCTUnwrap(result.get()) + XCTAssertFalse(isEntitled) + } + + func test_user_is_entitled_to_product_returns_true_when_entitled() throws { + // Given + try storeKitSession.buyProduct(productIdentifier: sampleProductID) + + // When + let result = waitFor { promise in + let action = InAppPurchaseAction.userIsEntitledToProduct(productID: self.sampleProductID) { result in + promise(result) + } + self.store.onAction(action) + } + + // Then + let isEntitled = try XCTUnwrap(result.get()) + XCTAssertTrue(isEntitled) + } +}