diff --git a/RELEASE-NOTES.txt b/RELEASE-NOTES.txt index f7eaebeefec..5d9508ca708 100644 --- a/RELEASE-NOTES.txt +++ b/RELEASE-NOTES.txt @@ -11,6 +11,7 @@ - [*] Background image upload: Fix missing error notice in iPhones [https://github.com/woocommerce/woocommerce-ios/pull/15117] - [*] Background image upload: Show a notice when the user leaves product details while uploads are pending [https://github.com/woocommerce/woocommerce-ios/pull/15134] - [*] Filters applied in product selector no longer affect the main product list screen. [https://github.com/woocommerce/woocommerce-ios/pull/14764] +- [Internal] Improve authentication logic for authenticated web view [https://github.com/woocommerce/woocommerce-ios/pull/15164] - [*] Better error messages if Application Password login is disabled on user's website. [https://github.com/woocommerce/woocommerce-ios/pull/15031] - [**] Product Images: Update error handling [https://github.com/woocommerce/woocommerce-ios/pull/15105] - [*] Merchants can mark and filter favorite products for quicker access. [https://github.com/woocommerce/woocommerce-ios/pull/14597] diff --git a/WooCommerce/Classes/Authentication/AuthenticatedWebViewController.swift b/WooCommerce/Classes/Authentication/AuthenticatedWebViewController.swift index 8fc478cf30d..11322a588e2 100644 --- a/WooCommerce/Classes/Authentication/AuthenticatedWebViewController.swift +++ b/WooCommerce/Classes/Authentication/AuthenticatedWebViewController.swift @@ -1,15 +1,17 @@ import Combine import UIKit import WebKit +import Yosemite import class Networking.UserAgent import struct WordPressAuthenticator.WordPressOrgCredentials -import enum Yosemite.Credentials /// A web view which is authenticated for WordPress.com, when possible. /// final class AuthenticatedWebViewController: UIViewController { + private let currentSite: Site? private let viewModel: AuthenticatedWebViewModel + private let authenticationFlow: WebViewAuthenticationFlow private lazy var activityIndicator: UIActivityIndicatorView = { let activityIndicator = UIActivityIndicatorView(style: .medium) @@ -45,12 +47,15 @@ final class AuthenticatedWebViewController: UIViewController { private let wpcomCredentials: Credentials? + private var isFirstNavigation = true - init(viewModel: AuthenticatedWebViewModel, extraCredentials: Credentials? = nil) { + init(stores: StoresManager = ServiceLocator.stores, + viewModel: AuthenticatedWebViewModel, + extraCredentials: Credentials? = nil) { self.viewModel = viewModel - let currentCredentials = ServiceLocator.stores.sessionManager.defaultCredentials + let currentCredentials = stores.sessionManager.defaultCredentials - self.siteCredentials = { + let siteCredentials: WordPressOrgCredentials? = { if case let .wporg(username, password, siteAddress) = extraCredentials { return WordPressOrgCredentials(username: username, password: password, @@ -65,7 +70,7 @@ final class AuthenticatedWebViewController: UIViewController { return nil }() - self.wpcomCredentials = { + let wpcomCredentials: Credentials? = { if case .wpcom = extraCredentials { return extraCredentials } else if case .wpcom = currentCredentials { @@ -73,6 +78,21 @@ final class AuthenticatedWebViewController: UIViewController { } return nil }() + + let currentSite = stores.sessionManager.defaultSite + + self.authenticationFlow = { + guard let currentSite else { + return WebViewAuthenticationFlow.none + } + return viewModel.authenticationFlow(currentSite: currentSite, + wpcomCredentialsAvailable: wpcomCredentials != nil, + wporgCredentialsAvailable: siteCredentials != nil) + }() + self.currentSite = currentSite + self.wpcomCredentials = wpcomCredentials + self.siteCredentials = siteCredentials + super.init(nibName: nil, bundle: nil) if let initialURL = viewModel.initialURL, @@ -93,6 +113,7 @@ final class AuthenticatedWebViewController: UIViewController { configureWebView() configureActivityIndicator() configureProgressBar() + observeWebView() startLoading() } @@ -144,7 +165,7 @@ private extension AuthenticatedWebViewController { ]) } - func startLoading() { + func observeWebView() { webView.publisher(for: \.estimatedProgress) .sink { [weak self] progress in if progress == 1 { @@ -157,42 +178,98 @@ private extension AuthenticatedWebViewController { webView.publisher(for: \.url) .sink { [weak self] url in - guard let self else { return } - let initialURL = self.viewModel.initialURL - // avoids infinite loop if the initial url happens to be the nonce retrieval path. - if url?.absoluteString.contains(WKWebView.wporgNoncePath) == true, - initialURL?.absoluteString.contains(WKWebView.wporgNoncePath) != true { - self.loadContent() - } else { - self.viewModel.handleRedirect(for: url) - } + guard let url else { return } + self?.handleRedirect(for: url) } .store(in: &subscriptions) + } - if let siteCredentials, let request = try? webView.authenticateForWPOrg(with: siteCredentials) { - webView.load(request) - } else { - loadContent() + /// Authentication logic differs depending on the destination URL and the current site. + /// More information: pe5sF9-3Si-p2 + /// + func startLoading() { + guard let url = viewModel.initialURL else { + return + } + + switch authenticationFlow { + case .wpcom: + authenticateWPComAndLoadContent(url: url) + case .jetpackSSO: + authenticateSSOAndLoadContent(url: url) + case .siteCredentials: + authenticateUsingSiteCredentialsAndLoadContent(url: url) + case .none: + loadContent(url: url) } } } // MARK: - Helper methods private extension AuthenticatedWebViewController { - func loadContent() { - guard let url = viewModel.initialURL else { + func authenticateWPComAndLoadContent(url: URL) { + guard let wpcomCredentials, case .wpcom = wpcomCredentials else { + return loadContent(url: url) + } + do { + try webView.authenticateForWPComAndRedirect(to: url, credentials: wpcomCredentials) + } catch { + loadContent(url: url) + } + } + + func authenticateSSOAndLoadContent(url: URL) { + let tempURL = WooConstants.URLs.wpcomTempRedirectURL.asURL() + authenticateWPComAndLoadContent(url: tempURL) + } + + func authenticateUsingSiteCredentialsAndLoadContent(url: URL) { + guard let siteCredentials, let request = try? webView.authenticateForWPOrg(with: siteCredentials) else { + return loadContent(url: url) + } + webView.load(request) + } + + func loadContent(url: URL) { + let request = URLRequest(url: url) + webView.load(request) + } + + func handleRedirect(for url: URL) { + guard let initialURL = viewModel.initialURL else { return } - /// Authenticate for WP.com automatically if credentials are available. - /// - if let wpcomCredentials, case .wpcom = wpcomCredentials { - webView.authenticateForWPComAndRedirect(to: url, credentials: wpcomCredentials) - } else { - let request = URLRequest(url: url) - webView.load(request) + switch url.absoluteString { + case WooConstants.URLs.wpcomTempRedirectURL.rawValue: + guard let currentSite, let host = URL(string: currentSite.url)?.host else { + return loadContent(url: initialURL) + } + let cookie = HTTPCookie(properties: [ + .domain: host, + .path: "/", + .name: Constants.ssoRedirectCookieName, + .value: initialURL.absoluteString, + ]) + + let queryItem = URLQueryItem(name: Constants.actionParam, value: Constants.jetpackSSOAction) + guard let cookie, let loginURL = URL(string: currentSite.loginURL)?.appending(queryItems: [queryItem]) else { + return loadContent(url: initialURL) + } + webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie) + loadContent(url: loginURL) + + default: + if url.absoluteString.contains(WKWebView.wporgNoncePath) == true, + initialURL.absoluteString.contains(WKWebView.wporgNoncePath) != true { + // Site credentials login completes, now proceed to load the initial URL. + loadContent(url: initialURL) + } else { + viewModel.handleRedirect(for: url) + } } } + } extension AuthenticatedWebViewController: WKNavigationDelegate { @@ -204,7 +281,19 @@ extension AuthenticatedWebViewController: WKNavigationDelegate { } func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse) async -> WKNavigationResponsePolicy { + defer { + isFirstNavigation = false + } let response = navigationResponse.response + if let initialURL = viewModel.initialURL, + viewModel.isAuthenticationFailure(response: response, + currentSite: currentSite, + authenticationFlow: authenticationFlow, + isFirstNavigation: isFirstNavigation) { + /// When automatic authentication fails, cancel the navigation and redirect to the original URL instead. + loadContent(url: initialURL) + return .cancel + } return await viewModel.decidePolicy(for: response) } @@ -244,3 +333,11 @@ extension AuthenticatedWebViewController: WKUIDelegate { return nil } } + +private extension AuthenticatedWebViewController { + enum Constants { + static let actionParam = "action" + static let jetpackSSOAction = "jetpack-sso" + static let ssoRedirectCookieName = "jetpack_sso_redirect_to" + } +} diff --git a/WooCommerce/Classes/Authentication/AuthenticatedWebViewModel.swift b/WooCommerce/Classes/Authentication/AuthenticatedWebViewModel.swift index af06e8dd77c..41864638dc6 100644 --- a/WooCommerce/Classes/Authentication/AuthenticatedWebViewModel.swift +++ b/WooCommerce/Classes/Authentication/AuthenticatedWebViewModel.swift @@ -1,5 +1,6 @@ import Foundation import WebKit +import struct Yosemite.Site /// Optional conformance for a `AuthenticatedWebViewModel` implementation to reload a webview asynchronously. protocol WebviewReloadable { @@ -70,4 +71,78 @@ extension AuthenticatedWebViewModel { } return false } + + /// Checks for the appropriate authentication flow based on the current site and the URL to be loaded. + /// + func authenticationFlow(currentSite: Site, + wpcomCredentialsAvailable: Bool, + wporgCredentialsAvailable: Bool) -> WebViewAuthenticationFlow { + guard let initialURL else { + return .none + } + + if wpcomCredentialsAvailable { + if let domain = initialURL.host, Constants.wpcomAcceptedDomains.contains(domain) { + return .wpcom + } else if currentSite.hasSSOEnabled, + initialURL.absoluteString.hasPrefix(currentSite.url) { + return .jetpackSSO + } + } + + if wporgCredentialsAvailable { + return .siteCredentials + } else { + return .none + } + } + + /// Returns `true` if the response indicates an authentication failure and `false` otherwise. + /// + func isAuthenticationFailure(response: URLResponse, + currentSite: Site?, + authenticationFlow: WebViewAuthenticationFlow, + isFirstNavigation: Bool) -> Bool { + guard authenticationFlow != .none, + isFirstNavigation, + let currentSite, + let urlResponse = response as? HTTPURLResponse, + let url = urlResponse.url else { + return false + } + + // if the authentication request fails + if Constants.errorResponseCodes ~= urlResponse.statusCode { + return true + } + + let isAuthenticationFailure: Bool = { + switch authenticationFlow { + case .none: + return false + case .siteCredentials: + return url.absoluteString == currentSite.loginURL + case .jetpackSSO, .wpcom: + return url.absoluteString == WooConstants.URLs.loginWPCom.rawValue + } + }() + + guard isAuthenticationFailure else { + return false + } + + return true + } +} + +enum WebViewAuthenticationFlow { + case wpcom + case jetpackSSO + case siteCredentials + case none +} + +private enum Constants { + static let errorResponseCodes = 400...599 + static let wpcomAcceptedDomains = ["wordpress.com", "wp.com", "jetpack.com", "woocommerce.com", "jetpack.wordpress.com"] } diff --git a/WooCommerce/Classes/Extensions/WKWebView+Authenticated.swift b/WooCommerce/Classes/Extensions/WKWebView+Authenticated.swift index d55a4564cc2..2cce4d556c2 100644 --- a/WooCommerce/Classes/Extensions/WKWebView+Authenticated.swift +++ b/WooCommerce/Classes/Extensions/WKWebView+Authenticated.swift @@ -27,13 +27,9 @@ extension WKWebView { return try URLEncoding.default.encode(request, with: parameters) } - func authenticateForWPComAndRedirect(to url: URL, credentials: Credentials?) { + func authenticateForWPComAndRedirect(to url: URL, credentials: Credentials?) throws { customUserAgent = UserAgent.defaultUserAgent - do { - try load(authenticatedPostData(with: credentials, redirectTo: url)) - } catch { - DDLogError("⛔️ Cannot load the authenticated web view on WPCom") - } + try load(authenticatedPostData(with: credentials, redirectTo: url)) } private func authenticatedPostData(with credentials: Credentials?, redirectTo url: URL) throws -> URLRequest { diff --git a/WooCommerce/Classes/System/WooConstants.swift b/WooCommerce/Classes/System/WooConstants.swift index 3425da593cd..7035182c516 100644 --- a/WooCommerce/Classes/System/WooConstants.swift +++ b/WooCommerce/Classes/System/WooConstants.swift @@ -250,6 +250,10 @@ extension WooConstants { /// case pointOfSaleDocumentation = "https://woocommerce.com/document/woo-mobile-app-point-of-sale-mode/" + /// Temporary redirect URL for authenticated web view when authenticating WPCom automatically + /// + case wpcomTempRedirectURL = "https://wordpress.com/mobile-redirect" + #if DEBUG case orderCreationFeedback = "https://automattic.survey.fm/woo-app-order-creation-testing" #else diff --git a/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift b/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift index b0c39c466f5..2e912d05708 100644 --- a/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift +++ b/WooCommerce/Classes/ViewRelated/Products/Edit Product/ProductFormViewController.swift @@ -1065,13 +1065,6 @@ private extension ProductFormViewController { return } - let stores = ServiceLocator.stores - guard let site = stores.sessionManager.defaultSite, - stores.shouldAuthenticateAdminPage(for: site) else { - WebviewHelper.launch(url.absoluteString, with: self) - return - } - let viewModel = DefaultAuthenticatedWebViewModel(title: product.name, initialURL: url) let controller = AuthenticatedWebViewController(viewModel: viewModel) let navigationController = UINavigationController(rootViewController: controller) diff --git a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj index be52c617002..74d2e0eebbb 100644 --- a/WooCommerce/WooCommerce.xcodeproj/project.pbxproj +++ b/WooCommerce/WooCommerce.xcodeproj/project.pbxproj @@ -2583,6 +2583,7 @@ DE02ABBE2B578D0E008E0AC4 /* CreditCardType.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE02ABBD2B578D0E008E0AC4 /* CreditCardType.swift */; }; DE02ABC02B57D333008E0AC4 /* BlazeConfirmPaymentViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE02ABBF2B57D333008E0AC4 /* BlazeConfirmPaymentViewModelTests.swift */; }; DE02ABC22B5903AB008E0AC4 /* BlazeCampaignCreationErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE02ABC12B5903AB008E0AC4 /* BlazeCampaignCreationErrorView.swift */; }; + DE06D6602D64699D00419FFA /* AuthenticatedWebViewModelTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE06D65F2D64699D00419FFA /* AuthenticatedWebViewModelTests.swift */; }; DE02C65C2D5A0B9F0089850D /* FailedProductImageCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE02C65B2D5A0B9F0089850D /* FailedProductImageCollectionViewCell.swift */; }; DE02C65E2D5A0C5D0089850D /* FailedProductImageCollectionViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = DE02C65D2D5A0C5D0089850D /* FailedProductImageCollectionViewCell.xib */; }; DE0A2EAA281BA083007A8015 /* ProductCategoryList.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE0A2EA9281BA083007A8015 /* ProductCategoryList.swift */; }; @@ -5777,6 +5778,7 @@ DE02ABBD2B578D0E008E0AC4 /* CreditCardType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreditCardType.swift; sourceTree = ""; }; DE02ABBF2B57D333008E0AC4 /* BlazeConfirmPaymentViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeConfirmPaymentViewModelTests.swift; sourceTree = ""; }; DE02ABC12B5903AB008E0AC4 /* BlazeCampaignCreationErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BlazeCampaignCreationErrorView.swift; sourceTree = ""; }; + DE06D65F2D64699D00419FFA /* AuthenticatedWebViewModelTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthenticatedWebViewModelTests.swift; sourceTree = ""; }; DE02C65B2D5A0B9F0089850D /* FailedProductImageCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FailedProductImageCollectionViewCell.swift; sourceTree = ""; }; DE02C65D2D5A0C5D0089850D /* FailedProductImageCollectionViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = FailedProductImageCollectionViewCell.xib; sourceTree = ""; }; DE0A2EA9281BA083007A8015 /* ProductCategoryList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProductCategoryList.swift; sourceTree = ""; }; @@ -9713,6 +9715,7 @@ DEF13C532963ED4E0024A02B /* PostSiteCredentialLoginCheckerTests.swift */, DE66C56E2978F24200DAA978 /* ApplicationPasswordDisabledViewModelTests.swift */, 02B21C5429C84E4A00C5623B /* WPAdminWebViewModelTests.swift */, + DE06D65F2D64699D00419FFA /* AuthenticatedWebViewModelTests.swift */, ); path = Authentication; sourceTree = ""; @@ -17586,6 +17589,7 @@ 02F5F80E246102240000613A /* FilterProductListViewModel+numberOfActiveFiltersTests.swift in Sources */, 021AEF9C2407B07300029D28 /* ProductImageStatus+HelpersTests.swift in Sources */, 024A543622BA84DB00F4F38E /* DeveloperEmailCheckerTests.swift in Sources */, + DE06D6602D64699D00419FFA /* AuthenticatedWebViewModelTests.swift in Sources */, B541B2132189E7FD008FE7C1 /* ScannerWooTests.swift in Sources */, CC4B252D27CFE443008D2E6E /* OrderTotalsCalculatorTests.swift in Sources */, DEF8CF1A29AC6E5900800A60 /* AdminRoleRequiredViewModelTests.swift in Sources */, diff --git a/WooCommerce/WooCommerceTests/Authentication/AuthenticatedWebViewModelTests.swift b/WooCommerce/WooCommerceTests/Authentication/AuthenticatedWebViewModelTests.swift new file mode 100644 index 00000000000..a25f287636d --- /dev/null +++ b/WooCommerce/WooCommerceTests/Authentication/AuthenticatedWebViewModelTests.swift @@ -0,0 +1,222 @@ +import Foundation +import Testing +import Yosemite +@testable import WooCommerce + +struct AuthenticatedWebViewModelTests { + + private static let testURLs = [ + URL(string: "https://wordpress.com/jetpack/connect?url=%@&mobile_redirect=%@&from=mobile"), + URL(string: "https://jetpack.wordpress.com/jetpack.authorize"), + URL(string: "https://woocommerce.com/products/product-bundles/"), + URL(string: "https://jetpack.com/stats/") + ] + private let siteURL = "http://example.com" + + @Test(arguments: Self.testURLs) + func authenticationFlow_for_wpcom_pages_when_wpcom_credentials_are_available(_ initialURL: URL?) throws { + // Given + let unwrappedURL = try #require(initialURL) + let viewModel = DefaultAuthenticatedWebViewModel(initialURL: unwrappedURL) + let testSite = Site.fake().copy(siteID: 123, url: siteURL, hasSSOEnabled: true) + + // When + let authenticationFlow = viewModel.authenticationFlow(currentSite: testSite, + wpcomCredentialsAvailable: true, + wporgCredentialsAvailable: false) + + // Then + #expect(authenticationFlow == .wpcom) + } + + @Test func authenticationFlow_for_site_pages_when_site_has_SSO_enabled() throws { + // Given + let testSite = Site.fake().copy(siteID: 123, url: siteURL, hasSSOEnabled: true) + let testURL = try #require(URL(string: siteURL + "/products/13")) + let viewModel = DefaultAuthenticatedWebViewModel(initialURL: testURL) + + // When + let authenticationFlow = viewModel.authenticationFlow(currentSite: testSite, + wpcomCredentialsAvailable: true, + wporgCredentialsAvailable: false) + + // Then + #expect(authenticationFlow == .jetpackSSO) + } + + @Test func authenticationFlow_for_site_pages_when_site_has_SSO_disabled() throws { + // Given + let testSite = Site.fake().copy(siteID: 123, url: siteURL, hasSSOEnabled: false) + let testURL = try #require(URL(string: siteURL + "/products/13")) + let viewModel = DefaultAuthenticatedWebViewModel(initialURL: testURL) + + // When + let authenticationFlow = viewModel.authenticationFlow(currentSite: testSite, + wpcomCredentialsAvailable: true, + wporgCredentialsAvailable: false) + + // Then + #expect(authenticationFlow == .none) + } + + @Test func authenticationFlow_when_user_is_authenticated_with_wporg_crendentials() throws { + // Given + let testSite = Site.fake().copy(siteID: 123, url: siteURL, hasSSOEnabled: false) + let testURL = try #require(URL(string: siteURL + "/products/13")) + let viewModel = DefaultAuthenticatedWebViewModel(initialURL: testURL) + + // When + let authenticationFlow = viewModel.authenticationFlow(currentSite: testSite, + wpcomCredentialsAvailable: false, + wporgCredentialsAvailable: true) + + // Then + #expect(authenticationFlow == .siteCredentials) + } + + @Test func authenticationFlow_when_user_is_not_authenticated_with_wpcom_or_wporg_crendentials() throws { + // Given + let testSite = Site.fake().copy(siteID: 123, url: siteURL, hasSSOEnabled: true) + let testURL = try #require(URL(string: siteURL + "/products/13")) + let viewModel = DefaultAuthenticatedWebViewModel(initialURL: testURL) + + // When + let authenticationFlow = viewModel.authenticationFlow(currentSite: testSite, + wpcomCredentialsAvailable: false, + wporgCredentialsAvailable: false) + + // Then + #expect(authenticationFlow == .none) + } + + @Test func isAuthenticationFailure_returns_true_if_first_navigation_fails_with_error_status_code() throws { + // Given + let testSite = Site.fake().copy(siteID: 123, url: siteURL, hasSSOEnabled: true) + let loginURL = try #require(URL(string: siteURL + "/wp-login.php")) + let response = try #require(HTTPURLResponse(url: loginURL, statusCode: 404, httpVersion: nil, headerFields: nil)) + + let testURL = try #require(URL(string: siteURL + "/products/13")) + let viewModel = DefaultAuthenticatedWebViewModel(initialURL: testURL) + + // When + let isFailure = viewModel.isAuthenticationFailure(response: response, + currentSite: testSite, + authenticationFlow: .siteCredentials, + isFirstNavigation: true) + + // Then + #expect(isFailure == true) + } + + @Test func isAuthenticationFailure_returns_true_if_first_navigation_url_is_login_page_for_site_credentials() throws { + // Given + let loginURL = try #require(URL(string: siteURL + "/wp-login.php")) + let testSite = Site.fake().copy(siteID: 123, url: siteURL, loginURL: loginURL.absoluteString) + let response = try #require(HTTPURLResponse(url: loginURL, statusCode: 200, httpVersion: nil, headerFields: nil)) + + let testURL = try #require(URL(string: siteURL + "/products/13")) + let viewModel = DefaultAuthenticatedWebViewModel(initialURL: testURL) + + // When + let isFailure = viewModel.isAuthenticationFailure(response: response, + currentSite: testSite, + authenticationFlow: .siteCredentials, + isFirstNavigation: true) + + // Then + #expect(isFailure == true) + } + + @Test func isAuthenticationFailure_returns_true_if_first_navigation_url_is_login_page_for_wpcom() throws { + // Given + let testSite = Site.fake().copy(siteID: 123, url: siteURL, hasSSOEnabled: true) + let loginURL = WooConstants.URLs.loginWPCom.asURL() + let response = try #require(HTTPURLResponse(url: loginURL, statusCode: 200, httpVersion: nil, headerFields: nil)) + + let testURL = try #require(URL(string: siteURL + "/products/13")) + let viewModel = DefaultAuthenticatedWebViewModel(initialURL: testURL) + + // When + let isFailure = viewModel.isAuthenticationFailure(response: response, + currentSite: testSite, + authenticationFlow: .wpcom, + isFirstNavigation: true) + + // Then + #expect(isFailure == true) + } + + @Test func isAuthenticationFailure_returns_true_if_first_navigation_url_is_login_page_for_jetpack_sso() throws { + // Given + let testSite = Site.fake().copy(siteID: 123, url: siteURL, hasSSOEnabled: true) + let loginURL = WooConstants.URLs.loginWPCom.asURL() + let response = try #require(HTTPURLResponse(url: loginURL, statusCode: 200, httpVersion: nil, headerFields: nil)) + + let testURL = try #require(URL(string: siteURL + "/products/13")) + let viewModel = DefaultAuthenticatedWebViewModel(initialURL: testURL) + + // When + let isFailure = viewModel.isAuthenticationFailure(response: response, + currentSite: testSite, + authenticationFlow: .jetpackSSO, + isFirstNavigation: true) + + // Then + #expect(isFailure == true) + } + + @Test func isAuthenticationFailure_returns_true_if_first_navigation_url_is_login_page_when_authentication_flow_is_none() throws { + // Given + let testSite = Site.fake().copy(siteID: 123, url: siteURL) + let loginURL = WooConstants.URLs.loginWPCom.asURL() + let response = try #require(HTTPURLResponse(url: loginURL, statusCode: 200, httpVersion: nil, headerFields: nil)) + + let testURL = try #require(URL(string: siteURL + "/products/13")) + let viewModel = DefaultAuthenticatedWebViewModel(initialURL: testURL) + + // When + let isFailure = viewModel.isAuthenticationFailure(response: response, + currentSite: testSite, + authenticationFlow: .none, + isFirstNavigation: true) + + // Then + #expect(isFailure == false) + } + + @Test func isAuthenticationFailure_returns_false_for_first_page_being_non_login_page() throws { + // Given + let testSite = Site.fake().copy(siteID: 123, url: siteURL, hasSSOEnabled: true) + let testURL = try #require(URL(string: siteURL + "/products/13")) + let response = try #require(HTTPURLResponse(url: testURL, statusCode: 200, httpVersion: nil, headerFields: nil)) + let viewModel = DefaultAuthenticatedWebViewModel(initialURL: testURL) + + // When + let isFailure = viewModel.isAuthenticationFailure(response: response, + currentSite: testSite, + authenticationFlow: .jetpackSSO, + isFirstNavigation: true) + + // Then + #expect(isFailure == false) + } + + @Test func isAuthenticationFailure_returns_false_for_non_first_navigation() throws { + // Given + let testSite = Site.fake().copy(siteID: 123, url: siteURL, hasSSOEnabled: true) + let loginURL = WooConstants.URLs.loginWPCom.asURL() + let response = try #require(HTTPURLResponse(url: loginURL, statusCode: 200, httpVersion: nil, headerFields: nil)) + + let testURL = try #require(URL(string: siteURL + "/products/13")) + let viewModel = DefaultAuthenticatedWebViewModel(initialURL: testURL) + + // When + let isFailure = viewModel.isAuthenticationFailure(response: response, + currentSite: testSite, + authenticationFlow: .jetpackSSO, + isFirstNavigation: false) + + // Then + #expect(isFailure == false) + } +}