diff --git a/Modules/Sources/Networking/Mapper/JetpackConnectionProvisionMapper.swift b/Modules/Sources/Networking/Mapper/JetpackConnectionProvisionMapper.swift index 8f8000deb90..a59ee081379 100644 --- a/Modules/Sources/Networking/Mapper/JetpackConnectionProvisionMapper.swift +++ b/Modules/Sources/Networking/Mapper/JetpackConnectionProvisionMapper.swift @@ -18,4 +18,11 @@ public struct JetpackConnectionProvisionResponse: Decodable { public let userId: Int64 public let scope: String public let secret: String + + /// periphery: ignore - used in test module + public init(userId: Int64, scope: String, secret: String) { + self.userId = userId + self.scope = scope + self.secret = secret + } } diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index ea7a5b57e2f..b687b52f809 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -3,6 +3,7 @@ 23.1 ----- +- [*] Jetpack setup: Native experience for the connection step [https://github.com/woocommerce/woocommerce-ios/pull/15983] - [*] Payments: Updated the In-Person Payments `Learn More` redirection to display the correct page based on the selected payment provider [https://github.com/woocommerce/woocommerce-ios/pull/15998] 23.0 diff --git a/WooCommerce/Classes/Authentication/Jetpack Setup/LoginJetpackSetupCoordinator.swift b/WooCommerce/Classes/Authentication/Jetpack Setup/LoginJetpackSetupCoordinator.swift index 853d30a85e3..ef532e259d9 100644 --- a/WooCommerce/Classes/Authentication/Jetpack Setup/LoginJetpackSetupCoordinator.swift +++ b/WooCommerce/Classes/Authentication/Jetpack Setup/LoginJetpackSetupCoordinator.swift @@ -44,7 +44,11 @@ final class LoginJetpackSetupCoordinator: Coordinator { // private extension LoginJetpackSetupCoordinator { func showSetupSteps() { - let setupUI = JetpackSetupHostingController(siteURL: siteURL, connectionOnly: connectionOnly, onStoreNavigation: { [weak self] connectedEmail in + let setupUI = JetpackSetupHostingController( + siteURL: siteURL, + connectionOnly: connectionOnly, + wpcomCredentials: stores.sessionManager.defaultCredentials, + onStoreNavigation: { [weak self] connectedEmail in guard let self, let email = connectedEmail else { return } if email != self.stores.sessionManager.defaultAccount?.email { // if the user authorized Jetpack with a different account, support them to log in with that account. diff --git a/WooCommerce/Classes/Authentication/Jetpack Setup/Native Jetpack Setup/JetpackSetupView.swift b/WooCommerce/Classes/Authentication/Jetpack Setup/Native Jetpack Setup/JetpackSetupView.swift index 1a318e3a3af..2e8bdcdbe69 100644 --- a/WooCommerce/Classes/Authentication/Jetpack Setup/Native Jetpack Setup/JetpackSetupView.swift +++ b/WooCommerce/Classes/Authentication/Jetpack Setup/Native Jetpack Setup/JetpackSetupView.swift @@ -7,22 +7,23 @@ import protocol WooFoundation.Analytics final class JetpackSetupHostingController: UIHostingController { private let viewModel: JetpackSetupViewModel private let authentication: Authentication - private let connectionWebViewCredentials: Credentials? + private let wpcomCredentials: Credentials? init(siteURL: String, connectionOnly: Bool, - connectionWebViewCredentials: Credentials? = nil, + wpcomCredentials: Credentials?, stores: StoresManager = ServiceLocator.stores, authentication: Authentication = ServiceLocator.authenticationManager, analytics: Analytics = ServiceLocator.analytics, onStoreNavigation: @escaping (String?) -> Void) { self.viewModel = JetpackSetupViewModel(siteURL: siteURL, connectionOnly: connectionOnly, + wpcomCredentials: wpcomCredentials, stores: stores, analytics: analytics, onStoreNavigation: onStoreNavigation) self.authentication = authentication - self.connectionWebViewCredentials = connectionWebViewCredentials + self.wpcomCredentials = wpcomCredentials super.init(rootView: JetpackSetupView(viewModel: viewModel)) rootView.webViewPresentationHandler = { [weak self] in @@ -92,7 +93,7 @@ final class JetpackSetupHostingController: UIHostingController guard let self else { return } self.viewModel.jetpackConnectionInterrupted = true }) - let webView = AuthenticatedWebViewController(viewModel: webViewModel, extraCredentials: connectionWebViewCredentials) + let webView = AuthenticatedWebViewController(viewModel: webViewModel, extraCredentials: wpcomCredentials) webView.navigationItem.leftBarButtonItem = UIBarButtonItem(title: Localization.cancel, style: .plain, target: self, @@ -327,10 +328,3 @@ private extension JetpackSetupView { static let interruptedConnectionActionHandlerDelayTime: Double = 0.3 } } - -struct JetpackSetupView_Previews: PreviewProvider { - static var previews: some View { - JetpackSetupView(viewModel: JetpackSetupViewModel(siteURL: "https://test.com", connectionOnly: true)) - JetpackSetupView(viewModel: JetpackSetupViewModel(siteURL: "https://test.com", connectionOnly: false)) - } -} diff --git a/WooCommerce/Classes/Authentication/Jetpack Setup/Native Jetpack Setup/JetpackSetupViewModel.swift b/WooCommerce/Classes/Authentication/Jetpack Setup/Native Jetpack Setup/JetpackSetupViewModel.swift index f84e04bea9a..bba77612719 100644 --- a/WooCommerce/Classes/Authentication/Jetpack Setup/Native Jetpack Setup/JetpackSetupViewModel.swift +++ b/WooCommerce/Classes/Authentication/Jetpack Setup/Native Jetpack Setup/JetpackSetupViewModel.swift @@ -3,6 +3,7 @@ import UIKit import Yosemite import enum Alamofire.AFError import enum Networking.NetworkError +import class Networking.AlamofireNetwork import protocol WooFoundation.Analytics /// View model for `JetpackSetupView`. @@ -14,6 +15,8 @@ final class JetpackSetupViewModel: ObservableObject { private let stores: StoresManager private let storeNavigationHandler: (_ connectedEmail: String?) -> Void + private let wpcomCredentials: Credentials? + private var isPluginActivated = false @Published private(set) var setupSteps: [JetpackInstallStep] @@ -65,18 +68,8 @@ final class JetpackSetupViewModel: ObservableObject { } } - private var setupErrorCode: Int? { - if let error = setupError as? NetworkError, let code = error.responseCode { - return code - } else if let error = setupError as? AFError, let code = error.responseCode { - return code - } else { - return (setupError as? NSError)?.code - } - } - var hasEncounteredPermissionError: Bool { - setupErrorCode == 403 + setupError?.errorCode == 403 } /// Attributed string for the description text @@ -101,12 +94,14 @@ final class JetpackSetupViewModel: ObservableObject { init(siteURL: String, connectionOnly: Bool, + wpcomCredentials: Credentials?, stores: StoresManager = ServiceLocator.stores, analytics: Analytics = ServiceLocator.analytics, delayBeforeRetry: Double = Constants.delayBeforeRetry, onStoreNavigation: @escaping (String?) -> Void = { _ in}) { self.siteURL = siteURL self.connectionOnly = connectionOnly + self.wpcomCredentials = wpcomCredentials self.stores = stores self.analytics = analytics self.setupSteps = connectionOnly ? [.connection, .done] : JetpackInstallStep.allCases @@ -138,14 +133,14 @@ final class JetpackSetupViewModel: ObservableObject { func startSetup() { if connectionOnly { - fetchJetpackConnectionURL() + checkJetpackConnection(afterConnection: false) } else { retrieveJetpackPluginDetails() } } func didAuthorizeJetpackConnection() { - checkJetpackConnection() + checkJetpackConnection(afterConnection: true) } func didEncounterErrorDuringConnection(code: Int?) { @@ -179,7 +174,7 @@ final class JetpackSetupViewModel: ObservableObject { func didTapContinueConnectionButton() { trackSetupDuringLogin(.loginJetpackSetupScreenTryAgainButtonTapped) trackSetupAfterLogin(tap: .continueSetup) - fetchJetpackConnectionURL() + checkJetpackConnection(afterConnection: false) } /// Tracks events if the current flow is Jetpack setup during login @@ -217,15 +212,15 @@ private extension JetpackSetupViewModel { if plugin.status == .inactive { self.activateJetpack() } else { - self.fetchJetpackConnectionURL() + self.checkJetpackConnection(afterConnection: false) } case .failure(let error): DDLogError("⛔️ Error retrieving Jetpack: \(error)") self.setupError = error - if self.setupErrorCode == 404 { + if error.errorCode == 404 { if self.connectionOnly { - /// If site has WCPay installed and activated but not connected, - /// plugins need to be installed even though we detected a connection before + /// If site has Jetpack connection package connected, + /// Jetpack plugin needs to be installed even though we detected a connection before self.setupSteps = JetpackInstallStep.allCases self.connectionOnly = false } @@ -267,8 +262,9 @@ private extension JetpackSetupViewModel { guard let self else { return } switch result { case .success: + isPluginActivated = true self.trackSetupDuringLogin(.loginJetpackSetupActivationSuccessful) - self.fetchJetpackConnectionURL() + self.checkJetpackConnection(afterConnection: false) case .failure(let error): self.trackSetupDuringLogin(.loginJetpackSetupActivationFailed, failure: error) self.trackSetupAfterLogin(failure: error) @@ -280,7 +276,10 @@ private extension JetpackSetupViewModel { stores.dispatch(action) } - func fetchJetpackConnectionURL() { + /// Jetpack connection flow using web view. + /// Used only for sites with Jetpack plugin versions lower than 14.4. + /// + func startConnectionWithWebView() { currentSetupStep = .connection trackSetupAfterLogin() let action = JetpackConnectionAction.fetchJetpackConnectionURL { [weak self] result in @@ -308,52 +307,8 @@ private extension JetpackSetupViewModel { stores.dispatch(action) } - func checkJetpackConnection(retryCount: Int = 0) { - guard retryCount <= Constants.maxRetryCount else { - setupFailed = true - if let setupError { - analytics.track(.loginJetpackSetupErrorCheckingJetpackConnection, withError: setupError) - } - return - } - currentConnectionStep = .inProgress - let action = JetpackConnectionAction.fetchJetpackConnectionData { [weak self] result in - guard let self else { return } - switch result { - case .success(let connectionData): - guard let connectedEmail = connectionData.currentUser.wpcomUser?.email else { - DDLogWarn("⚠️ Cannot find connected WPcom user") - let missingWpcomUserError = NSError(domain: Constants.errorDomain, - code: Constants.errorCodeNoWPComUser, - userInfo: [Constants.errorUserInfoReason: Constants.errorUserInfoNoWPComUser]) - self.setupError = missingWpcomUserError - self.trackSetupDuringLogin(.loginJetpackSetupCannotFindWPCOMUser, failure: missingWpcomUserError) - // Retry fetching user in case Jetpack sync takes some time. - DispatchQueue.main.asyncAfter(deadline: .now() + delayBeforeRetry) { [weak self] in - self?.checkJetpackConnection(retryCount: retryCount + 1) - } - return - } - - self.jetpackConnectedEmail = connectedEmail - self.currentConnectionStep = .authorized - self.currentSetupStep = .done - - self.trackSetupDuringLogin(.loginJetpackSetupAllStepsMarkedDone) - self.trackSetupAfterLogin() - case .failure(let error): - DDLogError("⛔️ Error checking Jetpack connection: \(error)") - self.setupError = error - DispatchQueue.main.asyncAfter(deadline: .now() + delayBeforeRetry) { [weak self] in - self?.checkJetpackConnection(retryCount: retryCount + 1) - } - } - } - stores.dispatch(action) - } - func updateErrorMessage() { - guard let setupErrorCode else { + guard let setupErrorCode = setupError?.errorCode else { setupErrorDetail = .init(setupErrorMessage: Localization.genericErrorMessage, setupErrorSuggestion: Localization.communicationErrorSuggestion, errorCode: nil) @@ -377,6 +332,163 @@ private extension JetpackSetupViewModel { } } +// MARK: Handle connection steps +// Ref: pe5sF9-401-p2 +private extension JetpackSetupViewModel { + func checkJetpackConnection(afterConnection: Bool, retryCount: Int = 0) { + currentConnectionStep = .inProgress + let action = JetpackConnectionAction.fetchJetpackConnectionData { [weak self] result in + guard let self else { return } + switch result { + case .success(let connectionData): + if afterConnection { + checkConnectedUser(data: connectionData, retryCount: retryCount) + } else { + handleJetpackConnectionData(connectionData) + } + case .failure(let error): + DDLogError("⛔️ Error checking Jetpack connection: \(error)") + if retryCount == Constants.maxRetryCount { + return didFailJetpackConnection(with: error) + } + DispatchQueue.main.asyncAfter(deadline: .now() + delayBeforeRetry) { [weak self] in + self?.checkJetpackConnection(afterConnection: afterConnection, retryCount: retryCount + 1) + } + } + } + stores.dispatch(action) + } + + func checkConnectedUser(data: JetpackConnectionData, retryCount: Int = 0) { + let connectedEmail = data.currentUser.wpcomUser?.email + if let connectedEmail { + return didCompleteJetpackConnection(connectedEmail: connectedEmail) + } + + DDLogWarn("⚠️ Cannot find connected WPcom user") + let missingWpcomUserError = NSError(domain: Constants.errorDomain, + code: Constants.errorCodeNoWPComUser, + userInfo: [Constants.errorUserInfoReason: Constants.errorUserInfoNoWPComUser]) + trackSetupDuringLogin(.loginJetpackSetupCannotFindWPCOMUser, failure: missingWpcomUserError) + if retryCount == Constants.maxRetryCount { + return didFailJetpackConnection(with: missingWpcomUserError) + } + // Retry fetching user in case Jetpack sync takes some time. + DispatchQueue.main.asyncAfter(deadline: .now() + delayBeforeRetry) { [weak self] in + self?.checkJetpackConnection(afterConnection: true, retryCount: retryCount + 1) + } + } + + func handleJetpackConnectionData(_ data: JetpackConnectionData) { + if let connectedEmail = data.currentUser.wpcomUser?.email { + return didCompleteJetpackConnection(connectedEmail: connectedEmail) + } + + if let isRegistered = data.isRegistered { + return handleSiteRegisterResult(isRegistered: isRegistered, blogID: data.blogID) + } + + if isPluginActivated { + /// Skips plugin check if plugin has just got activated. + /// `isRegistered` is unavailable due to outdated Jetpack. Proceed with web flow. + startConnectionWithWebView() + } else { + /// Fetch plugin details to check + stores.dispatch(JetpackConnectionAction.retrieveJetpackPluginDetails { [weak self] result in + guard let self else { return } + switch result { + case .success: + /// Plugin is available but`isRegistered` is unavailable due to outdated version. + /// Proceed with web flow. + startConnectionWithWebView() + case .failure(let error): + if error.errorCode == 404 { + /// For Jetpack-connected sites, if `isRegistered` is not returned, + /// check for `connectionOwner` instead. + handleSiteRegisterResult(isRegistered: data.connectionOwner != nil, blogID: data.blogID) + } else { + didFailJetpackConnection(with: error) + } + } + }) + } + } + + func handleSiteRegisterResult(isRegistered: Bool, blogID: Int64?) { + if let blogID, isRegistered { + provisionSiteConnection(blogID: blogID) + } else { + registerSiteConnection() + } + } + + func registerSiteConnection() { + stores.dispatch(JetpackConnectionAction.registerSite(completion: { [weak self] result in + guard let self else { return } + switch result { + case .success(let blogID): + provisionSiteConnection(blogID: blogID) + case .failure(let error): + didFailJetpackConnection(with: error) + } + })) + } + + func provisionSiteConnection(blogID: Int64) { + currentSetupStep = .connection + trackSetupAfterLogin() + stores.dispatch(JetpackConnectionAction.provisionConnection(completion: { [weak self] result in + guard let self else { return } + switch result { + case .success(let response): + finalizeSiteConnection(blogID: blogID, provisionResponse: response) + case .failure(let error): + didFailJetpackConnection(with: error) + } + })) + } + + func finalizeSiteConnection(blogID: Int64, provisionResponse: JetpackConnectionProvisionResponse) { + guard let wpcomCredentials, case .wpcom = wpcomCredentials else { + /// WPCom credentials are necessary to finalize connection through API + /// If this is unavailable, fall back to the web flow. + return startConnectionWithWebView() + } + let network = AlamofireNetwork(credentials: wpcomCredentials) + stores.dispatch(JetpackConnectionAction.finalizeConnection( + siteID: blogID, + siteURL: siteURL, + provisionResponse: provisionResponse, + network: network + ) { [weak self] result in + guard let self else { return } + switch result { + case .success: + checkJetpackConnection(afterConnection: true) + case .failure(let error): + didFailJetpackConnection(with: error) + } + }) + } + + func didCompleteJetpackConnection(connectedEmail: String) { + jetpackConnectedEmail = connectedEmail + currentConnectionStep = .authorized + currentSetupStep = .done + + trackSetupDuringLogin(.loginJetpackSetupAllStepsMarkedDone) + trackSetupAfterLogin() + } + + func didFailJetpackConnection(with error: Error) { + setupFailed = true + setupError = error + if let setupError { + analytics.track(.loginJetpackSetupErrorCheckingJetpackConnection, withError: setupError) + } + } +} + // MARK: Subtypes // extension JetpackSetupViewModel { @@ -476,3 +588,15 @@ extension JetpackSetupViewModel { static let accountConnectionURL = "https://jetpack.wordpress.com/jetpack.authorize" } } + +fileprivate extension Error { + var errorCode: Int? { + if let error = self as? NetworkError, let code = error.responseCode { + return code + } else if let error = self as? AFError, let code = error.responseCode { + return code + } else { + return (self as NSError).code + } + } +} diff --git a/WooCommerce/Classes/ViewRelated/JetpackSetup/JetpackSetupCoordinator.swift b/WooCommerce/Classes/ViewRelated/JetpackSetup/JetpackSetupCoordinator.swift index 7df8cc41ed4..8ae58038b7e 100644 --- a/WooCommerce/Classes/ViewRelated/JetpackSetup/JetpackSetupCoordinator.swift +++ b/WooCommerce/Classes/ViewRelated/JetpackSetup/JetpackSetupCoordinator.swift @@ -222,7 +222,7 @@ private extension JetpackSetupCoordinator { } let setupUI = JetpackSetupHostingController(siteURL: site.url, connectionOnly: requiresConnectionOnly, - connectionWebViewCredentials: credentials, + wpcomCredentials: credentials, onStoreNavigation: { [weak self] _ in DDLogInfo("🎉 Jetpack setup completes!") self?.rootViewController.topmostPresentedViewController.dismiss(animated: true, completion: { @@ -320,12 +320,7 @@ private extension JetpackSetupCoordinator { @MainActor func fetchJetpackConnectionData() async throws -> JetpackConnectionData { - /// Jetpack setup will fail anyway without admin role, so check that first. - let roles = stores.sessionManager.defaultRoles - guard roles.contains(.administrator) else { - throw JetpackCheckError.missingPermission - } - return try await withCheckedThrowingContinuation { continuation in + try await withCheckedThrowingContinuation { continuation in let action = JetpackConnectionAction.fetchJetpackConnectionData { result in continuation.resume(with: result) } @@ -489,11 +484,13 @@ private extension JetpackSetupCoordinator { } // MARK: - Subtypes -private extension JetpackSetupCoordinator { +extension JetpackSetupCoordinator { enum JetpackCheckError: Int, Error { case missingPermission = 403 } +} +private extension JetpackSetupCoordinator { enum Constants { static let magicLinkUrlHostname = "magic-login" } diff --git a/WooCommerce/WooCommerceTests/Authentication/Jetpack Setup/JetpackSetupHostingControllerTests.swift b/WooCommerce/WooCommerceTests/Authentication/Jetpack Setup/JetpackSetupHostingControllerTests.swift index 1b28b033e13..c28b5379ea3 100644 --- a/WooCommerce/WooCommerceTests/Authentication/Jetpack Setup/JetpackSetupHostingControllerTests.swift +++ b/WooCommerce/WooCommerceTests/Authentication/Jetpack Setup/JetpackSetupHostingControllerTests.swift @@ -2,11 +2,13 @@ import Foundation import XCTest @testable import WooCommerce +@testable import Yosemite /// Test cases for `JetpackSetupHostingController`. /// final class JetpackSetupHostingControllerTests: XCTestCase { private let testURL = "https://test.com" + private let credentials = Credentials.wpcom(username: "test", authToken: "secret", siteAddress: "https://example.com") func test_it_tracks_login_jetpack_setup_screen_viewed_when_view_loads_for_unauthenticated_users() throws { // Given @@ -15,6 +17,7 @@ final class JetpackSetupHostingControllerTests: XCTestCase { let analytics = WooAnalytics(analyticsProvider: analyticsProvider) let viewController = JetpackSetupHostingController(siteURL: testURL, connectionOnly: true, + wpcomCredentials: credentials, stores: stores, analytics: analytics, onStoreNavigation: { _ in }) @@ -33,6 +36,7 @@ final class JetpackSetupHostingControllerTests: XCTestCase { let analytics = WooAnalytics(analyticsProvider: analyticsProvider) let viewController = JetpackSetupHostingController(siteURL: testURL, connectionOnly: true, + wpcomCredentials: credentials, stores: stores, analytics: analytics, onStoreNavigation: { _ in }) diff --git a/WooCommerce/WooCommerceTests/Authentication/Jetpack Setup/JetpackSetupViewModelTests.swift b/WooCommerce/WooCommerceTests/Authentication/Jetpack Setup/JetpackSetupViewModelTests.swift index 5888a50a711..a0f208bfa39 100644 --- a/WooCommerce/WooCommerceTests/Authentication/Jetpack Setup/JetpackSetupViewModelTests.swift +++ b/WooCommerce/WooCommerceTests/Authentication/Jetpack Setup/JetpackSetupViewModelTests.swift @@ -6,6 +6,7 @@ import WordPressAuthenticator final class JetpackSetupViewModelTests: XCTestCase { private let testURL = "https://example.com" + private let credentials = Credentials.wpcom(username: "test", authToken: "secret", siteAddress: "https://example.com") override func setUp() { super.setUp() @@ -16,7 +17,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_title_is_correct_if_jetpack_installation_is_required() { // Given - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, delayBeforeRetry: 0) // Then XCTAssertEqual(viewModel.title, JetpackSetupViewModel.Localization.installingJetpack) @@ -24,7 +25,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_title_is_correct_if_only_jetpack_connection_is_missing() { // Given - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: true, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: true, wpcomCredentials: credentials, delayBeforeRetry: 0) // Then XCTAssertEqual(viewModel.title, JetpackSetupViewModel.Localization.connectingJetpack) @@ -32,7 +33,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_description_string_is_correct() { // Given - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, delayBeforeRetry: 0) let description = String(format: JetpackSetupViewModel.Localization.description, testURL.trimHTTPScheme()) // Then @@ -42,7 +43,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_isSetupStepFailed_is_correct_when_the_current_step_fails() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) let plugin = SitePlugin.fake().copy(plugin: "Jetpack", status: .inactive) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in @@ -68,7 +69,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_title_is_correct_when_retrieveJetpackPluginDetails_fails_with_permission_error() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in switch action { @@ -89,7 +90,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_title_and_tryAgainButtonTitle_are_correct_when_installation_step_fails() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in switch action { @@ -113,7 +114,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_title_and_tryAgainButtonTitle_are_correct_when_activation_step_fails() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) let plugin = SitePlugin.fake().copy(plugin: "Jetpack", status: .inactive) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in @@ -138,13 +139,14 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_title_and_tryAgainButtonTitle_are_correct_when_connection_step_fails() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) - let plugin = SitePlugin.fake().copy(plugin: "Jetpack", status: .active) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: true, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in switch action { case .retrieveJetpackPluginDetails(let completion): - completion(.success(plugin)) + completion(.success(.fake())) + case .fetchJetpackConnectionData(let completion): + completion(.success(JetpackConnectionData.fake().copy(isRegistered: nil))) case .fetchJetpackConnectionURL(let completion): completion(.failure(NSError(domain: "Test", code: -1001))) default: @@ -163,7 +165,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_shouldShowInitialLoadingIndicator_turns_on_correctly_when_startSetup_then_returns_true() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) // When viewModel.startSetup() @@ -176,7 +178,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_shouldShowInitialLoadingIndicator_turns_off_correctly_when_retrieveJetpackPluginDetails_is_success_then_returns_false() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) let plugin = SitePlugin.fake().copy(plugin: "Jetpack", status: .inactive) // When @@ -198,7 +200,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_shouldShowSetupSteps_when_startSetup_then_returns_false() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) // When viewModel.startSetup() @@ -211,7 +213,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_shouldShowSetupSteps_when_retrieveJetpackPluginDetails_is_success_then_returns_true() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) let plugin = SitePlugin.fake().copy(plugin: "Jetpack", status: .inactive) // When @@ -232,7 +234,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_shouldShowGoToStoreButton_is_correct() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) let data = JetpackConnectionData.fake().copy( currentUser: .fake().copy(isConnected: true, wpcomUser: DotcomUser.fake().copy(email: "test@mail.com")) @@ -263,12 +265,13 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_startSetup_triggers_connection_step_if_connectionOnly_is_true() throws { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: true, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: true, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) let testConnectionURL = try XCTUnwrap(URL(string: "https://jetpack.wordpress.com/jetpack.authorize")) var triggeredRetrieveJetpackPluginDetails = false var triggeredInstallation = false var triggeredActivation = false + var triggeredConnectionURL = false var triggeredConnection = false stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in switch action { @@ -283,6 +286,9 @@ final class JetpackSetupViewModelTests: XCTestCase { triggeredActivation = true case .fetchJetpackConnectionURL(let completion): completion(.success(testConnectionURL)) + triggeredConnectionURL = true + case .fetchJetpackConnectionData(let completion): + completion(.success(.fake().copy(isRegistered: false))) triggeredConnection = true default: break @@ -296,14 +302,14 @@ final class JetpackSetupViewModelTests: XCTestCase { XCTAssertFalse(triggeredRetrieveJetpackPluginDetails) XCTAssertFalse(triggeredInstallation) XCTAssertFalse(triggeredActivation) + XCTAssertFalse(triggeredConnectionURL) XCTAssertTrue(triggeredConnection) } func test_startSetup_triggers_installation_steps_if_connectionOnly_is_false() throws { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) - let testConnectionURL = try XCTUnwrap(URL(string: "https://jetpack.wordpress.com/jetpack.authorize")) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) var triggeredRetrieveJetpackPluginDetails = false var triggeredInstallation = false @@ -321,8 +327,8 @@ final class JetpackSetupViewModelTests: XCTestCase { case .activateJetpackPlugin(let completion): completion(.success(())) triggeredActivation = true - case .fetchJetpackConnectionURL(let completion): - completion(.success(testConnectionURL)) + case .fetchJetpackConnectionData(let completion): + completion(.success(.fake().copy(isRegistered: false))) triggeredConnection = true default: break @@ -342,7 +348,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_startSetup_triggers_jetpack_installation_if_retrieving_details_fails_with_404() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) var triggeredJetpackInstallation = false stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in @@ -370,7 +376,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_startSetup_triggers_jetpack_activation_if_retrieving_details_returns_inactive_jetpack() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) let plugin = SitePlugin.fake().copy(plugin: "Jetpack", status: .inactive) var triggeredInstallation = false @@ -406,7 +412,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_startSetup_triggers_jetpack_connection_if_retrieving_details_returns_active_jetpack() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) let plugin = SitePlugin.fake().copy(plugin: "Jetpack", status: .active) var triggeredInstallation = false @@ -420,7 +426,9 @@ final class JetpackSetupViewModelTests: XCTestCase { triggeredInstallation = true case .activateJetpackPlugin: triggeredActivation = true - case .fetchJetpackConnectionURL: + case .fetchJetpackConnectionData(let completion): + completion(.success(.fake().copy(isRegistered: true, blogID: 123))) + case .provisionConnection: triggeredConnection = true default: break @@ -443,7 +451,11 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_installation_triggers_activation_when_completing_successfully() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, + connectionOnly: false, + wpcomCredentials: credentials, + stores: stores, + delayBeforeRetry: 0) var triggeredActivation = false var triggeredConnection = false @@ -471,12 +483,16 @@ final class JetpackSetupViewModelTests: XCTestCase { XCTAssertFalse(triggeredConnection) } - func test_activation_triggers_fetching_connection_url_when_completing_successfully() { + func test_activation_success_triggers_all_connection_apis_if_isRegistered_is_false() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) - var triggeredConnection = false + var fetchedConnectionData = false + var triggeredConnectionURL = false + var triggeredRegisterSite = false + var triggeredProvisionConnection = false + var triggeredFinalizeConnection = false stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in switch action { case .retrieveJetpackPluginDetails(let completion): @@ -486,8 +502,20 @@ final class JetpackSetupViewModelTests: XCTestCase { completion(.success(())) case .activateJetpackPlugin(let completion): completion(.success(())) + case .fetchJetpackConnectionData(let completion): + fetchedConnectionData = true + completion(.success(.fake().copy(isRegistered: false))) + case .registerSite(let completion): + triggeredRegisterSite = true + completion(.success(124)) + case .provisionConnection(let completion): + triggeredProvisionConnection = true + completion(.success(JetpackConnectionProvisionResponse(userId: 131, scope: "test", secret: "secret"))) + case let .finalizeConnection(_, _, _, _, completion): + triggeredFinalizeConnection = true + completion(.success(())) case .fetchJetpackConnectionURL: - triggeredConnection = true + triggeredConnectionURL = true default: break } @@ -497,15 +525,23 @@ final class JetpackSetupViewModelTests: XCTestCase { viewModel.startSetup() // Then - XCTAssertTrue(triggeredConnection) + XCTAssertTrue(fetchedConnectionData) + XCTAssertFalse(triggeredConnectionURL) + XCTAssertTrue(triggeredRegisterSite) + XCTAssertTrue(triggeredProvisionConnection) + XCTAssertTrue(triggeredFinalizeConnection) } - func test_shouldPresentWebView_is_true_when_fetching_connection_url_returns_account_connection_url() throws { + func test_activation_success_triggers_all_connection_apis_except_register_if_isRegistered_is_true() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) - let testConnectionURL = try XCTUnwrap(URL(string: "https://jetpack.wordpress.com/jetpack.authorize")) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) + var fetchedConnectionData = false + var triggeredConnectionURL = false + var triggeredRegisterSite = false + var triggeredProvisionConnection = false + var triggeredFinalizeConnection = false stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in switch action { case .retrieveJetpackPluginDetails(let completion): @@ -515,8 +551,19 @@ final class JetpackSetupViewModelTests: XCTestCase { completion(.success(())) case .activateJetpackPlugin(let completion): completion(.success(())) - case .fetchJetpackConnectionURL(let completion): - completion(.success(testConnectionURL)) + case .fetchJetpackConnectionData(let completion): + fetchedConnectionData = true + completion(.success(.fake().copy(isRegistered: true, blogID: 124))) + case .registerSite: + triggeredRegisterSite = true + case .provisionConnection(let completion): + triggeredProvisionConnection = true + completion(.success(JetpackConnectionProvisionResponse(userId: 131, scope: "test", secret: "secret"))) + case let .finalizeConnection(_, _, _, _, completion): + triggeredFinalizeConnection = true + completion(.success(())) + case .fetchJetpackConnectionURL: + triggeredConnectionURL = true default: break } @@ -526,16 +573,20 @@ final class JetpackSetupViewModelTests: XCTestCase { viewModel.startSetup() // Then - XCTAssertTrue(viewModel.shouldPresentWebView) - XCTAssertEqual(viewModel.jetpackConnectionURL, testConnectionURL) + XCTAssertTrue(fetchedConnectionData) + XCTAssertFalse(triggeredConnectionURL) + XCTAssertFalse(triggeredRegisterSite) + XCTAssertTrue(triggeredProvisionConnection) + XCTAssertTrue(triggeredFinalizeConnection) } - func test_shouldPresentWebView_is_true_when_fetching_connection_url_returns_site_connection_url() throws { + func test_activation_triggers_fetching_connection_url_when_site_has_outdated_jetpack() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) - let testConnectionURL = try XCTUnwrap(URL(string: "\(testURL)/plugins/jetpack")) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) + var fetchedConnectionData = false + var triggeredConnectionURL = false stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in switch action { case .retrieveJetpackPluginDetails(let completion): @@ -545,6 +596,63 @@ final class JetpackSetupViewModelTests: XCTestCase { completion(.success(())) case .activateJetpackPlugin(let completion): completion(.success(())) + case .fetchJetpackConnectionData(let completion): + fetchedConnectionData = true + completion(.success(.fake().copy(isRegistered: nil))) + case .fetchJetpackConnectionURL: + triggeredConnectionURL = true + default: + break + } + } + + // When + viewModel.startSetup() + + // Then + XCTAssertTrue(fetchedConnectionData) + XCTAssertTrue(triggeredConnectionURL) + } + + func test_shouldPresentWebView_is_true_when_fetching_connection_url_returns_account_connection_url() throws { + // Given + let stores = MockStoresManager(sessionManager: .makeForTesting()) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: true, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) + let testConnectionURL = try XCTUnwrap(URL(string: "https://jetpack.wordpress.com/jetpack.authorize")) + + stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in + switch action { + case .retrieveJetpackPluginDetails(let completion): + completion(.success(.fake())) + case .fetchJetpackConnectionData(let completion): + completion(.success(.fake().copy(isRegistered: nil))) + case .fetchJetpackConnectionURL(let completion): + completion(.success(testConnectionURL)) + default: + break + } + } + + // When + viewModel.startSetup() + + // Then + XCTAssertTrue(viewModel.shouldPresentWebView) + XCTAssertEqual(viewModel.jetpackConnectionURL, testConnectionURL) + } + + func test_shouldPresentWebView_is_true_when_fetching_connection_url_returns_site_connection_url() throws { + // Given + let stores = MockStoresManager(sessionManager: .makeForTesting()) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: true, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) + let testConnectionURL = try XCTUnwrap(URL(string: "\(testURL)/plugins/jetpack")) + + stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in + switch action { + case .retrieveJetpackPluginDetails(let completion): + completion(.success(.fake())) + case .fetchJetpackConnectionData(let completion): + completion(.success(.fake().copy(isRegistered: nil))) case .fetchJetpackConnectionURL(let completion): completion(.success(testConnectionURL)) default: @@ -565,7 +673,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_authorizeJetpackConnection_sets_connection_status_to_in_progress_and_triggers_fetching_jetpack_connection() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) var triggeredFetchingJetpackConnection = false stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in @@ -588,7 +696,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_authorizeJetpackConnection_updates_connection_status_and_setup_step_correctly_when_fetching_jetpack_connection_successfully() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) let data = JetpackConnectionData.fake().copy( currentUser: .fake().copy(isConnected: true, wpcomUser: DotcomUser.fake().copy(email: "test@mail.com")) @@ -613,7 +721,11 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_navigateToStore_triggers_storeNavigationHandler() { // Given var storeNavigationTriggered = false - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, delayBeforeRetry: 0, onStoreNavigation: { _ in + let viewModel = JetpackSetupViewModel(siteURL: testURL, + connectionOnly: false, + wpcomCredentials: credentials, + delayBeforeRetry: 0, + onStoreNavigation: { _ in storeNavigationTriggered = true }) @@ -628,7 +740,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_setupFailed_is_true_when_retrieveJetpackPluginDetails_encounters_permission_error() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) XCTAssertFalse(viewModel.setupFailed) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in @@ -654,7 +766,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_retrieveJetpackPluginDetails_triggers_installJetpack_when_encountering_non_permission_error() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) var installJetpackTriggered = false stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in @@ -678,7 +790,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_installJetpack_relays_error_when_failed() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in switch action { @@ -704,7 +816,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_activateJetpack_relays_error_when_failed() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) let plugin = SitePlugin.fake().copy(plugin: "Jetpack", status: .inactive) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in @@ -728,16 +840,97 @@ final class JetpackSetupViewModelTests: XCTestCase { errorCode: -1001)) } + func test_register_connection_relays_error_when_failed() { + // Given + let stores = MockStoresManager(sessionManager: .makeForTesting()) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: true, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) + + stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in + switch action { + case .fetchJetpackConnectionData(let completion): + completion(.success(.fake().copy(isRegistered: false))) + case .registerSite(let completion): + completion(.failure(NSError(domain: "Test", code: -1001))) + default: + break + } + } + + // When + viewModel.startSetup() + + // Then + XCTAssertTrue(viewModel.setupFailed) + XCTAssertEqual(viewModel.setupErrorDetail, .init(setupErrorMessage: JetpackSetupViewModel.Localization.genericErrorMessage, + setupErrorSuggestion: JetpackSetupViewModel.Localization.communicationErrorSuggestion, + errorCode: -1001)) + } + + func test_provision_connection_relays_error_when_failed() { + // Given + let stores = MockStoresManager(sessionManager: .makeForTesting()) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: true, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) + + stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in + switch action { + case .fetchJetpackConnectionData(let completion): + completion(.success(.fake().copy(isRegistered: true, blogID: 123))) + case .provisionConnection(let completion): + completion(.failure(NSError(domain: "Test", code: -1001))) + default: + break + } + } + + // When + viewModel.startSetup() + + // Then + XCTAssertTrue(viewModel.setupFailed) + XCTAssertEqual(viewModel.setupErrorDetail, .init(setupErrorMessage: JetpackSetupViewModel.Localization.genericErrorMessage, + setupErrorSuggestion: JetpackSetupViewModel.Localization.communicationErrorSuggestion, + errorCode: -1001)) + } + + func test_finalize_connection_relays_error_when_failed() { + // Given + let stores = MockStoresManager(sessionManager: .makeForTesting()) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: true, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) + + stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in + switch action { + case .fetchJetpackConnectionData(let completion): + completion(.success(.fake().copy(isRegistered: true, blogID: 123))) + case .provisionConnection(let completion): + completion(.success(JetpackConnectionProvisionResponse(userId: 124, scope: "admin", secret: "secret"))) + case let .finalizeConnection(_, _, _, _, completion): + completion(.failure(NSError(domain: "Test", code: -1001))) + default: + break + } + } + + // When + viewModel.startSetup() + + // Then + XCTAssertTrue(viewModel.setupFailed) + XCTAssertEqual(viewModel.setupErrorDetail, .init(setupErrorMessage: JetpackSetupViewModel.Localization.genericErrorMessage, + setupErrorSuggestion: JetpackSetupViewModel.Localization.communicationErrorSuggestion, + errorCode: -1001)) + } + func test_fetchJetpackConnectionURL_relays_error_when_failed() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) - let plugin = SitePlugin.fake().copy(plugin: "Jetpack", status: .active) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: true, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in switch action { case .retrieveJetpackPluginDetails(let completion): - completion(.success(plugin)) + completion(.success(.fake())) + case .fetchJetpackConnectionData(let completion): + completion(.success(.fake().copy(isRegistered: nil))) case .fetchJetpackConnectionURL(let completion): completion(.failure(NSError(domain: "Test", code: -1001))) default: @@ -758,7 +951,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_checkJetpackConnection_hits_fetchJetpackConnection_3_times_when_encountering_error_consistently_and_relays_error() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) var fetchJetpackConnectionTriggerCount = 0 stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in @@ -787,7 +980,7 @@ final class JetpackSetupViewModelTests: XCTestCase { func test_checkJetpackConnection_hits_fetchJetpackConnectionData_3_times_when_failing_to_fetch_connected_wpcom_user() { // Given let stores = MockStoresManager(sessionManager: .makeForTesting()) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, wpcomCredentials: credentials, stores: stores, delayBeforeRetry: 0) var fetchJetpackConnectionTriggerCount = 0 stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in @@ -819,7 +1012,12 @@ final class JetpackSetupViewModelTests: XCTestCase { let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: false)) let analyticsProvider = MockAnalyticsProvider() let analytics = WooAnalytics(analyticsProvider: analyticsProvider) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, analytics: analytics, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, + connectionOnly: false, + wpcomCredentials: credentials, + stores: stores, + analytics: analytics, + delayBeforeRetry: 0) // When // Tapping "Go to Store" button @@ -835,7 +1033,12 @@ final class JetpackSetupViewModelTests: XCTestCase { let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: false)) let analyticsProvider = MockAnalyticsProvider() let analytics = WooAnalytics(analyticsProvider: analyticsProvider) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, analytics: analytics, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, + connectionOnly: false, + wpcomCredentials: credentials, + stores: stores, + analytics: analytics, + delayBeforeRetry: 0) let error = NetworkError.notFound(response: nil) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in @@ -862,7 +1065,12 @@ final class JetpackSetupViewModelTests: XCTestCase { let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: false)) let analyticsProvider = MockAnalyticsProvider() let analytics = WooAnalytics(analyticsProvider: analyticsProvider) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, analytics: analytics, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, + connectionOnly: false, + wpcomCredentials: credentials, + stores: stores, + analytics: analytics, + delayBeforeRetry: 0) let error = NetworkError.notFound(response: nil) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in @@ -889,7 +1097,12 @@ final class JetpackSetupViewModelTests: XCTestCase { let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: false)) let analyticsProvider = MockAnalyticsProvider() let analytics = WooAnalytics(analyticsProvider: analyticsProvider) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, analytics: analytics, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, + connectionOnly: false, + wpcomCredentials: credentials, + stores: stores, + analytics: analytics, + delayBeforeRetry: 0) let error = NetworkError.notFound(response: nil) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in @@ -918,7 +1131,12 @@ final class JetpackSetupViewModelTests: XCTestCase { let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: false)) let analyticsProvider = MockAnalyticsProvider() let analytics = WooAnalytics(analyticsProvider: analyticsProvider) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, analytics: analytics, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, + connectionOnly: false, + wpcomCredentials: credentials, + stores: stores, + analytics: analytics, + delayBeforeRetry: 0) let error = NetworkError.notFound(response: nil) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in @@ -946,18 +1164,20 @@ final class JetpackSetupViewModelTests: XCTestCase { let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: false)) let analyticsProvider = MockAnalyticsProvider() let analytics = WooAnalytics(analyticsProvider: analyticsProvider) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, analytics: analytics, delayBeforeRetry: 0) - let error = NetworkError.notFound(response: nil) + let viewModel = JetpackSetupViewModel(siteURL: testURL, + connectionOnly: true, + wpcomCredentials: credentials, + stores: stores, + analytics: analytics, + delayBeforeRetry: 0) let testConnectionURL = try XCTUnwrap(URL(string: "https://test-connection.com")) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in switch action { case .retrieveJetpackPluginDetails(let completion): - completion(.failure(error)) - case .installJetpackPlugin(let completion): - completion(.success(())) - case .activateJetpackPlugin(let completion): - completion(.success(())) + completion(.success(.fake())) + case .fetchJetpackConnectionData(let completion): + completion(.success(.fake().copy(isRegistered: nil))) case .fetchJetpackConnectionURL(let completion): completion(.success((testConnectionURL))) default: @@ -978,17 +1198,19 @@ final class JetpackSetupViewModelTests: XCTestCase { let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: false)) let analyticsProvider = MockAnalyticsProvider() let analytics = WooAnalytics(analyticsProvider: analyticsProvider) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, analytics: analytics, delayBeforeRetry: 0) - let error = NetworkError.notFound(response: nil) + let viewModel = JetpackSetupViewModel(siteURL: testURL, + connectionOnly: true, + wpcomCredentials: credentials, + stores: stores, + analytics: analytics, + delayBeforeRetry: 0) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in switch action { case .retrieveJetpackPluginDetails(let completion): - completion(.failure(error)) - case .installJetpackPlugin(let completion): - completion(.success(())) - case .activateJetpackPlugin(let completion): - completion(.success(())) + completion(.success(.fake())) + case .fetchJetpackConnectionData(let completion): + completion(.success(.fake().copy(isRegistered: nil))) case .fetchJetpackConnectionURL(let completion): let fetchError = NSError(domain: "Test", code: 1) completion(.failure(fetchError)) @@ -996,6 +1218,7 @@ final class JetpackSetupViewModelTests: XCTestCase { break } } + // When viewModel.startSetup() @@ -1009,13 +1232,20 @@ final class JetpackSetupViewModelTests: XCTestCase { let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: false)) let analyticsProvider = MockAnalyticsProvider() let analytics = WooAnalytics(analyticsProvider: analyticsProvider) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, analytics: analytics, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, + connectionOnly: false, + wpcomCredentials: credentials, + stores: stores, + analytics: analytics, + delayBeforeRetry: 0) let data = JetpackConnectionData.fake().copy( currentUser: .fake().copy(isConnected: true, wpcomUser: DotcomUser.fake().copy(email: "test@mail.com")) ) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in switch action { + case .retrieveJetpackPluginDetails(let completion): + completion(.success(.fake())) case .fetchJetpackConnectionData(let completion): completion(.success(data)) default: @@ -1036,7 +1266,12 @@ final class JetpackSetupViewModelTests: XCTestCase { let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: false)) let analyticsProvider = MockAnalyticsProvider() let analytics = WooAnalytics(analyticsProvider: analyticsProvider) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, analytics: analytics, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, + connectionOnly: false, + wpcomCredentials: credentials, + stores: stores, + analytics: analytics, + delayBeforeRetry: 0) let data = JetpackConnectionData.fake().copy(currentUser: .fake().copy(isConnected: true, wpcomUser: nil)) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in @@ -1061,7 +1296,12 @@ final class JetpackSetupViewModelTests: XCTestCase { let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: false)) let analyticsProvider = MockAnalyticsProvider() let analytics = WooAnalytics(analyticsProvider: analyticsProvider) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, analytics: analytics, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, + connectionOnly: false, + wpcomCredentials: credentials, + stores: stores, + analytics: analytics, + delayBeforeRetry: 0) let error = NSError(domain: "Test", code: 1) stores.whenReceivingAction(ofType: JetpackConnectionAction.self) { action in @@ -1090,7 +1330,12 @@ final class JetpackSetupViewModelTests: XCTestCase { let stores = MockStoresManager(sessionManager: .makeForTesting(authenticated: false)) let analyticsProvider = MockAnalyticsProvider() let analytics = WooAnalytics(analyticsProvider: analyticsProvider) - let viewModel = JetpackSetupViewModel(siteURL: testURL, connectionOnly: false, stores: stores, analytics: analytics, delayBeforeRetry: 0) + let viewModel = JetpackSetupViewModel(siteURL: testURL, + connectionOnly: false, + wpcomCredentials: credentials, + stores: stores, + analytics: analytics, + delayBeforeRetry: 0) // When viewModel.retryAllSteps() diff --git a/WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/JetpackSetupCoordinatorTests.swift b/WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/JetpackSetupCoordinatorTests.swift index 6be27fd8d73..f33062b9988 100644 --- a/WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/JetpackSetupCoordinatorTests.swift +++ b/WooCommerce/WooCommerceTests/ViewRelated/JetpackSetup/JetpackSetupCoordinatorTests.swift @@ -106,7 +106,7 @@ final class JetpackSetupCoordinatorTests: XCTestCase { case let .loadWPComAccount(_, onCompletion): onCompletion(expectedAccount) case let .fetchJetpackConnectionData(completion): - completion(.success(JetpackConnectionData.fake())) + completion(.failure(JetpackSetupCoordinator.JetpackCheckError.missingPermission)) default: break }