Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Modules/Sources/NetworkingCore/Network/AlamofireNetwork.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ public struct JetpackSite: Equatable {
}
}

public enum RequestAuthenticationMode: String {
case appPasswords = "app_passwords"
case appPasswordsWithJetpack = "app_passwords_with_jetpack" // switching to app password for Jetpack sites
case jetpackTunnel = "jetpack_tunnel"
}

extension Alamofire.MultipartFormData: MultipartFormData {
public func append(_ data: Data, withName name: String) {
self.append(data, withName: name, fileName: nil, mimeType: nil)
Expand All @@ -24,6 +30,10 @@ extension Alamofire.MultipartFormData: MultipartFormData {
/// AlamofireWrapper: Encapsulates all of the Alamofire OP's
///
public class AlamofireNetwork: Network {

/// authentication mode for requests
public private(set) var authenticationMode: RequestAuthenticationMode?

/// Lazy-initialized session manager. Use ensuresSessionManagerIsInitialized=true to avoid race conditions with concurrent requests.
private lazy var alamofireSession: Alamofire.Session = {
let sessionConfiguration = URLSessionConfiguration.default
Expand Down Expand Up @@ -88,6 +98,18 @@ public class AlamofireNetwork: Network {
} else if ensuresSessionManagerIsInitialized {
self.alamofireSession = makeSession(configuration: URLSessionConfiguration.default)
}

let authenticationMode: RequestAuthenticationMode? = {
switch credentials {
case .wporg, .applicationPassword:
return .appPasswords
case .wpcom:
return .jetpackTunnel
case .none:
return nil
}
}()
updateAuthenticationMode(authenticationMode)
}

public func updateAppPasswordSwitching(enabled: Bool) {
Expand All @@ -98,6 +120,7 @@ public class AlamofireNetwork: Network {
requestConverter = RequestConverter(siteAddress: nil)
requestAuthenticator.updateAuthenticator(DefaultRequestAuthenticator(credentials: credentials))
requestAuthenticator.delegate = nil
updateAuthenticationMode(.jetpackTunnel)
}
}

Expand Down Expand Up @@ -276,6 +299,7 @@ private extension AlamofireNetwork {
requestConverter = RequestConverter(siteAddress: nil)
requestAuthenticator.updateAuthenticator(DefaultRequestAuthenticator(credentials: credentials))
requestAuthenticator.delegate = nil
updateAuthenticationMode(.jetpackTunnel)
return
}
requestConverter = RequestConverter(siteAddress: site.siteAddress)
Expand All @@ -286,8 +310,15 @@ private extension AlamofireNetwork {
))
requestAuthenticator.delegate = self
errorHandler.resetFailureCount(for: site.siteID) // reset failure count
updateAuthenticationMode(.appPasswordsWithJetpack)
}
}

func updateAuthenticationMode(_ mode: RequestAuthenticationMode?) {
DispatchQueue.main.async { [weak self] in
self?.authenticationMode = mode
}
}
}

// MARK: Helper methods for error handling
Expand Down
4 changes: 4 additions & 0 deletions Modules/Sources/Yosemite/Base/StoresManager.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Combine
import Foundation
import enum NetworkingCore.RequestAuthenticationMode

/// Abstracts the Stores coordination
///
Expand Down Expand Up @@ -53,6 +54,9 @@ public protocol StoresManager {
///
var isAuthenticatedWithoutWPCom: Bool { get }

/// Authentication mode for network requests
var requestAuthenticationMode: RequestAuthenticationMode? { get }

/// Publishes signal that indicates if the user is currently logged in with credentials.
///
var isLoggedInPublisher: AnyPublisher<Bool, Never> { get }
Expand Down
5 changes: 5 additions & 0 deletions Modules/Sources/Yosemite/Model/Mocks/MockStoresManager.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import Combine
import Storage
import enum NetworkingCore.RequestAuthenticationMode

public class MockStoresManager: StoresManager {

Expand Down Expand Up @@ -223,6 +224,10 @@ public class MockStoresManager: StoresManager {
false
}

public var requestAuthenticationMode: RequestAuthenticationMode? {
.jetpackTunnel
}

public var needsDefaultStore: Bool {
sessionManager.defaultStoreID == nil
}
Expand Down
136 changes: 136 additions & 0 deletions Modules/Tests/NetworkingTests/Network/AlamofireNetworkTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,142 @@ final class AlamofireNetworkTests: XCTestCase {
let networkError = result.1 as? NetworkError
XCTAssertEqual(networkError?.errorCode, "failed")
}

// MARK: - Authentication Mode Tests

func test_authenticationMode_is_appPasswords_for_wporg_credentials() {
// Given
let wporgCredentials = Credentials.wporg(username: "user", password: "pass", siteAddress: "https://example.com")

// When
let network = AlamofireNetwork(credentials: wporgCredentials, sessionManager: createSessionWithMockURLProtocol())

// Then
let expectation = XCTestExpectation(description: "Authentication mode should be set")
DispatchQueue.main.async {
XCTAssertEqual(network.authenticationMode, .appPasswords)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
}

func test_authenticationMode_is_appPasswords_for_applicationPassword_credentials() {
// Given
let appPasswordCredentials = Credentials.applicationPassword(username: "user", password: "pass", siteAddress: "https://example.com")

// When
let network = AlamofireNetwork(credentials: appPasswordCredentials, sessionManager: createSessionWithMockURLProtocol())

// Then
let expectation = XCTestExpectation(description: "Authentication mode should be set")
DispatchQueue.main.async {
XCTAssertEqual(network.authenticationMode, .appPasswords)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
}

func test_authenticationMode_is_jetpackTunnel_for_wpcom_credentials() {
// Given
let wpcomCredentials = createWPComCredentials()

// When
let network = AlamofireNetwork(credentials: wpcomCredentials, sessionManager: createSessionWithMockURLProtocol())

// Then
let expectation = XCTestExpectation(description: "Authentication mode should be set")
DispatchQueue.main.async {
XCTAssertEqual(network.authenticationMode, .jetpackTunnel)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
}

func test_authenticationMode_is_nil_for_no_credentials() {
// When
let network = AlamofireNetwork(credentials: nil, sessionManager: createSessionWithMockURLProtocol())

// Then
let expectation = XCTestExpectation(description: "Authentication mode should be set")
DispatchQueue.main.async {
XCTAssertNil(network.authenticationMode)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
}

func test_authenticationMode_changes_to_appPasswordsWithJetpack_when_app_password_switching_enabled() {
// Given
let siteID: Int64 = 123
let wpcomCredentials = createWPComCredentials()
let network = createNetworkWithSelectedSite(siteID: siteID, credentials: wpcomCredentials, userDefaults: userDefaults)

// When - Enable app password switching
network.updateAppPasswordSwitching(enabled: true)

// Then
let expectation = XCTestExpectation(description: "Authentication mode should change")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
XCTAssertEqual(network.authenticationMode, .appPasswordsWithJetpack)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
}

func test_authenticationMode_reverts_to_jetpackTunnel_when_app_password_switching_disabled() {
// Given
let siteID: Int64 = 456
let wpcomCredentials = createWPComCredentials()
let network = createNetworkWithSelectedSite(siteID: siteID, credentials: wpcomCredentials, userDefaults: userDefaults)

// When - Enable then disable app password switching
network.updateAppPasswordSwitching(enabled: true)
network.updateAppPasswordSwitching(enabled: false)

// Then
let expectation = XCTestExpectation(description: "Authentication mode should revert")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
XCTAssertEqual(network.authenticationMode, .jetpackTunnel)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
}

func test_authenticationMode_remains_jetpackTunnel_when_site_flagged_as_unsupported() {
// Given
let siteID: Int64 = 789
let wpcomCredentials = createWPComCredentials()
userDefaults.applicationPasswordUnsupportedList = [String(siteID): Date()]
let network = createNetworkWithSelectedSite(siteID: siteID, credentials: wpcomCredentials, userDefaults: userDefaults)

// When - Enable app password switching for an unsupported site
network.updateAppPasswordSwitching(enabled: true)

// Then
let expectation = XCTestExpectation(description: "Authentication mode should remain jetpackTunnel")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
XCTAssertEqual(network.authenticationMode, .jetpackTunnel)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
}

func test_authenticationMode_does_not_change_for_non_wpcom_credentials() {
// Given
let wporgCredentials = Credentials.wporg(username: "user", password: "pass", siteAddress: "https://example.com")
let network = AlamofireNetwork(credentials: wporgCredentials, sessionManager: createSessionWithMockURLProtocol())

// When - Try to enable app password switching (should have no effect)
network.updateAppPasswordSwitching(enabled: true)

// Then
let expectation = XCTestExpectation(description: "Authentication mode should remain unchanged")
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
XCTAssertEqual(network.authenticationMode, .appPasswords)
expectation.fulfill()
}
wait(for: [expectation], timeout: 1.0)
}
}

private extension AlamofireNetworkTests {
Expand Down
9 changes: 8 additions & 1 deletion WooCommerce/Classes/Analytics/WooAnalyticsEvent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -147,12 +147,19 @@ extension WooAnalyticsEvent {
pageNumber: Int,
filters: FilterOrderListViewModel.Filters?,
totalCompletedOrders: Int?) -> WooAnalyticsEvent {
let requestAuthMode: String = {
guard let authState = ServiceLocator.stores.requestAuthenticationMode else {
return "none"
}
return authState.rawValue
}()
let properties: [String: WooAnalyticsEventPropertyType?] = [
"status": (filters?.orderStatus ?? []).map { $0.rawValue }.joined(separator: ","),
"page_number": Int64(pageNumber),
"total_duration": Double(totalDuration),
"date_range": filters?.dateRange?.analyticsDescription ?? String(),
"total_completed_orders": totalCompletedOrders
"total_completed_orders": totalCompletedOrders,
"request_type": requestAuthMode
]
return WooAnalyticsEvent(statName: .ordersListLoaded, properties: properties.compactMapValues { $0 })
}
Expand Down
5 changes: 5 additions & 0 deletions WooCommerce/Classes/Yosemite/AuthenticatedState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,16 @@ import Yosemite
import Networking
import Storage
import Combine
import enum NetworkingCore.RequestAuthenticationMode

// MARK: - AuthenticatedState
//
class AuthenticatedState: StoresManagerState {

var requestAuthenticationMode: RequestAuthenticationMode? {
network.authenticationMode
}

/// Dispatcher: Glues all of the Stores!
///
private let dispatcher = Dispatcher()
Expand Down
9 changes: 9 additions & 0 deletions WooCommerce/Classes/Yosemite/DefaultStoresManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import KeychainAccess
import class WidgetKit.WidgetCenter
import Experiments
import WordPressAuthenticator
import enum NetworkingCore.RequestAuthenticationMode

// MARK: - DefaultStoresManager
//
Expand Down Expand Up @@ -79,6 +80,14 @@ class DefaultStoresManager: StoresManager {
return state is AuthenticatedState
}

/// Authentication mode for network requests
var requestAuthenticationMode: RequestAuthenticationMode? {
guard let state = state as? AuthenticatedState else {
return nil
}
return state.requestAuthenticationMode
}

/// Indicates if the StoresManager is currently authenticated with site credentials only.
///
var isAuthenticatedWithoutWPCom: Bool {
Expand Down