From 03ebef5b8d2f09603734d925e956a22113f93866 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 27 Aug 2024 11:58:38 +0200 Subject: [PATCH 01/17] Tests for StorePurchaseManager --- DuckDuckGo.xcodeproj/project.pbxproj | 8 +- .../StorePurchaseManagerTests.swift | 171 ++++++++++++++++++ .../Subscription/Subscription.storekit | 0 3 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift rename {DuckDuckGo => DuckDuckGoTests}/Subscription/Subscription.storekit (100%) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index b6ffc9bca9..3676c93753 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -64,6 +64,8 @@ 1E4DCF4A27B6A38000961E25 /* DownloadListRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4DCF4927B6A38000961E25 /* DownloadListRepresentable.swift */; }; 1E4DCF4C27B6A4CB00961E25 /* URLFileExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4DCF4B27B6A4CB00961E25 /* URLFileExtension.swift */; }; 1E4DCF4E27B6A69600961E25 /* DownloadsListHostingController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4DCF4D27B6A69600961E25 /* DownloadsListHostingController.swift */; }; + 1E4E6C552C775B400059C0FA /* Subscription.storekit in Resources */ = {isa = PBXBuildFile; fileRef = D664C7952B289AA000CBFA76 /* Subscription.storekit */; }; + 1E4E6C572C78B8540059C0FA /* StorePurchaseManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4E6C562C78B8540059C0FA /* StorePurchaseManagerTests.swift */; }; 1E4F4A5A297193DE00625985 /* MainViewController+CookiesManaged.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4F4A59297193DE00625985 /* MainViewController+CookiesManaged.swift */; }; 1E4FAA6427D8DFB900ADC5B3 /* OngoingDownloadRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4FAA6327D8DFB900ADC5B3 /* OngoingDownloadRowViewModel.swift */; }; 1E4FAA6627D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4FAA6527D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift */; }; @@ -1301,6 +1303,7 @@ 1E4DCF4927B6A38000961E25 /* DownloadListRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadListRepresentable.swift; sourceTree = ""; }; 1E4DCF4B27B6A4CB00961E25 /* URLFileExtension.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = URLFileExtension.swift; sourceTree = ""; }; 1E4DCF4D27B6A69600961E25 /* DownloadsListHostingController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsListHostingController.swift; sourceTree = ""; }; + 1E4E6C562C78B8540059C0FA /* StorePurchaseManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StorePurchaseManagerTests.swift; sourceTree = ""; }; 1E4F4A59297193DE00625985 /* MainViewController+CookiesManaged.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MainViewController+CookiesManaged.swift"; sourceTree = ""; }; 1E4FAA6327D8DFB900ADC5B3 /* OngoingDownloadRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OngoingDownloadRowViewModel.swift; sourceTree = ""; }; 1E4FAA6527D8DFC800ADC5B3 /* CompleteDownloadRowViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CompleteDownloadRowViewModel.swift; sourceTree = ""; }; @@ -5050,7 +5053,6 @@ F1FDC92F2BF4E0B3006B1435 /* SubscriptionEnvironment+Default.swift */, D60170BB2BA32DD6001911B5 /* Subscription.swift */, D6D95CE42B6DA3F200960317 /* AsyncHeadlessWebview */, - D664C7952B289AA000CBFA76 /* Subscription.storekit */, D664C7932B289AA000CBFA76 /* ViewModel */, D664C7AC2B289AA000CBFA76 /* Views */, D664C7B02B289AA000CBFA76 /* UserScripts */, @@ -5818,10 +5820,12 @@ F1BDDBFC2C340D9C00459306 /* Subscription */ = { isa = PBXGroup; children = ( + D664C7952B289AA000CBFA76 /* Subscription.storekit */, BDE219E92C457B46005D5884 /* PrivacyProDataReporterTests.swift */, F1BDDBF92C340D9C00459306 /* SubscriptionContainerViewModelTests.swift */, F1BDDBFA2C340D9C00459306 /* SubscriptionFlowViewModelTests.swift */, F1BDDBFB2C340D9C00459306 /* SubscriptionPagesUseSubscriptionFeatureTests.swift */, + 1E4E6C562C78B8540059C0FA /* StorePurchaseManagerTests.swift */, ); path = Subscription; sourceTree = ""; @@ -6747,6 +6751,7 @@ buildActionMask = 2147483647; files = ( EA39B7E2268A1A35000C62CD /* privacy-reference-tests in Resources */, + 1E4E6C552C775B400059C0FA /* Subscription.storekit in Resources */, F17843E91F36226700390DCD /* MockFiles in Resources */, 8572298A2BBEF0C800E2E802 /* AppRatingPrompt_v1 in Resources */, ); @@ -7596,6 +7601,7 @@ B6AD9E3828D4512E0019CDE9 /* EmbeddedTrackerDataTests.swift in Sources */, 6FF915822B88E07A0042AC87 /* AdAttributionFetcherTests.swift in Sources */, 9F4CC51B2C48C0C7006A96EB /* MockTabDelegate.swift in Sources */, + 1E4E6C572C78B8540059C0FA /* StorePurchaseManagerTests.swift in Sources */, 1E722729292EB24D003B5F53 /* AppSettingsMock.swift in Sources */, 8536A1C8209AF2410050739E /* MockVariantManager.swift in Sources */, C1B7B53428944EFA0098FD6A /* CoreDataTestUtilities.swift in Sources */, diff --git a/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift b/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift new file mode 100644 index 0000000000..f5df31898f --- /dev/null +++ b/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift @@ -0,0 +1,171 @@ +// +// StorePurchaseManagerTests.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import XCTest +@testable import Subscription +import SubscriptionTestingUtilities +import StoreKitTest + +final class StorePurchaseManagerTests: XCTestCase { + + private struct Constants { + static let externalID = UUID().uuidString + static let monthlySubscriptionID = "ios.subscription.1month" + static let yearlySubscriptionID = "ios.subscription.1year" + } + + var session: SKTestSession! + var storePurchaseManager: StorePurchaseManager! + + override func setUpWithError() throws { + let path = Bundle.main.url(forResource: "Subscription", withExtension: "storekit") + + session = try SKTestSession(contentsOf: path!) + session.resetToDefaultState() + session.disableDialogs = true + session.clearTransactions() + + storePurchaseManager = DefaultStorePurchaseManager() + } + + override func tearDownWithError() throws { + storePurchaseManager = nil + session = nil + } + + func testSubscriptionOptionsWhenNoCachedProducts() async throws { + let subscriptionOptions = await storePurchaseManager.subscriptionOptions() + + XCTAssertNil(subscriptionOptions) + XCTAssertFalse(storePurchaseManager.areProductsAvailable) + } + + func testSubscriptionOptionsWhenAvailableProductsWereUpdated() async throws { + await storePurchaseManager.updateAvailableProducts() + + guard let subscriptionOptions = await storePurchaseManager.subscriptionOptions() else { + XCTFail("Expected subscription options") + return + } + + XCTAssertEqual(subscriptionOptions.options.count, 2) + XCTAssertEqual(subscriptionOptions.features.count, SubscriptionFeatureName.allCases.count) + XCTAssertTrue(storePurchaseManager.areProductsAvailable) + + let optionIDs = subscriptionOptions.options.map { $0.id } + XCTAssertTrue(optionIDs.contains(Constants.monthlySubscriptionID)) + XCTAssertTrue(optionIDs.contains(Constants.yearlySubscriptionID)) + } + + func testHasActiveSubscriptionIsFalseWithoutPurchase() async throws { + let hasActiveSubscription = await storePurchaseManager.hasActiveSubscription() + XCTAssertFalse(hasActiveSubscription) + } + + func testPurchaseSubscription() async throws { + await storePurchaseManager.updateAvailableProducts() + + XCTAssertEqual(storePurchaseManager.purchasedProductIDs, []) + + let result = await storePurchaseManager.purchaseSubscription(with: Constants.yearlySubscriptionID, externalID: Constants.externalID) + + switch result { + case .success: + XCTAssertTrue(storePurchaseManager.purchaseQueue.isEmpty) + XCTAssertEqual(storePurchaseManager.purchasedProductIDs, [Constants.yearlySubscriptionID]) + + let transactions = await StoreKitHelpers.currentEntitlements() + XCTAssertEqual(transactions.count, 1) + XCTAssertEqual(transactions.first!.appAccountToken?.uuidString, Constants.externalID) + + let hasActiveSubscription = await storePurchaseManager.hasActiveSubscription() + XCTAssertTrue(hasActiveSubscription) + case .failure: + XCTFail("Unexpected failure") + } + } + + func testPurchaseSubscriptionFailureWithoutValidProductID() async throws { + await storePurchaseManager.updateAvailableProducts() + + let result = await storePurchaseManager.purchaseSubscription(with: "", externalID: Constants.externalID) + + switch result { + case .success: + XCTFail("Unexpected success") + case .failure(let error): + XCTAssertEqual(error, StorePurchaseManagerError.productNotFound) + } + } + + func testPurchaseSubscriptionFailureWithoutValidUUID() async throws { + await storePurchaseManager.updateAvailableProducts() + + let invalidUUID = "a" + XCTAssertNil(UUID(uuidString: invalidUUID)) + + let result = await storePurchaseManager.purchaseSubscription(with: Constants.yearlySubscriptionID, externalID: invalidUUID) + + switch result { + case .success: + XCTFail("Unexpected success") + case .failure(let error): + XCTAssertEqual(error, StorePurchaseManagerError.externalIDisNotAValidUUID) + } + } + + @available(iOS 17.0, *) + func testPurchaseSubscriptionFailure() async throws { + try? await session.setSimulatedError(SKTestFailures.Purchase.purchase(.productUnavailable), + forAPI: StoreKitPurchaseAPI.purchase) + + await storePurchaseManager.updateAvailableProducts() + + let result = await storePurchaseManager.purchaseSubscription(with: Constants.yearlySubscriptionID, externalID: Constants.externalID) + + switch result { + case .success: + XCTFail("Unexpected success") + case .failure(let error): + XCTAssertEqual(error, StorePurchaseManagerError.purchaseFailed) + } + } +} + +private final class StoreKitHelpers { + + static func currentEntitlements() async -> [Transaction] { + return await Transaction.currentEntitlements.compactMap { result in + try? checkVerified(result) + }.reduce(into: [], { $0.append($1) }) + } + + static func checkVerified(_ result: VerificationResult) throws -> T { + // Check whether the JWS passes StoreKit verification. + switch result { + case .unverified: + // StoreKit parses the JWS, but it fails verification. + throw StoreError.failedVerification + case .verified(let safe): + // The result is verified. Return the unwrapped value. + return safe + } + } + +} diff --git a/DuckDuckGo/Subscription/Subscription.storekit b/DuckDuckGoTests/Subscription/Subscription.storekit similarity index 100% rename from DuckDuckGo/Subscription/Subscription.storekit rename to DuckDuckGoTests/Subscription/Subscription.storekit From b224dddd7d35c9e6a6181ab2d308b4e5ac3726da Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 3 Sep 2024 16:09:47 +0200 Subject: [PATCH 02/17] Move to local BSK --- DuckDuckGo.xcodeproj/project.pbxproj | 2 ++ .../xcshareddata/swiftpm/Package.resolved | 9 --------- 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 9581f6faa0..fc74a03bbe 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -1307,6 +1307,7 @@ 1E1D8B6929953CE300C96994 /* autoconsent-test-page-banner.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "autoconsent-test-page-banner.html"; sourceTree = ""; }; 1E24295D293F57FA00584836 /* LottieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieView.swift; sourceTree = ""; }; 1E24295F293F585300584836 /* cookie-icon-animated-40-light.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "cookie-icon-animated-40-light.json"; sourceTree = ""; }; + 1E377E8F2C80649C00AA2A2B /* BrowserServicesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = BrowserServicesKit; path = ../BrowserServicesKit; sourceTree = ""; }; 1E4DCF4527B6A33600961E25 /* DownloadsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsListViewModel.swift; sourceTree = ""; }; 1E4DCF4727B6A35400961E25 /* DownloadsListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsListModel.swift; sourceTree = ""; }; 1E4DCF4927B6A38000961E25 /* DownloadListRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadListRepresentable.swift; sourceTree = ""; }; @@ -4026,6 +4027,7 @@ 84E341891E2F7EFB00BDBA6F = { isa = PBXGroup; children = ( + 1E377E8F2C80649C00AA2A2B /* BrowserServicesKit */, EE3B98EB2A963515002F63A0 /* WidgetsExtensionAlpha.entitlements */, 6FB030C7234331B400A10DB9 /* Configuration.xcconfig */, EEB8FDB92A990AEE00EBEDCF /* Configuration-Alpha.xcconfig */, diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index a184ff8d6b..446e6bcfc0 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,15 +27,6 @@ "version" : "3.0.0" } }, - { - "identity" : "browserserviceskit", - "kind" : "remoteSourceControl", - "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", - "state" : { - "revision" : "ce1b7228a38d2b18525590256051a012109cfee6", - "version" : "188.1.0" - } - }, { "identity" : "content-scope-scripts", "kind" : "remoteSourceControl", From 7ee51147f26a99686f32bcc9759c7d4941d0eb9e Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 3 Sep 2024 16:10:06 +0200 Subject: [PATCH 03/17] Tests for SubscriptionPagesUseSubscriptionFeature --- ...tionPagesUseSubscriptionFeatureTests.swift | 435 +++++++++++++++++- 1 file changed, 423 insertions(+), 12 deletions(-) diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index 0cb08f6fac..a0b9581c61 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -21,26 +21,437 @@ import XCTest @testable import DuckDuckGo @testable import Subscription import SubscriptionTestingUtilities +import Common +import WebKit +import BrowserServicesKit final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { + private struct Constants { + static let userDefaultsSuiteName = "SubscriptionPagesUseSubscriptionFeatureTests" + + static let authToken = UUID().uuidString + static let accessToken = UUID().uuidString + static let externalID = UUID().uuidString + + static let email = "dax@duck.com" + + static let entitlements = [Entitlement(product: .dataBrokerProtection), + Entitlement(product: .identityTheftRestoration), + Entitlement(product: .networkProtection)] +// + static let mostRecentTransactionJWS = "dGhpcyBpcyBub3QgYSByZWFsIEFw(...)cCBTdG9yZSB0cmFuc2FjdGlvbiBKV1M=" + + static let subscriptionOptions = SubscriptionOptions(platform: SubscriptionPlatformName.ios.rawValue, + options: [ + SubscriptionOption(id: "1", + cost: SubscriptionOptionCost(displayPrice: "9 USD", recurrence: "monthly")), + SubscriptionOption(id: "2", + cost: SubscriptionOptionCost(displayPrice: "99 USD", recurrence: "yearly")) + ], + features: [ + SubscriptionFeature(name: "vpn"), + SubscriptionFeature(name: "personal-information-removal"), + SubscriptionFeature(name: "identity-theft-restoration") + ]) +// static let storeLoginResponse = StoreLoginResponse(authToken: Constants.authToken, +// email: "", +// externalID: Constants.externalID, +// id: 1, +// status: "ok") + static let validateTokenResponse = ValidateTokenResponse(account: ValidateTokenResponse.Account(email: Constants.email, + entitlements: Constants.entitlements, + externalID: Constants.externalID)) + + static let mockParams: [String: String] = [:] + @MainActor static let mockScriptMessage = MockWKScriptMessage(name: "", body: "", webView: WKWebView() ) + + static let invalidTokenError = APIServiceError.serverError(statusCode: 401, error: "invalid_token") + } + + var userDefaults: UserDefaults! + + var accountStorage: AccountKeychainStorageMock! + var accessTokenStorage: SubscriptionTokenKeychainStorageMock! + var entitlementsCache: UserDefaultsCache<[Entitlement]>! + + var subscriptionService: SubscriptionEndpointServiceMock! + var authService: AuthEndpointServiceMock! + + var storePurchaseManager: StorePurchaseManagerMock! + var subscriptionEnvironment: SubscriptionEnvironment! + + var appStorePurchaseFlow: AppStorePurchaseFlow! + var appStoreRestoreFlow: AppStoreRestoreFlow! + var appStoreAccountManagementFlow: AppStoreAccountManagementFlow! + +// var accountManager: AccountManagerMock! +// var subscriptionManager: SubscriptionManagerMock! + + var accountManager: AccountManager! + var subscriptionManager: SubscriptionManager! + + var feature: SubscriptionPagesUseSubscriptionFeature! + override func setUpWithError() throws { - // Put setup code here. This method is called before the invocation of each test method in the class. + // Mocks + subscriptionService = SubscriptionEndpointServiceMock() + authService = AuthEndpointServiceMock() + + storePurchaseManager = StorePurchaseManagerMock() + subscriptionEnvironment = SubscriptionEnvironment(serviceEnvironment: .production, + purchasePlatform: .appStore) + accountStorage = AccountKeychainStorageMock() + accessTokenStorage = SubscriptionTokenKeychainStorageMock() + + userDefaults = UserDefaults(suiteName: Constants.userDefaultsSuiteName)! + userDefaults.removePersistentDomain(forName: Constants.userDefaultsSuiteName) + + entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: userDefaults, + key: UserDefaultsCacheKey.subscriptionEntitlements, + settings: UserDefaultsCacheSettings(defaultExpirationInterval: .minutes(20))) + + // Real AccountManager + accountManager = DefaultAccountManager(storage: accountStorage, + accessTokenStorage: accessTokenStorage, + entitlementsCache: entitlementsCache, + subscriptionEndpointService: subscriptionService, + authEndpointService: authService) + + // Real Flows + appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: accountManager, + storePurchaseManager: storePurchaseManager, + subscriptionEndpointService: subscriptionService, + authEndpointService: authService) + + appStorePurchaseFlow = DefaultAppStorePurchaseFlow(subscriptionEndpointService: subscriptionService, + storePurchaseManager: storePurchaseManager, + accountManager: accountManager, + appStoreRestoreFlow: appStoreRestoreFlow, + authEndpointService: authService) + + appStoreAccountManagementFlow = DefaultAppStoreAccountManagementFlow(authEndpointService: authService, + storePurchaseManager: storePurchaseManager, + accountManager: accountManager) + // Real SubscriptionManager + subscriptionManager = DefaultSubscriptionManager(storePurchaseManager: storePurchaseManager, + accountManager: accountManager, + subscriptionEndpointService: subscriptionService, + authEndpointService: authService, + subscriptionEnvironment: subscriptionEnvironment) + + feature = SubscriptionPagesUseSubscriptionFeature(subscriptionManager: subscriptionManager, + subscriptionAttributionOrigin: nil, + appStorePurchaseFlow: appStorePurchaseFlow, + appStoreRestoreFlow: appStoreRestoreFlow, + appStoreAccountManagementFlow: appStoreAccountManagementFlow) } override func tearDownWithError() throws { - // Put teardown code here. This method is called after the invocation of each test method in the class. + AppDependencyProvider.shared = AppDependencyProvider() + + subscriptionService = nil + authService = nil + storePurchaseManager = nil + subscriptionEnvironment = nil + + userDefaults = nil + + accountStorage = nil + accessTokenStorage = nil + + entitlementsCache.reset() + entitlementsCache = nil + + // Real AccountManager + accountManager = nil + + // Real Flows + appStorePurchaseFlow = nil + appStoreRestoreFlow = nil + appStoreAccountManagementFlow = nil + // Real SubscriptionManager + subscriptionManager = nil + + feature = nil + } + + // MARK: - Tests for getSubscription + + func testGetSubscription() async throws { + ensureUserAuthenticatedState() + + let newAuthToken = UUID().uuidString + + authService.validateTokenResult = .failure(Constants.invalidTokenError) + storePurchaseManager.onMostRecentTransaction = { Constants.mostRecentTransactionJWS } + authService.storeLoginResult = .success(StoreLoginResponse(authToken: newAuthToken, + email: Constants.email, + externalID: Constants.externalID, + id: 1, status: "ok")) + + let result = await feature.getSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) + + if let result = result as? [String: String] { + XCTAssertEqual(result[SubscriptionPagesUseSubscriptionFeature.Constants.token], newAuthToken) + XCTAssertEqual(accountManager.authToken, newAuthToken) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + } else { + XCTFail("Incorrect return type") + } + } + + // MARK: - Tests for getSubscriptionOptions + + func testGetSubscriptionOptionsSuccess() async throws { + storePurchaseManager.subscriptionOptionsResult = Constants.subscriptionOptions + + let result = await feature.getSubscriptionOptions(params: Constants.mockParams, original: Constants.mockScriptMessage) + + if let result = result as? SubscriptionOptions { + XCTAssertEqual(result, Constants.subscriptionOptions) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + } else { + XCTFail("Incorrect return type") + } + } + + func testGetSubscriptionOptionsReturnsEmptyOptionsWhenNoSubscriptionOptions() async throws { + storePurchaseManager.subscriptionOptionsResult = nil + + let result = await feature.getSubscriptionOptions(params: Constants.mockParams, original: Constants.mockScriptMessage) + + if let result = result as? SubscriptionOptions { + XCTAssertEqual(result, SubscriptionOptions.empty) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .failedToGetSubscriptionOptions) + } else { + XCTFail("Incorrect return type") + } + } + + func testGetSubscriptionOptionsReturnsEmptyOptionsWhenPurchaseNotAllowed() async throws { + let mockDependencyProvider = MockDependencyProvider() + mockDependencyProvider.subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock(isFeatureAvailable: true, + isSubscriptionPurchaseAllowed: false, + usesUnifiedFeedbackForm: true) + AppDependencyProvider.shared = mockDependencyProvider + + storePurchaseManager.subscriptionOptionsResult = Constants.subscriptionOptions + + let result = await feature.getSubscriptionOptions(params: Constants.mockParams, original: Constants.mockScriptMessage) + + if let result = result as? SubscriptionOptions { + XCTAssertEqual(result, SubscriptionOptions.empty) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + } else { + XCTFail("Incorrect return type") + } + } + + // MARK: - Tests for subscriptionSelected + + func testSubscriptionSelectedSuccessWhenPurchasingFirstTime() async throws { + ensureUserUnauthenticatedState() + + XCTAssertFalse(accountManager.isUserAuthenticated) + + storePurchaseManager.onHasActiveSubscription = { false } + storePurchaseManager.onMostRecentTransaction = { nil } + + authService.createAccountResult = .success(CreateAccountResponse(authToken: Constants.authToken, + externalID: Constants.externalID, + status: "")) + authService.accessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) + authService.validateTokenResult = .success(Constants.validateTokenResponse) + storePurchaseManager.onPurchaseSubscription = { _, _ in .success(Constants.mostRecentTransactionJWS) } + subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, + entitlements: Constants.entitlements, + subscription: SubscriptionMockFactory.subscription)) + + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + // TODO: Check pixel fired: DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseAttempt) + // DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseSuccess) + // UniquePixel.fire(pixel: .privacyProSubscriptionActivated) + // Pixel.fireAttribution(pixel: .privacyProSuccessfulSubscriptionAttribution, origin: subscriptionAttributionOrigin, privacyProDataReporter: privacyProDataReporter) + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + } + + // MARK: - Tests for setSubscription + + func testSetSubscriptionTokenSuccess() async throws { + ensureUserUnauthenticatedState() + + authService.accessTokenResult = .success(.init(accessToken: Constants.accessToken)) + authService.validateTokenResult = .success(Constants.validateTokenResponse) + + let onSetSubscriptionCalled = expectation(description: "onSetSubscription") + feature.onSetSubscription = { + onSetSubscriptionCalled.fulfill() + } + + let setSubscriptionParams = ["token": Constants.authToken] + let result = await feature.setSubscription(params: setSubscriptionParams, original: Constants.mockScriptMessage) + + XCTAssertEqual(accountManager.authToken, Constants.authToken) + XCTAssertEqual(accountManager.accessToken, Constants.accessToken) + XCTAssertEqual(accountManager.email, Constants.email) + XCTAssertEqual(accountManager.externalID, Constants.externalID) + + await fulfillment(of: [onSetSubscriptionCalled], timeout: 0.5) + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + } + + // MARK: - Tests for activateSubscription + + func testActivateSubscriptionTokenSuccess() async throws { + ensureUserAuthenticatedState() + + let onActivateSubscriptionCalled = expectation(description: "onActivateSubscription") + feature.onActivateSubscription = { + onActivateSubscriptionCalled.fulfill() + } + + let result = await feature.activateSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) + + // TODO: Check pixel fired: Pixel.fire(pixel: .privacyProRestorePurchaseOfferPageEntry, debounce: 2) + await fulfillment(of: [onActivateSubscriptionCalled], timeout: 0.5) + XCTAssertNil(result) + } + + // MARK: - Tests for featureSelected + + func testFeatureSelectedSuccess() async throws { + + struct FeatureSelection: Codable { + let feature: String + } + + ensureUserAuthenticatedState() + + let onFeatureSelectedCalled = expectation(description: "onFeatureSelected") + feature.onFeatureSelected = { selection in + onFeatureSelectedCalled.fulfill() + XCTAssertEqual(selection, SubscriptionFeatureSelection.itr) + } + + let featureSelectionParams = ["feature": SubscriptionFeatureName.itr] + let result = await feature.featureSelected(params: featureSelectionParams, original: Constants.mockScriptMessage) + + await fulfillment(of: [onFeatureSelectedCalled], timeout: 0.5) + XCTAssertNil(result) + } + + // MARK: - Tests for backToSettings + + func testBackToSettingsSuccess() async throws { + ensureUserAuthenticatedState() + accountStorage.email = nil + + XCTAssertNil(accountManager.email) + + let onBackToSettingsCalled = expectation(description: "onBackToSettings") + feature.onBackToSettings = { + onBackToSettingsCalled.fulfill() + } + + authService.validateTokenResult = .success(Constants.validateTokenResponse) + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.subscription) + + let result = await feature.backToSettings(params: Constants.mockParams, original: Constants.mockScriptMessage) + + await fulfillment(of: [onBackToSettingsCalled], timeout: 0.5) + + XCTAssertEqual(accountManager.email, Constants.email) + XCTAssertNil(result) + } + + + // MARK: - Tests for getAccessToken + func testGetAccessTokenSuccess() async throws { + ensureUserAuthenticatedState() + + let result = try await feature.getAccessToken(params: Constants.mockParams, original: Constants.mockScriptMessage) + + if let result = result as? [String: String] { + XCTAssertEqual(result[SubscriptionPagesUseSubscriptionFeature.Constants.token], Constants.accessToken) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + } else { + XCTFail("Incorrect return type") + } + } + + func testGetAccessTokenEmptyOnMissingToken() async throws { + ensureUserUnauthenticatedState() + XCTAssertNil(accountManager.accessToken) + + let result = try await feature.getAccessToken(params: Constants.mockParams, original: Constants.mockScriptMessage) + + if let result = result as? [String: String] { + XCTAssertEqual(result, [String: String]()) + } else { + XCTFail("Incorrect return type") + } + } + + // MARK: - Tests for restoreAccountFromAppStorePurchase + + func testRestoreAccountFromAppStorePurchaseSuccess() async throws { + // uses appStoreRestoreFlow + // TODO: + + ensureUserAuthenticatedState() + } + + // MARK: - Tests for mapAppStoreRestoreErrorToTransactionError + + func testMapAppStoreRestoreErrorToTransactionErrorSuccess() async throws { + // TODO: + + ensureUserAuthenticatedState() } +} + +extension SubscriptionPagesUseSubscriptionFeatureTests { - func testExample() throws { - let appStorePurchaseFlow = AppStorePurchaseFlowMock(purchaseSubscriptionResult: .success("TransactionJWS"), - completeSubscriptionPurchaseResult: .success(PurchaseUpdate(type: "t", token: "t"))) - let appStoreAccountManagementFlow = AppStoreAccountManagementFlowMock(refreshAuthTokenIfNeededResult: .success("Something")) - let feature = SubscriptionPagesUseSubscriptionFeature(subscriptionManager: SubscriptionMockFactory.subscriptionManager, - subscriptionAttributionOrigin: "???", - appStorePurchaseFlow: appStorePurchaseFlow, - appStoreRestoreFlow: SubscriptionMockFactory.appStoreRestoreFlow, - appStoreAccountManagementFlow: appStoreAccountManagementFlow) - // To be implemented + func ensureUserAuthenticatedState() { + accountStorage.authToken = Constants.authToken + accountStorage.email = Constants.email + accountStorage.externalID = Constants.externalID + accessTokenStorage.accessToken = Constants.accessToken } + + func ensureUserUnauthenticatedState() { + try? accessTokenStorage.removeAccessToken() + try? accountStorage.clearAuthenticationState() + } +} + +class SubscriptionFeatureAvailabilityMock: SubscriptionFeatureAvailability { + var isFeatureAvailable: Bool + var isSubscriptionPurchaseAllowed: Bool + var usesUnifiedFeedbackForm: Bool + + init(isFeatureAvailable: Bool, isSubscriptionPurchaseAllowed: Bool, usesUnifiedFeedbackForm: Bool) { + self.isFeatureAvailable = isFeatureAvailable + self.isSubscriptionPurchaseAllowed = isSubscriptionPurchaseAllowed + self.usesUnifiedFeedbackForm = usesUnifiedFeedbackForm + } + } From 16d5cd5b14161d49957f715bd34bd50192ff42f8 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 4 Sep 2024 08:52:11 +0200 Subject: [PATCH 04/17] Tests for restoreAccountFromAppStorePurchase --- ...tionPagesUseSubscriptionFeatureTests.swift | 111 +++++++++++++++--- 1 file changed, 96 insertions(+), 15 deletions(-) diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index a0b9581c61..e2b5e887f7 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -39,7 +39,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { static let entitlements = [Entitlement(product: .dataBrokerProtection), Entitlement(product: .identityTheftRestoration), Entitlement(product: .networkProtection)] -// + static let mostRecentTransactionJWS = "dGhpcyBpcyBub3QgYSByZWFsIEFw(...)cCBTdG9yZSB0cmFuc2FjdGlvbiBKV1M=" static let subscriptionOptions = SubscriptionOptions(platform: SubscriptionPlatformName.ios.rawValue, @@ -54,11 +54,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { SubscriptionFeature(name: "personal-information-removal"), SubscriptionFeature(name: "identity-theft-restoration") ]) -// static let storeLoginResponse = StoreLoginResponse(authToken: Constants.authToken, -// email: "", -// externalID: Constants.externalID, -// id: 1, -// status: "ok") + static let validateTokenResponse = ValidateTokenResponse(account: ValidateTokenResponse.Account(email: Constants.email, entitlements: Constants.entitlements, externalID: Constants.externalID)) @@ -163,14 +159,13 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { entitlementsCache.reset() entitlementsCache = nil - // Real AccountManager accountManager = nil // Real Flows appStorePurchaseFlow = nil appStoreRestoreFlow = nil appStoreAccountManagementFlow = nil - // Real SubscriptionManager + subscriptionManager = nil feature = nil @@ -413,18 +408,104 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // MARK: - Tests for restoreAccountFromAppStorePurchase func testRestoreAccountFromAppStorePurchaseSuccess() async throws { - // uses appStoreRestoreFlow - // TODO: + ensureUserUnauthenticatedState() - ensureUserAuthenticatedState() + storePurchaseManager.onMostRecentTransaction = { Constants.mostRecentTransactionJWS } + authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, + email: Constants.email, + externalID: Constants.externalID, + id: 1, status: "ok")) + authService.accessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) + authService.validateTokenResult = .success(Constants.validateTokenResponse) + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.subscription) + + do { + try await feature.restoreAccountFromAppStorePurchase() + + XCTAssertTrue(accountManager.isUserAuthenticated) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + } catch let error { + XCTFail("Unexpected error \(error)") + } } - // MARK: - Tests for mapAppStoreRestoreErrorToTransactionError + func testRestoreAccountFromAppStorePurchaseErrorDueToExpiredSubscription() async throws { + ensureUserUnauthenticatedState() - func testMapAppStoreRestoreErrorToTransactionErrorSuccess() async throws { - // TODO: + storePurchaseManager.onMostRecentTransaction = { Constants.mostRecentTransactionJWS } + authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, + email: Constants.email, + externalID: Constants.externalID, + id: 1, status: "ok")) + authService.accessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) + authService.validateTokenResult = .success(Constants.validateTokenResponse) + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredSubscription) - ensureUserAuthenticatedState() + do { + try await feature.restoreAccountFromAppStorePurchase() + + XCTFail("Unexpected success") + } catch let error { + guard let error = error as? SubscriptionPagesUseSubscriptionFeature.UseSubscriptionError else { + XCTFail("Unexpected error type") + return + } + + XCTAssertEqual(error, .subscriptionExpired) + XCTAssertFalse(accountManager.isUserAuthenticated) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + } + } + + func testRestoreAccountFromAppStorePurchaseErrorDueToNoTransaction() async throws { + ensureUserUnauthenticatedState() + + storePurchaseManager.onMostRecentTransaction = { nil } + + do { + try await feature.restoreAccountFromAppStorePurchase() + + XCTFail("Unexpected success") + } catch let error { + guard let error = error as? SubscriptionPagesUseSubscriptionFeature.UseSubscriptionError else { + XCTFail("Unexpected error type") + return + } + + XCTAssertEqual(error, .subscriptionNotFound) + XCTAssertFalse(accountManager.isUserAuthenticated) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + } + } + + func testRestoreAccountFromAppStorePurchaseErrorDueToOtherError() async throws { + ensureUserUnauthenticatedState() + + storePurchaseManager.onMostRecentTransaction = { Constants.mostRecentTransactionJWS } + authService.storeLoginResult = .failure(Constants.invalidTokenError) + + do { + try await feature.restoreAccountFromAppStorePurchase() + + XCTFail("Unexpected success") + } catch let error { + guard let error = error as? SubscriptionPagesUseSubscriptionFeature.UseSubscriptionError else { + XCTFail("Unexpected error type") + return + } + + XCTAssertEqual(error, .failedToRestorePastPurchase) + XCTAssertFalse(accountManager.isUserAuthenticated) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + } } } From f68a508694b338b30b95e21803ce1371156ca0d0 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 4 Sep 2024 19:58:23 +0200 Subject: [PATCH 05/17] Add purchase error scenarios --- ...tionPagesUseSubscriptionFeatureTests.swift | 361 +++++++++++++++++- 1 file changed, 351 insertions(+), 10 deletions(-) diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index e2b5e887f7..9314693e01 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -81,9 +81,6 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { var appStoreRestoreFlow: AppStoreRestoreFlow! var appStoreAccountManagementFlow: AppStoreAccountManagementFlow! -// var accountManager: AccountManagerMock! -// var subscriptionManager: SubscriptionManagerMock! - var accountManager: AccountManager! var subscriptionManager: SubscriptionManager! @@ -173,7 +170,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // MARK: - Tests for getSubscription - func testGetSubscription() async throws { + func testGetSubscriptionSuccessRefreshingAuthToken() async throws { ensureUserAuthenticatedState() let newAuthToken = UUID().uuidString @@ -198,6 +195,43 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } } + func testGetSubscriptionSuccessWithoutRefreshingAuthToken() async throws { + ensureUserAuthenticatedState() + + authService.validateTokenResult = .success(Constants.validateTokenResponse) + + let result = await feature.getSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) + + if let result = result as? [String: String] { + XCTAssertEqual(result[SubscriptionPagesUseSubscriptionFeature.Constants.token], Constants.authToken) + XCTAssertEqual(accountManager.authToken, Constants.authToken) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + } else { + XCTFail("Incorrect return type") + } + } + + func testGetSubscriptionSuccessErrorWhenUnauthenticated() async throws { + ensureUserUnauthenticatedState() + + authService.validateTokenResult = .failure(Constants.invalidTokenError) + storePurchaseManager.onMostRecentTransaction = { nil } + + let result = await feature.getSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) + + if let result = result as? [String: String] { + XCTAssertEqual(result[SubscriptionPagesUseSubscriptionFeature.Constants.token], SubscriptionPagesUseSubscriptionFeature.Constants.empty) + XCTAssertFalse(accountManager.isUserAuthenticated) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + } else { + XCTFail("Incorrect return type") + } + } + // MARK: - Tests for getSubscriptionOptions func testGetSubscriptionOptionsSuccess() async throws { @@ -283,9 +317,254 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionError, nil) } + func testSubscriptionSelectedSuccessWhenRepurchasingForExpiredAppleSubscription() async throws { + ensureUserAuthenticatedState() + + XCTAssertTrue(accountManager.isUserAuthenticated) + + storePurchaseManager.onHasActiveSubscription = { false } + storePurchaseManager.onMostRecentTransaction = { Constants.mostRecentTransactionJWS } + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredSubscription) + + authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, + email: Constants.email, + externalID: Constants.externalID, + id: 1, + status: "ok")) + authService.accessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) + authService.validateTokenResult = .success(Constants.validateTokenResponse) + storePurchaseManager.onPurchaseSubscription = { _, _ in .success(Constants.mostRecentTransactionJWS) } + subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, + entitlements: Constants.entitlements, + subscription: SubscriptionMockFactory.subscription)) + + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + // TODO: Check pixel fired: DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseAttempt) + // DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseSuccess) + // UniquePixel.fire(pixel: .privacyProSubscriptionActivated) + // Pixel.fireAttribution(pixel: .privacyProSuccessfulSubscriptionAttribution, origin: subscriptionAttributionOrigin, privacyProDataReporter: privacyProDataReporter) + + XCTAssertFalse(authService.createAccountCalled) + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + } + + func testSubscriptionSelectedSuccessWhenRepurchasingForExpiredStripeSubscription() async throws { + ensureUserAuthenticatedState() + + XCTAssertTrue(accountManager.isUserAuthenticated) + + storePurchaseManager.onHasActiveSubscription = { false } + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + storePurchaseManager.onPurchaseSubscription = { _, _ in .success(Constants.mostRecentTransactionJWS) } + subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, + entitlements: Constants.entitlements, + subscription: SubscriptionMockFactory.subscription)) + + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + // TODO: Check pixel fired: DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseAttempt) + // DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseSuccess) + // UniquePixel.fire(pixel: .privacyProSubscriptionActivated) + // Pixel.fireAttribution(pixel: .privacyProSuccessfulSubscriptionAttribution, origin: subscriptionAttributionOrigin, privacyProDataReporter: privacyProDataReporter) + + XCTAssertFalse(authService.createAccountCalled) + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) + } + + func testSubscriptionSelectedErrorWhenPurchasingWhenHavingActiveSubscription() async throws { + ensureUserAuthenticatedState() + + storePurchaseManager.onHasActiveSubscription = { true } + + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + XCTAssertFalse(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .hasActiveSubscription) + } + + func testSubscriptionSelectedErrorWhenPurchasingWhenUnauthenticatedAndHavingActiveSubscriptionOnAppleID() async throws { + ensureUserUnauthenticatedState() + + storePurchaseManager.onHasActiveSubscription = { true } + + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + XCTAssertFalse(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .hasActiveSubscription) + } + + func testSubscriptionSelectedErrorWhenUnauthenticatedAndAccountCreationFails() async throws { + ensureUserUnauthenticatedState() + + storePurchaseManager.onHasActiveSubscription = { false } + storePurchaseManager.onMostRecentTransaction = { nil } + + authService.createAccountResult = .failure(Constants.invalidTokenError) + + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // TODO: Check pixel fired: DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseAttempt) + + XCTAssertFalse(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .accountCreationFailed) + } + + func testSubscriptionSelectedErrorWhenPurchaseCancelledByUser() async throws { + ensureUserAuthenticatedState() + + storePurchaseManager.onHasActiveSubscription = { false } + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + storePurchaseManager.onPurchaseSubscription = { _, _ in .failure(StorePurchaseManagerError.purchaseCancelledByUser) } + + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .cancelledByUser) + } + + func testSubscriptionSelectedErrorWhenProductNotFound() async throws { + ensureUserAuthenticatedState() + + storePurchaseManager.onHasActiveSubscription = { false } + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + storePurchaseManager.onPurchaseSubscription = { _, _ in .failure(StorePurchaseManagerError.productNotFound) } + + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .purchaseFailed) + } + + func testSubscriptionSelectedErrorWhenExternalIDIsNotValidUUID() async throws { + ensureUserAuthenticatedState() + + storePurchaseManager.onHasActiveSubscription = { false } + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + storePurchaseManager.onPurchaseSubscription = { _, _ in .failure(StorePurchaseManagerError.externalIDisNotAValidUUID) } + + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .purchaseFailed) + } + + func testSubscriptionSelectedErrorWhenPurchaseFailed() async throws { + ensureUserAuthenticatedState() + + storePurchaseManager.onHasActiveSubscription = { false } + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + storePurchaseManager.onPurchaseSubscription = { _, _ in .failure(StorePurchaseManagerError.purchaseFailed) } + + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .purchaseFailed) + } + + func testSubscriptionSelectedErrorWhenTransactionCannotBeVerified() async throws { + ensureUserAuthenticatedState() + + storePurchaseManager.onHasActiveSubscription = { false } + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + storePurchaseManager.onPurchaseSubscription = { _, _ in .failure(StorePurchaseManagerError.transactionCannotBeVerified) } + + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .purchaseFailed) + } + + func testSubscriptionSelectedErrorWhenTransactionPendingAuthentication() async throws { + ensureUserAuthenticatedState() + + storePurchaseManager.onHasActiveSubscription = { false } + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + storePurchaseManager.onPurchaseSubscription = { _, _ in .failure(StorePurchaseManagerError.transactionPendingAuthentication) } + + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .purchaseFailed) + } + + func testSubscriptionSelectedErrorDueToUnknownPurchaseError() async throws { + ensureUserAuthenticatedState() + + storePurchaseManager.onHasActiveSubscription = { false } + subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) + storePurchaseManager.onPurchaseSubscription = { _, _ in .failure(StorePurchaseManagerError.unknownError) } + + let subscriptionSelectedParams = ["id": "some-subscription-id"] + let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) + + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .purchaseFailed) + } + + // MARK: - Tests for setSubscription - func testSetSubscriptionTokenSuccess() async throws { + func testSetSubscriptionSuccess() async throws { ensureUserUnauthenticatedState() authService.accessTokenResult = .success(.init(accessToken: Constants.accessToken)) @@ -311,6 +590,55 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionError, nil) } + func testSetSubscriptionErrorWhenFailedToExchangeToken() async throws { + ensureUserUnauthenticatedState() + + authService.accessTokenResult = .failure(Constants.invalidTokenError) + + let onSetSubscriptionCalled = expectation(description: "onSetSubscription") + onSetSubscriptionCalled.isInverted = true + feature.onSetSubscription = { + onSetSubscriptionCalled.fulfill() + } + + let setSubscriptionParams = ["token": Constants.authToken] + let result = await feature.setSubscription(params: setSubscriptionParams, original: Constants.mockScriptMessage) + + XCTAssertNil(accountManager.authToken) + XCTAssertFalse(accountManager.isUserAuthenticated) + + await fulfillment(of: [onSetSubscriptionCalled], timeout: 0.5) + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .failedToSetSubscription) + } + + func testSetSubscriptionErrorWhenFailedToFetchAccountDetails() async throws { + ensureUserUnauthenticatedState() + + authService.accessTokenResult = .success(.init(accessToken: Constants.accessToken)) + authService.validateTokenResult = .failure(Constants.invalidTokenError) + + let onSetSubscriptionCalled = expectation(description: "onSetSubscription") + onSetSubscriptionCalled.isInverted = true + feature.onSetSubscription = { + onSetSubscriptionCalled.fulfill() + } + + let setSubscriptionParams = ["token": Constants.authToken] + let result = await feature.setSubscription(params: setSubscriptionParams, original: Constants.mockScriptMessage) + + XCTAssertNil(accountManager.authToken) + XCTAssertFalse(accountManager.isUserAuthenticated) + + await fulfillment(of: [onSetSubscriptionCalled], timeout: 0.5) + XCTAssertNil(result) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .failedToSetSubscription) + } + // MARK: - Tests for activateSubscription func testActivateSubscriptionTokenSuccess() async throws { @@ -331,11 +659,6 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // MARK: - Tests for featureSelected func testFeatureSelectedSuccess() async throws { - - struct FeatureSelection: Codable { - let feature: String - } - ensureUserAuthenticatedState() let onFeatureSelectedCalled = expectation(description: "onFeatureSelected") @@ -375,6 +698,24 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertNil(result) } + func testBackToSettingsErrorOnFetchingAccountDetails() async throws { + ensureUserAuthenticatedState() + + let onBackToSettingsCalled = expectation(description: "onBackToSettings") + onBackToSettingsCalled.isInverted = true + feature.onBackToSettings = { + onBackToSettingsCalled.fulfill() + } + + authService.validateTokenResult = .failure(Constants.invalidTokenError) + + let result = await feature.backToSettings(params: Constants.mockParams, original: Constants.mockScriptMessage) + + await fulfillment(of: [onBackToSettingsCalled], timeout: 0.5) + + XCTAssertEqual(feature.transactionError, .generalError) + XCTAssertNil(result) + } // MARK: - Tests for getAccessToken func testGetAccessTokenSuccess() async throws { From bb3d33ee37ee407da976080cd630302ac7a994ed Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 5 Sep 2024 13:34:23 +0200 Subject: [PATCH 06/17] Adapt to mock API updates --- ...tionPagesUseSubscriptionFeatureTests.swift | 82 +++++++++---------- 1 file changed, 41 insertions(+), 41 deletions(-) diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index 9314693e01..743f62f1f7 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -112,8 +112,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { authEndpointService: authService) // Real Flows - appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: accountManager, - storePurchaseManager: storePurchaseManager, + appStoreRestoreFlow = DefaultAppStoreRestoreFlow(accountManager: accountManager, + storePurchaseManager: storePurchaseManager, subscriptionEndpointService: subscriptionService, authEndpointService: authService) @@ -176,7 +176,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { let newAuthToken = UUID().uuidString authService.validateTokenResult = .failure(Constants.invalidTokenError) - storePurchaseManager.onMostRecentTransaction = { Constants.mostRecentTransactionJWS } + storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS authService.storeLoginResult = .success(StoreLoginResponse(authToken: newAuthToken, email: Constants.email, externalID: Constants.externalID, @@ -217,7 +217,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { ensureUserUnauthenticatedState() authService.validateTokenResult = .failure(Constants.invalidTokenError) - storePurchaseManager.onMostRecentTransaction = { nil } + storePurchaseManager.mostRecentTransactionResult = nil let result = await feature.getSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) @@ -292,15 +292,15 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertFalse(accountManager.isUserAuthenticated) - storePurchaseManager.onHasActiveSubscription = { false } - storePurchaseManager.onMostRecentTransaction = { nil } + storePurchaseManager.hasActiveSubscriptionResult = false + storePurchaseManager.mostRecentTransactionResult = nil authService.createAccountResult = .success(CreateAccountResponse(authToken: Constants.authToken, externalID: Constants.externalID, status: "")) - authService.accessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) + authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) authService.validateTokenResult = .success(Constants.validateTokenResponse) - storePurchaseManager.onPurchaseSubscription = { _, _ in .success(Constants.mostRecentTransactionJWS) } + storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, entitlements: Constants.entitlements, subscription: SubscriptionMockFactory.subscription)) @@ -322,8 +322,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertTrue(accountManager.isUserAuthenticated) - storePurchaseManager.onHasActiveSubscription = { false } - storePurchaseManager.onMostRecentTransaction = { Constants.mostRecentTransactionJWS } + storePurchaseManager.hasActiveSubscriptionResult = false + storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredSubscription) authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, @@ -331,9 +331,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { externalID: Constants.externalID, id: 1, status: "ok")) - authService.accessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) + authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) authService.validateTokenResult = .success(Constants.validateTokenResponse) - storePurchaseManager.onPurchaseSubscription = { _, _ in .success(Constants.mostRecentTransactionJWS) } + storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, entitlements: Constants.entitlements, subscription: SubscriptionMockFactory.subscription)) @@ -359,9 +359,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertTrue(accountManager.isUserAuthenticated) - storePurchaseManager.onHasActiveSubscription = { false } + storePurchaseManager.hasActiveSubscriptionResult = false subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) - storePurchaseManager.onPurchaseSubscription = { _, _ in .success(Constants.mostRecentTransactionJWS) } + storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) subscriptionService.confirmPurchaseResult = .success(ConfirmPurchaseResponse(email: Constants.email, entitlements: Constants.entitlements, subscription: SubscriptionMockFactory.subscription)) @@ -385,7 +385,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testSubscriptionSelectedErrorWhenPurchasingWhenHavingActiveSubscription() async throws { ensureUserAuthenticatedState() - storePurchaseManager.onHasActiveSubscription = { true } + storePurchaseManager.hasActiveSubscriptionResult = true let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) @@ -401,7 +401,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testSubscriptionSelectedErrorWhenPurchasingWhenUnauthenticatedAndHavingActiveSubscriptionOnAppleID() async throws { ensureUserUnauthenticatedState() - storePurchaseManager.onHasActiveSubscription = { true } + storePurchaseManager.hasActiveSubscriptionResult = true let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) @@ -417,8 +417,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testSubscriptionSelectedErrorWhenUnauthenticatedAndAccountCreationFails() async throws { ensureUserUnauthenticatedState() - storePurchaseManager.onHasActiveSubscription = { false } - storePurchaseManager.onMostRecentTransaction = { nil } + storePurchaseManager.hasActiveSubscriptionResult = false + storePurchaseManager.mostRecentTransactionResult = nil authService.createAccountResult = .failure(Constants.invalidTokenError) @@ -438,9 +438,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testSubscriptionSelectedErrorWhenPurchaseCancelledByUser() async throws { ensureUserAuthenticatedState() - storePurchaseManager.onHasActiveSubscription = { false } + storePurchaseManager.hasActiveSubscriptionResult = false subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) - storePurchaseManager.onPurchaseSubscription = { _, _ in .failure(StorePurchaseManagerError.purchaseCancelledByUser) } + storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.purchaseCancelledByUser) let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) @@ -456,9 +456,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testSubscriptionSelectedErrorWhenProductNotFound() async throws { ensureUserAuthenticatedState() - storePurchaseManager.onHasActiveSubscription = { false } + storePurchaseManager.hasActiveSubscriptionResult = false subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) - storePurchaseManager.onPurchaseSubscription = { _, _ in .failure(StorePurchaseManagerError.productNotFound) } + storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.productNotFound) let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) @@ -474,9 +474,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testSubscriptionSelectedErrorWhenExternalIDIsNotValidUUID() async throws { ensureUserAuthenticatedState() - storePurchaseManager.onHasActiveSubscription = { false } + storePurchaseManager.hasActiveSubscriptionResult = false subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) - storePurchaseManager.onPurchaseSubscription = { _, _ in .failure(StorePurchaseManagerError.externalIDisNotAValidUUID) } + storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.externalIDisNotAValidUUID) let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) @@ -492,9 +492,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testSubscriptionSelectedErrorWhenPurchaseFailed() async throws { ensureUserAuthenticatedState() - storePurchaseManager.onHasActiveSubscription = { false } + storePurchaseManager.hasActiveSubscriptionResult = false subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) - storePurchaseManager.onPurchaseSubscription = { _, _ in .failure(StorePurchaseManagerError.purchaseFailed) } + storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.purchaseFailed) let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) @@ -510,9 +510,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testSubscriptionSelectedErrorWhenTransactionCannotBeVerified() async throws { ensureUserAuthenticatedState() - storePurchaseManager.onHasActiveSubscription = { false } + storePurchaseManager.hasActiveSubscriptionResult = false subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) - storePurchaseManager.onPurchaseSubscription = { _, _ in .failure(StorePurchaseManagerError.transactionCannotBeVerified) } + storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.transactionCannotBeVerified) let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) @@ -528,9 +528,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testSubscriptionSelectedErrorWhenTransactionPendingAuthentication() async throws { ensureUserAuthenticatedState() - storePurchaseManager.onHasActiveSubscription = { false } + storePurchaseManager.hasActiveSubscriptionResult = false subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) - storePurchaseManager.onPurchaseSubscription = { _, _ in .failure(StorePurchaseManagerError.transactionPendingAuthentication) } + storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.transactionPendingAuthentication) let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) @@ -546,9 +546,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testSubscriptionSelectedErrorDueToUnknownPurchaseError() async throws { ensureUserAuthenticatedState() - storePurchaseManager.onHasActiveSubscription = { false } + storePurchaseManager.hasActiveSubscriptionResult = false subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) - storePurchaseManager.onPurchaseSubscription = { _, _ in .failure(StorePurchaseManagerError.unknownError) } + storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.unknownError) let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) @@ -567,7 +567,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testSetSubscriptionSuccess() async throws { ensureUserUnauthenticatedState() - authService.accessTokenResult = .success(.init(accessToken: Constants.accessToken)) + authService.getAccessTokenResult = .success(.init(accessToken: Constants.accessToken)) authService.validateTokenResult = .success(Constants.validateTokenResponse) let onSetSubscriptionCalled = expectation(description: "onSetSubscription") @@ -593,7 +593,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testSetSubscriptionErrorWhenFailedToExchangeToken() async throws { ensureUserUnauthenticatedState() - authService.accessTokenResult = .failure(Constants.invalidTokenError) + authService.getAccessTokenResult = .failure(Constants.invalidTokenError) let onSetSubscriptionCalled = expectation(description: "onSetSubscription") onSetSubscriptionCalled.isInverted = true @@ -617,7 +617,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testSetSubscriptionErrorWhenFailedToFetchAccountDetails() async throws { ensureUserUnauthenticatedState() - authService.accessTokenResult = .success(.init(accessToken: Constants.accessToken)) + authService.getAccessTokenResult = .success(.init(accessToken: Constants.accessToken)) authService.validateTokenResult = .failure(Constants.invalidTokenError) let onSetSubscriptionCalled = expectation(description: "onSetSubscription") @@ -751,12 +751,12 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testRestoreAccountFromAppStorePurchaseSuccess() async throws { ensureUserUnauthenticatedState() - storePurchaseManager.onMostRecentTransaction = { Constants.mostRecentTransactionJWS } + storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, email: Constants.email, externalID: Constants.externalID, id: 1, status: "ok")) - authService.accessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) + authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) authService.validateTokenResult = .success(Constants.validateTokenResponse) subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.subscription) @@ -775,12 +775,12 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testRestoreAccountFromAppStorePurchaseErrorDueToExpiredSubscription() async throws { ensureUserUnauthenticatedState() - storePurchaseManager.onMostRecentTransaction = { Constants.mostRecentTransactionJWS } + storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, email: Constants.email, externalID: Constants.externalID, id: 1, status: "ok")) - authService.accessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) + authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) authService.validateTokenResult = .success(Constants.validateTokenResponse) subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredSubscription) @@ -805,7 +805,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testRestoreAccountFromAppStorePurchaseErrorDueToNoTransaction() async throws { ensureUserUnauthenticatedState() - storePurchaseManager.onMostRecentTransaction = { nil } + storePurchaseManager.mostRecentTransactionResult = nil do { try await feature.restoreAccountFromAppStorePurchase() @@ -828,7 +828,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { func testRestoreAccountFromAppStorePurchaseErrorDueToOtherError() async throws { ensureUserUnauthenticatedState() - storePurchaseManager.onMostRecentTransaction = { Constants.mostRecentTransactionJWS } + storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS authService.storeLoginResult = .failure(Constants.invalidTokenError) do { From 027eb40ee7369c9a02675f6f7a13cc32ce15857c Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Fri, 6 Sep 2024 11:10:32 +0200 Subject: [PATCH 07/17] Fix status literals --- .../SubscriptionPagesUseSubscriptionFeatureTests.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index 743f62f1f7..df7bf4b055 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -180,7 +180,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { authService.storeLoginResult = .success(StoreLoginResponse(authToken: newAuthToken, email: Constants.email, externalID: Constants.externalID, - id: 1, status: "ok")) + id: 1, status: "authenticated")) let result = await feature.getSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) @@ -297,7 +297,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { authService.createAccountResult = .success(CreateAccountResponse(authToken: Constants.authToken, externalID: Constants.externalID, - status: "")) + status: "created")) authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) authService.validateTokenResult = .success(Constants.validateTokenResponse) storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) @@ -330,7 +330,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { email: Constants.email, externalID: Constants.externalID, id: 1, - status: "ok")) + status: "authenticated")) authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) authService.validateTokenResult = .success(Constants.validateTokenResponse) storePurchaseManager.purchaseSubscriptionResult = .success(Constants.mostRecentTransactionJWS) @@ -755,7 +755,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, email: Constants.email, externalID: Constants.externalID, - id: 1, status: "ok")) + id: 1, status: "authenticated")) authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) authService.validateTokenResult = .success(Constants.validateTokenResponse) subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.subscription) @@ -779,7 +779,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { authService.storeLoginResult = .success(StoreLoginResponse(authToken: Constants.authToken, email: Constants.email, externalID: Constants.externalID, - id: 1, status: "ok")) + id: 1, status: "authenticated")) authService.getAccessTokenResult = .success(AccessTokenResponse(accessToken: Constants.accessToken)) authService.validateTokenResult = .success(Constants.validateTokenResponse) subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredSubscription) From 678e947e402f12be8047331510fa94ebd12af03b Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 11 Sep 2024 20:38:56 +0200 Subject: [PATCH 08/17] Move to remote BSK --- DuckDuckGo.xcodeproj/project.pbxproj | 18 +++++++++++++---- .../xcshareddata/swiftpm/Package.resolved | 11 +++++++++- .../SubscriptionFeatureAvailabilityMock.swift | 20 +++++++++++++++++++ 3 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 DuckDuckGoTests/Subscription/SubscriptionFeatureAvailabilityMock.swift diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 687d2346d5..8085814ed1 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -59,6 +59,7 @@ 1E1D8B6C29953CE300C96994 /* autoconsent-test-page-banner.html in Resources */ = {isa = PBXBuildFile; fileRef = 1E1D8B6929953CE300C96994 /* autoconsent-test-page-banner.html */; }; 1E24295E293F57FA00584836 /* LottieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E24295D293F57FA00584836 /* LottieView.swift */; }; 1E242960293F585300584836 /* cookie-icon-animated-40-light.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E24295F293F585300584836 /* cookie-icon-animated-40-light.json */; }; + 1E25D5322C92126B004400F0 /* SubscriptionFeatureAvailabilityMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E25D5312C92126B004400F0 /* SubscriptionFeatureAvailabilityMock.swift */; }; 1E4DCF4627B6A33600961E25 /* DownloadsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4DCF4527B6A33600961E25 /* DownloadsListViewModel.swift */; }; 1E4DCF4827B6A35400961E25 /* DownloadsListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4DCF4727B6A35400961E25 /* DownloadsListModel.swift */; }; 1E4DCF4A27B6A38000961E25 /* DownloadListRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4DCF4927B6A38000961E25 /* DownloadListRepresentable.swift */; }; @@ -1323,7 +1324,7 @@ 1E1D8B6929953CE300C96994 /* autoconsent-test-page-banner.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = "autoconsent-test-page-banner.html"; sourceTree = ""; }; 1E24295D293F57FA00584836 /* LottieView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LottieView.swift; sourceTree = ""; }; 1E24295F293F585300584836 /* cookie-icon-animated-40-light.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "cookie-icon-animated-40-light.json"; sourceTree = ""; }; - 1E377E8F2C80649C00AA2A2B /* BrowserServicesKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = BrowserServicesKit; path = ../BrowserServicesKit; sourceTree = ""; }; + 1E25D5312C92126B004400F0 /* SubscriptionFeatureAvailabilityMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionFeatureAvailabilityMock.swift; sourceTree = ""; }; 1E4DCF4527B6A33600961E25 /* DownloadsListViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsListViewModel.swift; sourceTree = ""; }; 1E4DCF4727B6A35400961E25 /* DownloadsListModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadsListModel.swift; sourceTree = ""; }; 1E4DCF4927B6A38000961E25 /* DownloadListRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DownloadListRepresentable.swift; sourceTree = ""; }; @@ -3256,6 +3257,14 @@ name = Autoconsent; sourceTree = ""; }; + 1E25D5302C921246004400F0 /* Mocks */ = { + isa = PBXGroup; + children = ( + 1E25D5312C92126B004400F0 /* SubscriptionFeatureAvailabilityMock.swift */, + ); + name = Mocks; + sourceTree = ""; + }; 1E4DCF4227B6A29D00961E25 /* View */ = { isa = PBXGroup; children = ( @@ -4055,7 +4064,6 @@ 84E341891E2F7EFB00BDBA6F = { isa = PBXGroup; children = ( - 1E377E8F2C80649C00AA2A2B /* BrowserServicesKit */, EE3B98EB2A963515002F63A0 /* WidgetsExtensionAlpha.entitlements */, 6FB030C7234331B400A10DB9 /* Configuration.xcconfig */, EEB8FDB92A990AEE00EBEDCF /* Configuration-Alpha.xcconfig */, @@ -5917,6 +5925,7 @@ F1BDDBFC2C340D9C00459306 /* Subscription */ = { isa = PBXGroup; children = ( + 1E25D5302C921246004400F0 /* Mocks */, D664C7952B289AA000CBFA76 /* Subscription.storekit */, BDE219E92C457B46005D5884 /* PrivacyProDataReporterTests.swift */, F1BDDBF92C340D9C00459306 /* SubscriptionContainerViewModelTests.swift */, @@ -7461,6 +7470,7 @@ F4B0B78C252CAFF700830156 /* OnboardingWidgetsViewController.swift in Sources */, C17B595A2A03AAD30055F2D1 /* PasswordGenerationPromptViewController.swift in Sources */, 8531A08E1F9950E6000484F0 /* UnprotectedSitesViewController.swift in Sources */, + 1E25D5322C92126B004400F0 /* SubscriptionFeatureAvailabilityMock.swift in Sources */, CBD4F13C279EBF4A00B20FD7 /* HomeMessage.swift in Sources */, 6FB2A67E2C2DAFB4004D20C8 /* NewTabPageGridView.swift in Sources */, 1DEAADEC2BA45B4500E25A97 /* SettingsAccessibilityView.swift in Sources */, @@ -10716,8 +10726,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { - kind = exactVersion; - version = 191.2.0; + branch = "michal/subs-tests"; + kind = branch; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 5c04db7de5..a65b76a29d 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -27,6 +27,15 @@ "version" : "3.0.0" } }, + { + "identity" : "browserserviceskit", + "kind" : "remoteSourceControl", + "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", + "state" : { + "branch" : "michal/subs-tests", + "revision" : "1fcec773c42fe4ce4e753d6845e82df0c6784153" + } + }, { "identity" : "content-scope-scripts", "kind" : "remoteSourceControl", @@ -129,7 +138,7 @@ { "identity" : "swift-argument-parser", "kind" : "remoteSourceControl", - "location" : "https://github.com/apple/swift-argument-parser", + "location" : "https://github.com/apple/swift-argument-parser.git", "state" : { "revision" : "0fbc8848e389af3bb55c182bc19ca9d5dc2f255b", "version" : "1.4.0" diff --git a/DuckDuckGoTests/Subscription/SubscriptionFeatureAvailabilityMock.swift b/DuckDuckGoTests/Subscription/SubscriptionFeatureAvailabilityMock.swift new file mode 100644 index 0000000000..37b7ed5350 --- /dev/null +++ b/DuckDuckGoTests/Subscription/SubscriptionFeatureAvailabilityMock.swift @@ -0,0 +1,20 @@ +// +// SubscriptionFeatureAvailabilityMock.swift +// DuckDuckGo +// +// Copyright © 2024 DuckDuckGo. All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +import Foundation From ba26b370f4ae1fb66a5f86dfb803bcd1a1631785 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Wed, 11 Sep 2024 20:39:11 +0200 Subject: [PATCH 09/17] Test clean up --- .../SubscriptionFeatureAvailabilityMock.swift | 14 ++ ...tionPagesUseSubscriptionFeatureTests.swift | 230 +++++++++++------- 2 files changed, 159 insertions(+), 85 deletions(-) diff --git a/DuckDuckGoTests/Subscription/SubscriptionFeatureAvailabilityMock.swift b/DuckDuckGoTests/Subscription/SubscriptionFeatureAvailabilityMock.swift index 37b7ed5350..e785adf5f5 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionFeatureAvailabilityMock.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionFeatureAvailabilityMock.swift @@ -18,3 +18,17 @@ // import Foundation +@testable import BrowserServicesKit + +class SubscriptionFeatureAvailabilityMock: SubscriptionFeatureAvailability { + var isFeatureAvailable: Bool + var isSubscriptionPurchaseAllowed: Bool + var usesUnifiedFeedbackForm: Bool + + init(isFeatureAvailable: Bool, isSubscriptionPurchaseAllowed: Bool, usesUnifiedFeedbackForm: Bool) { + self.isFeatureAvailable = isFeatureAvailable + self.isSubscriptionPurchaseAllowed = isSubscriptionPurchaseAllowed + self.usesUnifiedFeedbackForm = usesUnifiedFeedbackForm + } + +} diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index df7bf4b055..894949d4ed 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -66,11 +66,11 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } var userDefaults: UserDefaults! - + var accountStorage: AccountKeychainStorageMock! var accessTokenStorage: SubscriptionTokenKeychainStorageMock! var entitlementsCache: UserDefaultsCache<[Entitlement]>! - + var subscriptionService: SubscriptionEndpointServiceMock! var authService: AuthEndpointServiceMock! @@ -90,7 +90,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Mocks subscriptionService = SubscriptionEndpointServiceMock() authService = AuthEndpointServiceMock() - + storePurchaseManager = StorePurchaseManagerMock() subscriptionEnvironment = SubscriptionEnvironment(serviceEnvironment: .production, purchasePlatform: .appStore) @@ -171,6 +171,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // MARK: - Tests for getSubscription func testGetSubscriptionSuccessRefreshingAuthToken() async throws { + // Given ensureUserAuthenticatedState() let newAuthToken = UUID().uuidString @@ -182,89 +183,93 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { externalID: Constants.externalID, id: 1, status: "authenticated")) + // When let result = await feature.getSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) - if let result = result as? [String: String] { - XCTAssertEqual(result[SubscriptionPagesUseSubscriptionFeature.Constants.token], newAuthToken) - XCTAssertEqual(accountManager.authToken, newAuthToken) + // Then + let resultDictionary = try XCTUnwrap(result as? [String: String]) - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - } else { - XCTFail("Incorrect return type") - } + XCTAssertEqual(resultDictionary[SubscriptionPagesUseSubscriptionFeature.Constants.token], newAuthToken) + XCTAssertEqual(accountManager.authToken, newAuthToken) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) } func testGetSubscriptionSuccessWithoutRefreshingAuthToken() async throws { + // Given ensureUserAuthenticatedState() authService.validateTokenResult = .success(Constants.validateTokenResponse) + // When let result = await feature.getSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) - if let result = result as? [String: String] { - XCTAssertEqual(result[SubscriptionPagesUseSubscriptionFeature.Constants.token], Constants.authToken) - XCTAssertEqual(accountManager.authToken, Constants.authToken) + // Then + let resultDictionary = try XCTUnwrap(result as? [String: String]) - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - } else { - XCTFail("Incorrect return type") - } + XCTAssertEqual(resultDictionary[SubscriptionPagesUseSubscriptionFeature.Constants.token], Constants.authToken) + XCTAssertEqual(accountManager.authToken, Constants.authToken) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) } func testGetSubscriptionSuccessErrorWhenUnauthenticated() async throws { + // Given ensureUserUnauthenticatedState() authService.validateTokenResult = .failure(Constants.invalidTokenError) storePurchaseManager.mostRecentTransactionResult = nil + // When let result = await feature.getSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) - if let result = result as? [String: String] { - XCTAssertEqual(result[SubscriptionPagesUseSubscriptionFeature.Constants.token], SubscriptionPagesUseSubscriptionFeature.Constants.empty) - XCTAssertFalse(accountManager.isUserAuthenticated) + // Then + let resultDictionary = try XCTUnwrap(result as? [String: String]) - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - } else { - XCTFail("Incorrect return type") - } + XCTAssertEqual(resultDictionary[SubscriptionPagesUseSubscriptionFeature.Constants.token], SubscriptionPagesUseSubscriptionFeature.Constants.empty) + XCTAssertFalse(accountManager.isUserAuthenticated) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) } // MARK: - Tests for getSubscriptionOptions func testGetSubscriptionOptionsSuccess() async throws { + // Given storePurchaseManager.subscriptionOptionsResult = Constants.subscriptionOptions + // When let result = await feature.getSubscriptionOptions(params: Constants.mockParams, original: Constants.mockScriptMessage) - if let result = result as? SubscriptionOptions { - XCTAssertEqual(result, Constants.subscriptionOptions) + // Then + let subscriptionOptionsResult = try XCTUnwrap(result as? SubscriptionOptions) - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - } else { - XCTFail("Incorrect return type") - } + XCTAssertEqual(subscriptionOptionsResult, Constants.subscriptionOptions) + + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) } func testGetSubscriptionOptionsReturnsEmptyOptionsWhenNoSubscriptionOptions() async throws { + // Given storePurchaseManager.subscriptionOptionsResult = nil + // When let result = await feature.getSubscriptionOptions(params: Constants.mockParams, original: Constants.mockScriptMessage) - if let result = result as? SubscriptionOptions { - XCTAssertEqual(result, SubscriptionOptions.empty) + // Then + let subscriptionOptionsResult = try XCTUnwrap(result as? SubscriptionOptions) + XCTAssertEqual(subscriptionOptionsResult, SubscriptionOptions.empty) - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, .failedToGetSubscriptionOptions) - } else { - XCTFail("Incorrect return type") - } + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, .failedToGetSubscriptionOptions) } func testGetSubscriptionOptionsReturnsEmptyOptionsWhenPurchaseNotAllowed() async throws { + // Given let mockDependencyProvider = MockDependencyProvider() mockDependencyProvider.subscriptionFeatureAvailability = SubscriptionFeatureAvailabilityMock(isFeatureAvailable: true, isSubscriptionPurchaseAllowed: false, @@ -273,21 +278,21 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { storePurchaseManager.subscriptionOptionsResult = Constants.subscriptionOptions + // When let result = await feature.getSubscriptionOptions(params: Constants.mockParams, original: Constants.mockScriptMessage) - if let result = result as? SubscriptionOptions { - XCTAssertEqual(result, SubscriptionOptions.empty) + // Then + let subscriptionOptionsResult = try XCTUnwrap(result as? SubscriptionOptions) + XCTAssertEqual(subscriptionOptionsResult, SubscriptionOptions.empty) - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - } else { - XCTFail("Incorrect return type") - } + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) } // MARK: - Tests for subscriptionSelected func testSubscriptionSelectedSuccessWhenPurchasingFirstTime() async throws { + // Given ensureUserUnauthenticatedState() XCTAssertFalse(accountManager.isUserAuthenticated) @@ -305,8 +310,11 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { entitlements: Constants.entitlements, subscription: SubscriptionMockFactory.subscription)) + // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // Then // TODO: Check pixel fired: DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseAttempt) // DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseSuccess) // UniquePixel.fire(pixel: .privacyProSubscriptionActivated) @@ -318,6 +326,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testSubscriptionSelectedSuccessWhenRepurchasingForExpiredAppleSubscription() async throws { + // Given ensureUserAuthenticatedState() XCTAssertTrue(accountManager.isUserAuthenticated) @@ -338,8 +347,11 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { entitlements: Constants.entitlements, subscription: SubscriptionMockFactory.subscription)) + // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // Then // TODO: Check pixel fired: DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseAttempt) // DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseSuccess) // UniquePixel.fire(pixel: .privacyProSubscriptionActivated) @@ -355,6 +367,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testSubscriptionSelectedSuccessWhenRepurchasingForExpiredStripeSubscription() async throws { + // Given ensureUserAuthenticatedState() XCTAssertTrue(accountManager.isUserAuthenticated) @@ -366,13 +379,16 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { entitlements: Constants.entitlements, subscription: SubscriptionMockFactory.subscription)) + // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + // Then // TODO: Check pixel fired: DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseAttempt) // DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseSuccess) // UniquePixel.fire(pixel: .privacyProSubscriptionActivated) // Pixel.fireAttribution(pixel: .privacyProSuccessfulSubscriptionAttribution, origin: subscriptionAttributionOrigin, privacyProDataReporter: privacyProDataReporter) - + XCTAssertFalse(authService.createAccountCalled) XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) @@ -383,13 +399,16 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testSubscriptionSelectedErrorWhenPurchasingWhenHavingActiveSubscription() async throws { + // Given ensureUserAuthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = true + // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + // Then XCTAssertFalse(storePurchaseManager.purchaseSubscriptionCalled) XCTAssertNil(result) @@ -399,13 +418,16 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testSubscriptionSelectedErrorWhenPurchasingWhenUnauthenticatedAndHavingActiveSubscriptionOnAppleID() async throws { + // Given ensureUserUnauthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = true + // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + // Then XCTAssertFalse(storePurchaseManager.purchaseSubscriptionCalled) XCTAssertNil(result) @@ -415,6 +437,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testSubscriptionSelectedErrorWhenUnauthenticatedAndAccountCreationFails() async throws { + // Given ensureUserUnauthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = false @@ -422,9 +445,11 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { authService.createAccountResult = .failure(Constants.invalidTokenError) + // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + // Then // TODO: Check pixel fired: DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseAttempt) XCTAssertFalse(storePurchaseManager.purchaseSubscriptionCalled) @@ -436,15 +461,18 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testSubscriptionSelectedErrorWhenPurchaseCancelledByUser() async throws { + // Given ensureUserAuthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = false subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.purchaseCancelledByUser) + // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + // Then XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) XCTAssertNil(result) @@ -454,15 +482,18 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testSubscriptionSelectedErrorWhenProductNotFound() async throws { + // Given ensureUserAuthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = false subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.productNotFound) + // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + // Then XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) XCTAssertNil(result) @@ -472,15 +503,18 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testSubscriptionSelectedErrorWhenExternalIDIsNotValidUUID() async throws { + // Given ensureUserAuthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = false subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.externalIDisNotAValidUUID) + // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + // Then XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) XCTAssertNil(result) @@ -490,15 +524,18 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testSubscriptionSelectedErrorWhenPurchaseFailed() async throws { + // Given ensureUserAuthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = false subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.purchaseFailed) + // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + // Then XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) XCTAssertNil(result) @@ -508,15 +545,18 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testSubscriptionSelectedErrorWhenTransactionCannotBeVerified() async throws { + // Given ensureUserAuthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = false subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.transactionCannotBeVerified) + // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + // Then XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) XCTAssertNil(result) @@ -526,15 +566,18 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testSubscriptionSelectedErrorWhenTransactionPendingAuthentication() async throws { + // Given ensureUserAuthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = false subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.transactionPendingAuthentication) + // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + // Then XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) XCTAssertNil(result) @@ -544,15 +587,18 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testSubscriptionSelectedErrorDueToUnknownPurchaseError() async throws { + // Given ensureUserAuthenticatedState() storePurchaseManager.hasActiveSubscriptionResult = false subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredStripeSubscription) storePurchaseManager.purchaseSubscriptionResult = .failure(StorePurchaseManagerError.unknownError) + // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + // Then XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) XCTAssertNil(result) @@ -565,6 +611,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // MARK: - Tests for setSubscription func testSetSubscriptionSuccess() async throws { + // Given ensureUserUnauthenticatedState() authService.getAccessTokenResult = .success(.init(accessToken: Constants.accessToken)) @@ -575,9 +622,11 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { onSetSubscriptionCalled.fulfill() } + // When let setSubscriptionParams = ["token": Constants.authToken] let result = await feature.setSubscription(params: setSubscriptionParams, original: Constants.mockScriptMessage) + // Then XCTAssertEqual(accountManager.authToken, Constants.authToken) XCTAssertEqual(accountManager.accessToken, Constants.accessToken) XCTAssertEqual(accountManager.email, Constants.email) @@ -591,6 +640,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testSetSubscriptionErrorWhenFailedToExchangeToken() async throws { + // Given ensureUserUnauthenticatedState() authService.getAccessTokenResult = .failure(Constants.invalidTokenError) @@ -601,9 +651,11 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { onSetSubscriptionCalled.fulfill() } + // When let setSubscriptionParams = ["token": Constants.authToken] let result = await feature.setSubscription(params: setSubscriptionParams, original: Constants.mockScriptMessage) + // Then XCTAssertNil(accountManager.authToken) XCTAssertFalse(accountManager.isUserAuthenticated) @@ -615,6 +667,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testSetSubscriptionErrorWhenFailedToFetchAccountDetails() async throws { + // Given ensureUserUnauthenticatedState() authService.getAccessTokenResult = .success(.init(accessToken: Constants.accessToken)) @@ -626,9 +679,11 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { onSetSubscriptionCalled.fulfill() } + // When let setSubscriptionParams = ["token": Constants.authToken] let result = await feature.setSubscription(params: setSubscriptionParams, original: Constants.mockScriptMessage) + // Then XCTAssertNil(accountManager.authToken) XCTAssertFalse(accountManager.isUserAuthenticated) @@ -642,6 +697,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // MARK: - Tests for activateSubscription func testActivateSubscriptionTokenSuccess() async throws { + // Given ensureUserAuthenticatedState() let onActivateSubscriptionCalled = expectation(description: "onActivateSubscription") @@ -649,8 +705,10 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { onActivateSubscriptionCalled.fulfill() } + // When let result = await feature.activateSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) + // Then // TODO: Check pixel fired: Pixel.fire(pixel: .privacyProRestorePurchaseOfferPageEntry, debounce: 2) await fulfillment(of: [onActivateSubscriptionCalled], timeout: 0.5) XCTAssertNil(result) @@ -659,6 +717,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // MARK: - Tests for featureSelected func testFeatureSelectedSuccess() async throws { + // Given ensureUserAuthenticatedState() let onFeatureSelectedCalled = expectation(description: "onFeatureSelected") @@ -667,9 +726,11 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(selection, SubscriptionFeatureSelection.itr) } + // When let featureSelectionParams = ["feature": SubscriptionFeatureName.itr] let result = await feature.featureSelected(params: featureSelectionParams, original: Constants.mockScriptMessage) + // Then await fulfillment(of: [onFeatureSelectedCalled], timeout: 0.5) XCTAssertNil(result) } @@ -677,6 +738,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // MARK: - Tests for backToSettings func testBackToSettingsSuccess() async throws { + // Given ensureUserAuthenticatedState() accountStorage.email = nil @@ -690,8 +752,10 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { authService.validateTokenResult = .success(Constants.validateTokenResponse) subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.subscription) + // When let result = await feature.backToSettings(params: Constants.mockParams, original: Constants.mockScriptMessage) + // Then await fulfillment(of: [onBackToSettingsCalled], timeout: 0.5) XCTAssertEqual(accountManager.email, Constants.email) @@ -699,6 +763,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testBackToSettingsErrorOnFetchingAccountDetails() async throws { + // Given ensureUserAuthenticatedState() let onBackToSettingsCalled = expectation(description: "onBackToSettings") @@ -709,8 +774,10 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { authService.validateTokenResult = .failure(Constants.invalidTokenError) + // When let result = await feature.backToSettings(params: Constants.mockParams, original: Constants.mockScriptMessage) + // Then await fulfillment(of: [onBackToSettingsCalled], timeout: 0.5) XCTAssertEqual(feature.transactionError, .generalError) @@ -719,36 +786,37 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // MARK: - Tests for getAccessToken func testGetAccessTokenSuccess() async throws { + // Given ensureUserAuthenticatedState() + // When let result = try await feature.getAccessToken(params: Constants.mockParams, original: Constants.mockScriptMessage) - if let result = result as? [String: String] { - XCTAssertEqual(result[SubscriptionPagesUseSubscriptionFeature.Constants.token], Constants.accessToken) + // Then + let resultDictionary = try XCTUnwrap(result as? [String: String]) + XCTAssertEqual(resultDictionary[SubscriptionPagesUseSubscriptionFeature.Constants.token], Constants.accessToken) - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - } else { - XCTFail("Incorrect return type") - } + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) } func testGetAccessTokenEmptyOnMissingToken() async throws { + // Given ensureUserUnauthenticatedState() XCTAssertNil(accountManager.accessToken) + // When let result = try await feature.getAccessToken(params: Constants.mockParams, original: Constants.mockScriptMessage) - if let result = result as? [String: String] { - XCTAssertEqual(result, [String: String]()) - } else { - XCTFail("Incorrect return type") - } + // Then + let resultDictionary = try XCTUnwrap(result as? [String: String]) + XCTAssertEqual(resultDictionary, [String: String]()) } // MARK: - Tests for restoreAccountFromAppStorePurchase func testRestoreAccountFromAppStorePurchaseSuccess() async throws { + // Given ensureUserUnauthenticatedState() storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS @@ -760,19 +828,18 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { authService.validateTokenResult = .success(Constants.validateTokenResponse) subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.subscription) - do { - try await feature.restoreAccountFromAppStorePurchase() + // When + try await feature.restoreAccountFromAppStorePurchase() - XCTAssertTrue(accountManager.isUserAuthenticated) + // Then + XCTAssertTrue(accountManager.isUserAuthenticated) - XCTAssertEqual(feature.transactionStatus, .idle) - XCTAssertEqual(feature.transactionError, nil) - } catch let error { - XCTFail("Unexpected error \(error)") - } + XCTAssertEqual(feature.transactionStatus, .idle) + XCTAssertEqual(feature.transactionError, nil) } func testRestoreAccountFromAppStorePurchaseErrorDueToExpiredSubscription() async throws { + // Given ensureUserUnauthenticatedState() storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS @@ -784,11 +851,13 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { authService.validateTokenResult = .success(Constants.validateTokenResponse) subscriptionService.getSubscriptionResult = .success(SubscriptionMockFactory.expiredSubscription) + do { + // When try await feature.restoreAccountFromAppStorePurchase() - XCTFail("Unexpected success") } catch let error { + // Then guard let error = error as? SubscriptionPagesUseSubscriptionFeature.UseSubscriptionError else { XCTFail("Unexpected error type") return @@ -803,15 +872,17 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testRestoreAccountFromAppStorePurchaseErrorDueToNoTransaction() async throws { + // Given ensureUserUnauthenticatedState() storePurchaseManager.mostRecentTransactionResult = nil do { + // When try await feature.restoreAccountFromAppStorePurchase() - XCTFail("Unexpected success") } catch let error { + // Then guard let error = error as? SubscriptionPagesUseSubscriptionFeature.UseSubscriptionError else { XCTFail("Unexpected error type") return @@ -826,16 +897,18 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } func testRestoreAccountFromAppStorePurchaseErrorDueToOtherError() async throws { + // Given ensureUserUnauthenticatedState() storePurchaseManager.mostRecentTransactionResult = Constants.mostRecentTransactionJWS authService.storeLoginResult = .failure(Constants.invalidTokenError) do { + // When try await feature.restoreAccountFromAppStorePurchase() - XCTFail("Unexpected success") } catch let error { + // Then guard let error = error as? SubscriptionPagesUseSubscriptionFeature.UseSubscriptionError else { XCTFail("Unexpected error type") return @@ -864,16 +937,3 @@ extension SubscriptionPagesUseSubscriptionFeatureTests { try? accountStorage.clearAuthenticationState() } } - -class SubscriptionFeatureAvailabilityMock: SubscriptionFeatureAvailability { - var isFeatureAvailable: Bool - var isSubscriptionPurchaseAllowed: Bool - var usesUnifiedFeedbackForm: Bool - - init(isFeatureAvailable: Bool, isSubscriptionPurchaseAllowed: Bool, usesUnifiedFeedbackForm: Bool) { - self.isFeatureAvailable = isFeatureAvailable - self.isSubscriptionPurchaseAllowed = isSubscriptionPurchaseAllowed - self.usesUnifiedFeedbackForm = usesUnifiedFeedbackForm - } - -} From 06fade9ee0a5af0b01a8de2c86a5f4d924a46168 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 12 Sep 2024 21:02:33 +0200 Subject: [PATCH 10/17] Add checks for pixels fired --- Core/DailyPixel.swift | 2 +- ...tionPagesUseSubscriptionFeatureTests.swift | 165 ++++++++++++++++-- 2 files changed, 148 insertions(+), 19 deletions(-) diff --git a/Core/DailyPixel.swift b/Core/DailyPixel.swift index db0b5f0b33..98d0f037e9 100644 --- a/Core/DailyPixel.swift +++ b/Core/DailyPixel.swift @@ -43,7 +43,7 @@ public final class DailyPixel { } - private static let storage: UserDefaults = UserDefaults(suiteName: Constant.dailyPixelStorageIdentifier)! + public static let storage: UserDefaults = UserDefaults(suiteName: Constant.dailyPixelStorageIdentifier)! /// Sends a given Pixel once per day. /// This is useful in situations where pixels receive spikes in volume, as the daily pixel can be used to determine how many users are actually affected. diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index 894949d4ed..f886c01226 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -19,11 +19,15 @@ import XCTest @testable import DuckDuckGo +@testable import Core @testable import Subscription import SubscriptionTestingUtilities import Common import WebKit import BrowserServicesKit +import OHHTTPStubs +import OHHTTPStubsSwift +import os.log final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { @@ -86,7 +90,27 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { var feature: SubscriptionPagesUseSubscriptionFeature! + var pixelsFired: [String] = [] + override func setUpWithError() throws { + // Pixels + Pixel.isDryRun = false + stub(condition: isHost("improving.duckduckgo.com")) { request -> HTTPStubsResponse in + if let path = request.url?.path { + let pixelName = path.dropping(prefix: "/t/") + .dropping(suffix: "_ios_phone") + .dropping(suffix: "_ios_tablet") + self.pixelsFired.append(pixelName) + } + + return HTTPStubsResponse(data: Data(), statusCode: 200, headers: nil) + } + + // Reset all daily pixel storage + [Pixel.storage, DailyPixel.storage, UniquePixel.storage].forEach { storage in + storage.dictionaryRepresentation().keys.forEach(storage.removeObject(forKey:)) + } + // Mocks subscriptionService = SubscriptionEndpointServiceMock() authService = AuthEndpointServiceMock() @@ -141,6 +165,19 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { } override func tearDownWithError() throws { + Pixel.isDryRun = true + if !pixelsFired.isEmpty { + Logger.general.log("= pixels =") + + pixelsFired.forEach { pixel in + Logger.general.log("\(String(describing: pixel))") + } + + Logger.general.log("==========") + } + pixelsFired.removeAll() + HTTPStubs.removeAllStubs() + AppDependencyProvider.shared = AppDependencyProvider() subscriptionService = nil @@ -194,6 +231,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) + + XCTAssertPrivacyPixelsFired([]) } func testGetSubscriptionSuccessWithoutRefreshingAuthToken() async throws { @@ -213,6 +252,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) + + XCTAssertPrivacyPixelsFired([]) } func testGetSubscriptionSuccessErrorWhenUnauthenticated() async throws { @@ -233,6 +274,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) + + XCTAssertPrivacyPixelsFired([]) } // MARK: - Tests for getSubscriptionOptions @@ -251,6 +294,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) + + XCTAssertPrivacyPixelsFired([]) } func testGetSubscriptionOptionsReturnsEmptyOptionsWhenNoSubscriptionOptions() async throws { @@ -266,6 +311,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .failedToGetSubscriptionOptions) + + XCTAssertPrivacyPixelsFired([]) } func testGetSubscriptionOptionsReturnsEmptyOptionsWhenPurchaseNotAllowed() async throws { @@ -287,6 +334,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) + + XCTAssertPrivacyPixelsFired([]) } // MARK: - Tests for subscriptionSelected @@ -315,14 +364,17 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) // Then - // TODO: Check pixel fired: DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseAttempt) - // DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseSuccess) - // UniquePixel.fire(pixel: .privacyProSubscriptionActivated) - // Pixel.fireAttribution(pixel: .privacyProSuccessfulSubscriptionAttribution, origin: subscriptionAttributionOrigin, privacyProDataReporter: privacyProDataReporter) XCTAssertNil(result) XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) + + XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c", + Pixel.Event.privacyProPurchaseSuccess.name + "_d", + Pixel.Event.privacyProPurchaseSuccess.name + "_c", + Pixel.Event.privacyProSubscriptionActivated.name, + Pixel.Event.privacyProSuccessfulSubscriptionAttribution.name]) } func testSubscriptionSelectedSuccessWhenRepurchasingForExpiredAppleSubscription() async throws { @@ -352,11 +404,6 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) // Then - // TODO: Check pixel fired: DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseAttempt) - // DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseSuccess) - // UniquePixel.fire(pixel: .privacyProSubscriptionActivated) - // Pixel.fireAttribution(pixel: .privacyProSuccessfulSubscriptionAttribution, origin: subscriptionAttributionOrigin, privacyProDataReporter: privacyProDataReporter) - XCTAssertFalse(authService.createAccountCalled) XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) @@ -364,6 +411,13 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) + + XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c", + Pixel.Event.privacyProPurchaseSuccess.name + "_d", + Pixel.Event.privacyProPurchaseSuccess.name + "_c", + Pixel.Event.privacyProSubscriptionActivated.name, + Pixel.Event.privacyProSuccessfulSubscriptionAttribution.name]) } func testSubscriptionSelectedSuccessWhenRepurchasingForExpiredStripeSubscription() async throws { @@ -384,11 +438,6 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) // Then - // TODO: Check pixel fired: DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseAttempt) - // DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseSuccess) - // UniquePixel.fire(pixel: .privacyProSubscriptionActivated) - // Pixel.fireAttribution(pixel: .privacyProSuccessfulSubscriptionAttribution, origin: subscriptionAttributionOrigin, privacyProDataReporter: privacyProDataReporter) - XCTAssertFalse(authService.createAccountCalled) XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) @@ -396,6 +445,12 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) + XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c", + Pixel.Event.privacyProPurchaseSuccess.name + "_d", + Pixel.Event.privacyProPurchaseSuccess.name + "_c", + Pixel.Event.privacyProSubscriptionActivated.name, + Pixel.Event.privacyProSuccessfulSubscriptionAttribution.name]) } func testSubscriptionSelectedErrorWhenPurchasingWhenHavingActiveSubscription() async throws { @@ -415,6 +470,10 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .hasActiveSubscription) + + XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c", + Pixel.Event.privacyProRestoreAfterPurchaseAttempt.name]) } func testSubscriptionSelectedErrorWhenPurchasingWhenUnauthenticatedAndHavingActiveSubscriptionOnAppleID() async throws { @@ -434,6 +493,10 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .hasActiveSubscription) + + XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c", + Pixel.Event.privacyProRestoreAfterPurchaseAttempt.name]) } func testSubscriptionSelectedErrorWhenUnauthenticatedAndAccountCreationFails() async throws { @@ -450,14 +513,15 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) // Then - // TODO: Check pixel fired: DailyPixel.fireDailyAndCount(pixel: .privacyProPurchaseAttempt) - XCTAssertFalse(storePurchaseManager.purchaseSubscriptionCalled) XCTAssertNil(result) XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .accountCreationFailed) + + XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) } func testSubscriptionSelectedErrorWhenPurchaseCancelledByUser() async throws { @@ -479,6 +543,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .cancelledByUser) + + XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) } func testSubscriptionSelectedErrorWhenProductNotFound() async throws { @@ -500,6 +567,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .purchaseFailed) + + XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) } func testSubscriptionSelectedErrorWhenExternalIDIsNotValidUUID() async throws { @@ -521,6 +591,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .purchaseFailed) + + XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) } func testSubscriptionSelectedErrorWhenPurchaseFailed() async throws { @@ -542,6 +615,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .purchaseFailed) + + XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) } func testSubscriptionSelectedErrorWhenTransactionCannotBeVerified() async throws { @@ -563,6 +639,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .purchaseFailed) + + XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) } func testSubscriptionSelectedErrorWhenTransactionPendingAuthentication() async throws { @@ -584,6 +663,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .purchaseFailed) + + XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) } func testSubscriptionSelectedErrorDueToUnknownPurchaseError() async throws { @@ -605,8 +687,10 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .purchaseFailed) - } + XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + } // MARK: - Tests for setSubscription @@ -637,6 +721,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) + + XCTAssertPrivacyPixelsFired([]) } func testSetSubscriptionErrorWhenFailedToExchangeToken() async throws { @@ -664,6 +750,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .failedToSetSubscription) + + XCTAssertPrivacyPixelsFired([]) } func testSetSubscriptionErrorWhenFailedToFetchAccountDetails() async throws { @@ -692,6 +780,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .failedToSetSubscription) + + XCTAssertPrivacyPixelsFired([]) } // MARK: - Tests for activateSubscription @@ -709,9 +799,10 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { let result = await feature.activateSubscription(params: Constants.mockParams, original: Constants.mockScriptMessage) // Then - // TODO: Check pixel fired: Pixel.fire(pixel: .privacyProRestorePurchaseOfferPageEntry, debounce: 2) await fulfillment(of: [onActivateSubscriptionCalled], timeout: 0.5) XCTAssertNil(result) + + XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProRestorePurchaseOfferPageEntry.name]) } // MARK: - Tests for featureSelected @@ -733,6 +824,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Then await fulfillment(of: [onFeatureSelectedCalled], timeout: 0.5) XCTAssertNil(result) + + XCTAssertPrivacyPixelsFired([]) } // MARK: - Tests for backToSettings @@ -760,6 +853,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(accountManager.email, Constants.email) XCTAssertNil(result) + + XCTAssertPrivacyPixelsFired([]) } func testBackToSettingsErrorOnFetchingAccountDetails() async throws { @@ -782,6 +877,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionError, .generalError) XCTAssertNil(result) + + XCTAssertPrivacyPixelsFired([]) } // MARK: - Tests for getAccessToken @@ -798,6 +895,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) + + XCTAssertPrivacyPixelsFired([]) } func testGetAccessTokenEmptyOnMissingToken() async throws { @@ -811,6 +910,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // Then let resultDictionary = try XCTUnwrap(result as? [String: String]) XCTAssertEqual(resultDictionary, [String: String]()) + + XCTAssertPrivacyPixelsFired([]) } // MARK: - Tests for restoreAccountFromAppStorePurchase @@ -836,6 +937,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) + + XCTAssertPrivacyPixelsFired([]) } func testRestoreAccountFromAppStorePurchaseErrorDueToExpiredSubscription() async throws { @@ -868,6 +971,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) + + XCTAssertPrivacyPixelsFired([]) } } @@ -893,6 +998,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) + + XCTAssertPrivacyPixelsFired([]) } } @@ -919,6 +1026,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) + + XCTAssertPrivacyPixelsFired([]) } } } @@ -936,4 +1045,24 @@ extension SubscriptionPagesUseSubscriptionFeatureTests { try? accessTokenStorage.removeAccessToken() try? accountStorage.clearAuthenticationState() } + + public func XCTAssertPrivacyPixelsFired(_ pixels: [String], file: StaticString = #file, line: UInt = #line) { + let pixelsFired = Set(pixelsFired) + let expectedPixels = Set(pixels) + + // Assert expected pixels were fired + XCTAssertTrue(expectedPixels.isSubset(of: pixelsFired), + "Expected Privacy Pro pixels were not fired: \(expectedPixels.subtracting(pixelsFired))", + file: file, + line: line) + + // Assert no other Privacy Pro pixels were fired except the expected + let privacyProPixelPrefix = "m_privacy-pro" + let otherPixels = pixelsFired.subtracting(expectedPixels) + let otherPrivacyProPixels = otherPixels.filter { $0.hasPrefix(privacyProPixelPrefix) } + XCTAssertTrue(otherPrivacyProPixels.isEmpty, + "Unexpected Privacy Pro pixels fired: \(otherPrivacyProPixels)", + file: file, + line: line) + } } From 8996bb461d68fb82deb33a5109355f24f3cd14a0 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 12 Sep 2024 21:03:05 +0200 Subject: [PATCH 11/17] Remove pixel logging --- .../SubscriptionPagesUseSubscriptionFeatureTests.swift | 9 --------- 1 file changed, 9 deletions(-) diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index f886c01226..e9e77eedbb 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -166,15 +166,6 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { override func tearDownWithError() throws { Pixel.isDryRun = true - if !pixelsFired.isEmpty { - Logger.general.log("= pixels =") - - pixelsFired.forEach { pixel in - Logger.general.log("\(String(describing: pixel))") - } - - Logger.general.log("==========") - } pixelsFired.removeAll() HTTPStubs.removeAllStubs() From d337c8d8f991ba443a09ccffe8d6d79b10125e5d Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 12 Sep 2024 21:11:11 +0200 Subject: [PATCH 12/17] Update tests with missing given-when-then --- .../StorePurchaseManagerTests.swift | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift b/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift index f5df31898f..d8f38e823f 100644 --- a/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift +++ b/DuckDuckGoTests/Subscription/StorePurchaseManagerTests.swift @@ -50,20 +50,25 @@ final class StorePurchaseManagerTests: XCTestCase { } func testSubscriptionOptionsWhenNoCachedProducts() async throws { + // When let subscriptionOptions = await storePurchaseManager.subscriptionOptions() + // Then XCTAssertNil(subscriptionOptions) XCTAssertFalse(storePurchaseManager.areProductsAvailable) } func testSubscriptionOptionsWhenAvailableProductsWereUpdated() async throws { + // Given await storePurchaseManager.updateAvailableProducts() + // When guard let subscriptionOptions = await storePurchaseManager.subscriptionOptions() else { XCTFail("Expected subscription options") return } + // Then XCTAssertEqual(subscriptionOptions.options.count, 2) XCTAssertEqual(subscriptionOptions.features.count, SubscriptionFeatureName.allCases.count) XCTAssertTrue(storePurchaseManager.areProductsAvailable) @@ -74,17 +79,23 @@ final class StorePurchaseManagerTests: XCTestCase { } func testHasActiveSubscriptionIsFalseWithoutPurchase() async throws { + // When let hasActiveSubscription = await storePurchaseManager.hasActiveSubscription() + + // Then XCTAssertFalse(hasActiveSubscription) } func testPurchaseSubscription() async throws { + // Given await storePurchaseManager.updateAvailableProducts() XCTAssertEqual(storePurchaseManager.purchasedProductIDs, []) + // When let result = await storePurchaseManager.purchaseSubscription(with: Constants.yearlySubscriptionID, externalID: Constants.externalID) + // Then switch result { case .success: XCTAssertTrue(storePurchaseManager.purchaseQueue.isEmpty) @@ -102,10 +113,13 @@ final class StorePurchaseManagerTests: XCTestCase { } func testPurchaseSubscriptionFailureWithoutValidProductID() async throws { + // Given await storePurchaseManager.updateAvailableProducts() + // When let result = await storePurchaseManager.purchaseSubscription(with: "", externalID: Constants.externalID) + // Then switch result { case .success: XCTFail("Unexpected success") @@ -115,13 +129,16 @@ final class StorePurchaseManagerTests: XCTestCase { } func testPurchaseSubscriptionFailureWithoutValidUUID() async throws { + // Given await storePurchaseManager.updateAvailableProducts() let invalidUUID = "a" XCTAssertNil(UUID(uuidString: invalidUUID)) + // When let result = await storePurchaseManager.purchaseSubscription(with: Constants.yearlySubscriptionID, externalID: invalidUUID) + // Then switch result { case .success: XCTFail("Unexpected success") @@ -132,13 +149,16 @@ final class StorePurchaseManagerTests: XCTestCase { @available(iOS 17.0, *) func testPurchaseSubscriptionFailure() async throws { + // Given try? await session.setSimulatedError(SKTestFailures.Purchase.purchase(.productUnavailable), forAPI: StoreKitPurchaseAPI.purchase) await storePurchaseManager.updateAvailableProducts() + // When let result = await storePurchaseManager.purchaseSubscription(with: Constants.yearlySubscriptionID, externalID: Constants.externalID) + // Then switch result { case .success: XCTFail("Unexpected success") @@ -167,5 +187,4 @@ private final class StoreKitHelpers { return safe } } - } From 7d89b755a55e219f3404da03496671bdc231eece Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Thu, 12 Sep 2024 21:21:08 +0200 Subject: [PATCH 13/17] Make vars public --- .../SubscriptionFeatureAvailabilityMock.swift | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/DuckDuckGoTests/Subscription/SubscriptionFeatureAvailabilityMock.swift b/DuckDuckGoTests/Subscription/SubscriptionFeatureAvailabilityMock.swift index e785adf5f5..70d007616f 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionFeatureAvailabilityMock.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionFeatureAvailabilityMock.swift @@ -20,12 +20,12 @@ import Foundation @testable import BrowserServicesKit -class SubscriptionFeatureAvailabilityMock: SubscriptionFeatureAvailability { - var isFeatureAvailable: Bool - var isSubscriptionPurchaseAllowed: Bool - var usesUnifiedFeedbackForm: Bool +public final class SubscriptionFeatureAvailabilityMock: SubscriptionFeatureAvailability { + public var isFeatureAvailable: Bool + public var isSubscriptionPurchaseAllowed: Bool + public var usesUnifiedFeedbackForm: Bool - init(isFeatureAvailable: Bool, isSubscriptionPurchaseAllowed: Bool, usesUnifiedFeedbackForm: Bool) { + public init(isFeatureAvailable: Bool, isSubscriptionPurchaseAllowed: Bool, usesUnifiedFeedbackForm: Bool) { self.isFeatureAvailable = isFeatureAvailable self.isSubscriptionPurchaseAllowed = isSubscriptionPurchaseAllowed self.usesUnifiedFeedbackForm = usesUnifiedFeedbackForm From 8bfbcef0f2fe63710ce8d9459040b3b1f0e19b81 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 17 Sep 2024 17:53:58 +0200 Subject: [PATCH 14/17] Update BSK --- DuckDuckGo.xcodeproj/project.pbxproj | 2 +- .../project.xcworkspace/xcshareddata/swiftpm/Package.resolved | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 634f9ba67a..6dfa49353d 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -10927,7 +10927,7 @@ repositoryURL = "https://github.com/DuckDuckGo/BrowserServicesKit"; requirement = { kind = exactVersion; - version = 196.1.0; + version = 196.2.0; }; }; 9F8FE9472BAE50E50071E372 /* XCRemoteSwiftPackageReference "lottie-spm" */ = { diff --git a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 09bdc9694d..cf5b1aae12 100644 --- a/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/DuckDuckGo.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -32,8 +32,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/DuckDuckGo/BrowserServicesKit", "state" : { - "revision" : "f7083a3c74a4aa1f6a0f4ab65265eb2f422a2cf0", - "version" : "196.1.0" + "revision" : "32a2ec64385543ccfbaaafbfe9545543a2c06aac", + "version" : "196.2.0" } }, { From 07d25f42e7f8c95ea22cf62b0cefd55b0dfc72ff Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 17 Sep 2024 18:03:46 +0200 Subject: [PATCH 15/17] Fix tests --- .../NetworkProtectionFeatureVisibilityTests.swift | 2 +- .../SubscriptionPagesUseSubscriptionFeatureTests.swift | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift b/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift index 1ccbd0f91a..04b77fcfa8 100644 --- a/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift +++ b/DuckDuckGoTests/NetworkProtectionFeatureVisibilityTests.swift @@ -64,7 +64,7 @@ struct NetworkProtectionFeatureVisibilityMocks: NetworkProtectionFeatureVisibili init(with options: Options) { self.options = options - let subscriptionAppGroup = Bundle.main.appGroup(bundle: .subs) + let subscriptionAppGroup = "NetworkProtectionFeatureVisibilityTests" let subscriptionUserDefaults = UserDefaults(suiteName: subscriptionAppGroup)! let subscriptionEnvironment = DefaultSubscriptionManager.getSavedOrDefaultEnvironment(userDefaults: subscriptionUserDefaults) let entitlementsCache = UserDefaultsCache<[Entitlement]>(userDefaults: subscriptionUserDefaults, diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index e9e77eedbb..9324046401 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -354,6 +354,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + try? await Task.sleep(seconds: 1) + // Then XCTAssertNil(result) @@ -393,6 +395,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + + try? await Task.sleep(seconds: 1) // Then XCTAssertFalse(authService.createAccountCalled) @@ -428,6 +432,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) + try? await Task.sleep(seconds: 1) + // Then XCTAssertFalse(authService.createAccountCalled) XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) From 6a3099e5d6dbeb76e38536774139c5a93b411f43 Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 17 Sep 2024 20:06:07 +0200 Subject: [PATCH 16/17] Fix target membership --- DuckDuckGo.xcodeproj/project.pbxproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/DuckDuckGo.xcodeproj/project.pbxproj b/DuckDuckGo.xcodeproj/project.pbxproj index 6dfa49353d..0fcf5016ef 100644 --- a/DuckDuckGo.xcodeproj/project.pbxproj +++ b/DuckDuckGo.xcodeproj/project.pbxproj @@ -63,7 +63,6 @@ 1E1D8B6C29953CE300C96994 /* autoconsent-test-page-banner.html in Resources */ = {isa = PBXBuildFile; fileRef = 1E1D8B6929953CE300C96994 /* autoconsent-test-page-banner.html */; }; 1E24295E293F57FA00584836 /* LottieView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E24295D293F57FA00584836 /* LottieView.swift */; }; 1E242960293F585300584836 /* cookie-icon-animated-40-light.json in Resources */ = {isa = PBXBuildFile; fileRef = 1E24295F293F585300584836 /* cookie-icon-animated-40-light.json */; }; - 1E25D5322C92126B004400F0 /* SubscriptionFeatureAvailabilityMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E25D5312C92126B004400F0 /* SubscriptionFeatureAvailabilityMock.swift */; }; 1E4DCF4627B6A33600961E25 /* DownloadsListViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4DCF4527B6A33600961E25 /* DownloadsListViewModel.swift */; }; 1E4DCF4827B6A35400961E25 /* DownloadsListModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4DCF4727B6A35400961E25 /* DownloadsListModel.swift */; }; 1E4DCF4A27B6A38000961E25 /* DownloadListRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E4DCF4927B6A38000961E25 /* DownloadListRepresentable.swift */; }; @@ -103,6 +102,7 @@ 1E9529A12C4E748B006E80D4 /* UINavigationControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E9529A02C4E748B006E80D4 /* UINavigationControllerExtension.swift */; }; 1EA51376286596A000493C6A /* PrivacyIconLogic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA51375286596A000493C6A /* PrivacyIconLogic.swift */; }; 1EA513782866039400493C6A /* TrackerAnimationLogic.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EA513772866039400493C6A /* TrackerAnimationLogic.swift */; }; + 1EAABE712C99FC75003F5137 /* SubscriptionFeatureAvailabilityMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E25D5312C92126B004400F0 /* SubscriptionFeatureAvailabilityMock.swift */; }; 1EC458462948932500CB2B13 /* UIHostingControllerExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EC458452948932500CB2B13 /* UIHostingControllerExtension.swift */; }; 1EDE39D22705D4A200C99C72 /* FileSizeDebugViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EDE39D12705D4A100C99C72 /* FileSizeDebugViewController.swift */; }; 1EE411F12857C3640003FE64 /* TrackerAnimationImageProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1EE411F02857C3640003FE64 /* TrackerAnimationImageProvider.swift */; }; @@ -7653,7 +7653,6 @@ F4B0B78C252CAFF700830156 /* OnboardingWidgetsViewController.swift in Sources */, C17B595A2A03AAD30055F2D1 /* PasswordGenerationPromptViewController.swift in Sources */, 8531A08E1F9950E6000484F0 /* UnprotectedSitesViewController.swift in Sources */, - 1E25D5322C92126B004400F0 /* SubscriptionFeatureAvailabilityMock.swift in Sources */, CBD4F13C279EBF4A00B20FD7 /* HomeMessage.swift in Sources */, 6FB2A67E2C2DAFB4004D20C8 /* NewTabPageGridView.swift in Sources */, 1DEAADEC2BA45B4500E25A97 /* SettingsAccessibilityView.swift in Sources */, @@ -7967,6 +7966,7 @@ BDFF03262BA3DA4900F324C9 /* NetworkProtectionFeatureVisibilityTests.swift in Sources */, 0283A2042C6E572F00508FBD /* BrokenSitePromptLimiterTests.swift in Sources */, D62EC3BA2C246A7000FC9D04 /* YoutublePlayerNavigationHandlerTests.swift in Sources */, + 1EAABE712C99FC75003F5137 /* SubscriptionFeatureAvailabilityMock.swift in Sources */, 8341D807212D5E8D000514C2 /* HashExtensionTest.swift in Sources */, C1D21E2F293A599C006E5A05 /* AutofillLoginSessionTests.swift in Sources */, 85D2187924BF6B8B004373D2 /* FaviconSourcesProviderTests.swift in Sources */, From 26c858c2cff34322802b627c195f0df0127a0e2f Mon Sep 17 00:00:00 2001 From: Michal Smaga Date: Tue, 17 Sep 2024 21:16:53 +0200 Subject: [PATCH 17/17] Wait for pixels before asserting --- ...tionPagesUseSubscriptionFeatureTests.swift | 128 +++++++++--------- 1 file changed, 62 insertions(+), 66 deletions(-) diff --git a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift index 9324046401..a3b80166f4 100644 --- a/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift +++ b/DuckDuckGoTests/Subscription/SubscriptionPagesUseSubscriptionFeatureTests.swift @@ -223,7 +223,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } func testGetSubscriptionSuccessWithoutRefreshingAuthToken() async throws { @@ -244,7 +244,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } func testGetSubscriptionSuccessErrorWhenUnauthenticated() async throws { @@ -266,7 +266,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } // MARK: - Tests for getSubscriptionOptions @@ -286,7 +286,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } func testGetSubscriptionOptionsReturnsEmptyOptionsWhenNoSubscriptionOptions() async throws { @@ -303,7 +303,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .failedToGetSubscriptionOptions) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } func testGetSubscriptionOptionsReturnsEmptyOptionsWhenPurchaseNotAllowed() async throws { @@ -326,7 +326,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } // MARK: - Tests for subscriptionSelected @@ -354,20 +354,18 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) - try? await Task.sleep(seconds: 1) - // Then XCTAssertNil(result) XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) - XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c", - Pixel.Event.privacyProPurchaseSuccess.name + "_d", - Pixel.Event.privacyProPurchaseSuccess.name + "_c", - Pixel.Event.privacyProSubscriptionActivated.name, - Pixel.Event.privacyProSuccessfulSubscriptionAttribution.name]) + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c", + Pixel.Event.privacyProPurchaseSuccess.name + "_d", + Pixel.Event.privacyProPurchaseSuccess.name + "_c", + Pixel.Event.privacyProSubscriptionActivated.name, + Pixel.Event.privacyProSuccessfulSubscriptionAttribution.name]) } func testSubscriptionSelectedSuccessWhenRepurchasingForExpiredAppleSubscription() async throws { @@ -395,8 +393,6 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { // When let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) - - try? await Task.sleep(seconds: 1) // Then XCTAssertFalse(authService.createAccountCalled) @@ -407,12 +403,12 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) - XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c", - Pixel.Event.privacyProPurchaseSuccess.name + "_d", - Pixel.Event.privacyProPurchaseSuccess.name + "_c", - Pixel.Event.privacyProSubscriptionActivated.name, - Pixel.Event.privacyProSuccessfulSubscriptionAttribution.name]) + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c", + Pixel.Event.privacyProPurchaseSuccess.name + "_d", + Pixel.Event.privacyProPurchaseSuccess.name + "_c", + Pixel.Event.privacyProSubscriptionActivated.name, + Pixel.Event.privacyProSuccessfulSubscriptionAttribution.name]) } func testSubscriptionSelectedSuccessWhenRepurchasingForExpiredStripeSubscription() async throws { @@ -432,8 +428,6 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { let subscriptionSelectedParams = ["id": "some-subscription-id"] let result = await feature.subscriptionSelected(params: subscriptionSelectedParams, original: Constants.mockScriptMessage) - try? await Task.sleep(seconds: 1) - // Then XCTAssertFalse(authService.createAccountCalled) XCTAssertTrue(storePurchaseManager.purchaseSubscriptionCalled) @@ -442,12 +436,12 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) - XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c", - Pixel.Event.privacyProPurchaseSuccess.name + "_d", - Pixel.Event.privacyProPurchaseSuccess.name + "_c", - Pixel.Event.privacyProSubscriptionActivated.name, - Pixel.Event.privacyProSuccessfulSubscriptionAttribution.name]) + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c", + Pixel.Event.privacyProPurchaseSuccess.name + "_d", + Pixel.Event.privacyProPurchaseSuccess.name + "_c", + Pixel.Event.privacyProSubscriptionActivated.name, + Pixel.Event.privacyProSuccessfulSubscriptionAttribution.name]) } func testSubscriptionSelectedErrorWhenPurchasingWhenHavingActiveSubscription() async throws { @@ -468,9 +462,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .hasActiveSubscription) - XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c", - Pixel.Event.privacyProRestoreAfterPurchaseAttempt.name]) + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c", + Pixel.Event.privacyProRestoreAfterPurchaseAttempt.name]) } func testSubscriptionSelectedErrorWhenPurchasingWhenUnauthenticatedAndHavingActiveSubscriptionOnAppleID() async throws { @@ -491,9 +485,9 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .hasActiveSubscription) - XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c", - Pixel.Event.privacyProRestoreAfterPurchaseAttempt.name]) + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c", + Pixel.Event.privacyProRestoreAfterPurchaseAttempt.name]) } func testSubscriptionSelectedErrorWhenUnauthenticatedAndAccountCreationFails() async throws { @@ -517,8 +511,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .accountCreationFailed) - XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) } func testSubscriptionSelectedErrorWhenPurchaseCancelledByUser() async throws { @@ -541,8 +535,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .cancelledByUser) - XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) } func testSubscriptionSelectedErrorWhenProductNotFound() async throws { @@ -565,8 +559,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .purchaseFailed) - XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) } func testSubscriptionSelectedErrorWhenExternalIDIsNotValidUUID() async throws { @@ -589,8 +583,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .purchaseFailed) - XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) } func testSubscriptionSelectedErrorWhenPurchaseFailed() async throws { @@ -613,8 +607,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .purchaseFailed) - XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) } func testSubscriptionSelectedErrorWhenTransactionCannotBeVerified() async throws { @@ -637,8 +631,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .purchaseFailed) - XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) } func testSubscriptionSelectedErrorWhenTransactionPendingAuthentication() async throws { @@ -661,8 +655,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .purchaseFailed) - XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) } func testSubscriptionSelectedErrorDueToUnknownPurchaseError() async throws { @@ -685,8 +679,8 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .purchaseFailed) - XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", - Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProPurchaseAttempt.name + "_d", + Pixel.Event.privacyProPurchaseAttempt.name + "_c"]) } // MARK: - Tests for setSubscription @@ -719,7 +713,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } func testSetSubscriptionErrorWhenFailedToExchangeToken() async throws { @@ -748,7 +742,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .failedToSetSubscription) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } func testSetSubscriptionErrorWhenFailedToFetchAccountDetails() async throws { @@ -778,7 +772,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, .failedToSetSubscription) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } // MARK: - Tests for activateSubscription @@ -799,7 +793,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { await fulfillment(of: [onActivateSubscriptionCalled], timeout: 0.5) XCTAssertNil(result) - XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProRestorePurchaseOfferPageEntry.name]) + await XCTAssertPrivacyPixelsFired([Pixel.Event.privacyProRestorePurchaseOfferPageEntry.name]) } // MARK: - Tests for featureSelected @@ -822,7 +816,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { await fulfillment(of: [onFeatureSelectedCalled], timeout: 0.5) XCTAssertNil(result) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } // MARK: - Tests for backToSettings @@ -851,7 +845,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(accountManager.email, Constants.email) XCTAssertNil(result) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } func testBackToSettingsErrorOnFetchingAccountDetails() async throws { @@ -875,7 +869,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionError, .generalError) XCTAssertNil(result) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } // MARK: - Tests for getAccessToken @@ -893,7 +887,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } func testGetAccessTokenEmptyOnMissingToken() async throws { @@ -908,7 +902,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { let resultDictionary = try XCTUnwrap(result as? [String: String]) XCTAssertEqual(resultDictionary, [String: String]()) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } // MARK: - Tests for restoreAccountFromAppStorePurchase @@ -935,7 +929,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } func testRestoreAccountFromAppStorePurchaseErrorDueToExpiredSubscription() async throws { @@ -969,7 +963,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } } @@ -996,7 +990,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } } @@ -1024,7 +1018,7 @@ final class SubscriptionPagesUseSubscriptionFeatureTests: XCTestCase { XCTAssertEqual(feature.transactionStatus, .idle) XCTAssertEqual(feature.transactionError, nil) - XCTAssertPrivacyPixelsFired([]) + await XCTAssertPrivacyPixelsFired([]) } } } @@ -1043,7 +1037,9 @@ extension SubscriptionPagesUseSubscriptionFeatureTests { try? accountStorage.clearAuthenticationState() } - public func XCTAssertPrivacyPixelsFired(_ pixels: [String], file: StaticString = #file, line: UInt = #line) { + public func XCTAssertPrivacyPixelsFired(_ pixels: [String], file: StaticString = #file, line: UInt = #line) async { + try? await Task.sleep(seconds: 0.1) + let pixelsFired = Set(pixelsFired) let expectedPixels = Set(pixels)