Skip to content

Commit 75aeefa

Browse files
authored
Update AlamofireNetwork to support application password experiment (#16027)
2 parents 2ed4650 + 627348b commit 75aeefa

File tree

12 files changed

+184
-86
lines changed

12 files changed

+184
-86
lines changed

Modules/Sources/Experiments/DefaultFeatureFlagService.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,8 @@ public struct DefaultFeatureFlagService: FeatureFlagService {
100100
return true
101101
case .pointOfSaleHistoricalOrdersi1:
102102
return buildConfig == .localDeveloper || buildConfig == .alpha
103+
case .applicationPasswordExperiment:
104+
return buildConfig == .localDeveloper || buildConfig == .alpha
103105
case .pointOfSaleLocalCatalogi1:
104106
return buildConfig == .localDeveloper || buildConfig == .alpha
105107
default:

Modules/Sources/Experiments/FeatureFlag.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,10 @@ public enum FeatureFlag: Int {
207207
///
208208
case pointOfSaleHistoricalOrdersi1
209209

210+
/// Enables switching Jetpack requests to use application password
211+
///
212+
case applicationPasswordExperiment
213+
210214
/// Enables Local Catalog i1 in Point of Sale.
211215
/// It syncs products and variations to local storage and display them in POS for quick access.
212216
///

Modules/Sources/Networking/Model/Site.swift

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import Foundation
22
import Codegen
3+
import struct NetworkingCore.JetpackSite
34

45
/// Represents a WordPress.com Site.
56
///
@@ -93,7 +94,7 @@ public struct Site: Decodable, Equatable, Hashable, GeneratedFakeable, Generated
9394
public let hasSSOEnabled: Bool
9495

9596
/// Whether application password authentication is available
96-
/// periphery: ignore - to be used as part of WOOMOB-1123
97+
///
9798
public let applicationPasswordAvailable: Bool
9899

99100
/// Decodable Conformance.
@@ -322,6 +323,10 @@ public extension Site {
322323
var isPrivateWPCOMSite: Bool {
323324
return isWordPressComStore && (visibility == .privateSite)
324325
}
326+
327+
func toJetpackSite() -> JetpackSite {
328+
JetpackSite(siteID: siteID, siteAddress: url, applicationPasswordAvailable: applicationPasswordAvailable)
329+
}
325330
}
326331

327332
private extension Site {

Modules/Sources/NetworkingCore/ApplicationPassword/ApplicationPasswordUseCase.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,6 @@ final public class DefaultApplicationPasswordUseCase: ApplicationPasswordUseCase
6161
}
6262

6363
/// Internal initializer
64-
/// periphery: ignore - used in future PR for WOOMOB-1123
6564
init(type: AuthenticationType,
6665
network: Network,
6766
keychain: Keychain = Keychain(service: WooConstants.keychainServiceName)) {

Modules/Sources/NetworkingCore/ApplicationPassword/RequestAuthenticator.swift

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,26 +37,51 @@ public struct DefaultRequestAuthenticator: RequestAuthenticator {
3737
///
3838
private let applicationPasswordUseCase: ApplicationPasswordUseCase?
3939

40+
private let siteAddress: String?
41+
4042
/// Sets up the authenticator with optional credentials and application password use case.
4143
/// `applicationPasswordUseCase` can be injected for unit tests.
4244
///
43-
init(credentials: Credentials?, applicationPasswordUseCase: ApplicationPasswordUseCase? = nil) {
45+
init(credentials: Credentials?,
46+
selectedSite: JetpackSite? = nil,
47+
applicationPasswordUseCase: ApplicationPasswordUseCase? = nil,
48+
network: Network? = nil) {
4449
self.credentials = credentials
45-
let useCase: ApplicationPasswordUseCase? = {
50+
51+
self.applicationPasswordUseCase = {
4652
if let applicationPasswordUseCase {
4753
return applicationPasswordUseCase
48-
} else if case let .wporg(username, password, siteAddress) = credentials {
54+
}
55+
switch credentials {
56+
case let .some(.wporg(username, password, siteAddress)):
4957
return try? DefaultApplicationPasswordUseCase(username: username,
5058
password: password,
5159
siteAddress: siteAddress)
52-
} else if let credentials,
53-
case .applicationPassword(_, _, let siteAddress) = credentials {
60+
case .some(.applicationPassword(_, _, let siteAddress)):
5461
return OneTimeApplicationPasswordUseCase(siteAddress: siteAddress)
55-
} else {
62+
case .some(.wpcom):
63+
guard let network, let selectedSite else {
64+
return nil
65+
}
66+
return DefaultApplicationPasswordUseCase(
67+
type: .wpcom(siteID: selectedSite.siteID),
68+
network: network
69+
)
70+
default:
5671
return nil
5772
}
5873
}()
59-
self.applicationPasswordUseCase = useCase
74+
75+
self.siteAddress = {
76+
switch credentials {
77+
case let .some(.wporg(_, _, siteAddress)):
78+
return siteAddress
79+
case let .some(.applicationPassword(_, _, siteAddress)):
80+
return siteAddress
81+
default:
82+
return selectedSite?.siteAddress
83+
}
84+
}()
6085
}
6186

6287
/// Authenticates the provided urlRequest using the `credentials`
@@ -93,16 +118,6 @@ private extension DefaultRequestAuthenticator {
93118
/// To check whether the given URLRequest is a REST API request
94119
///
95120
func isRestAPIRequest(_ urlRequest: URLRequest) -> Bool {
96-
let siteAddress: String? = {
97-
switch credentials {
98-
case let .wporg(_, _, siteAddress):
99-
return siteAddress
100-
case let .applicationPassword(_, _, siteAddress):
101-
return siteAddress
102-
default:
103-
return nil
104-
}
105-
}()
106121
guard let siteAddress,
107122
let url = urlRequest.url,
108123
url.absoluteString.hasPrefix(siteAddress.trimSlashes() + "/" + RESTRequest.Settings.basePath) else {

Modules/Sources/NetworkingCore/ApplicationPassword/RequestConverter.swift

Lines changed: 1 addition & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,12 @@ import Alamofire
33
/// Converter to convert Jetpack tunnel requests into REST API requests if needed
44
///
55
struct RequestConverter {
6-
let credentials: Credentials?
6+
let siteAddress: String?
77

88
func convert(_ request: URLRequestConvertible) -> URLRequestConvertible {
99
if request is RESTRequest {
1010
return request
1111
}
12-
let siteAddress: String? = {
13-
switch credentials {
14-
case let .wporg(_, _, siteAddress):
15-
return siteAddress
16-
case let .applicationPassword(_, _, siteAddress):
17-
return siteAddress
18-
default:
19-
return nil
20-
}
21-
}()
2212
guard let convertibleRequest = request as? RESTRequestConvertible,
2313
let siteAddress,
2414
let restRequest = convertibleRequest.asRESTRequest(with: siteAddress) else {

Modules/Sources/NetworkingCore/ApplicationPassword/RequestProcessor.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ final class RequestProcessor: RequestInterceptor {
88

99
private var isAuthenticating = false
1010

11-
private let requestAuthenticator: RequestAuthenticator
11+
private var requestAuthenticator: RequestAuthenticator
1212

1313
private let notificationCenter: NotificationCenter
1414

@@ -17,6 +17,10 @@ final class RequestProcessor: RequestInterceptor {
1717
self.requestAuthenticator = requestAuthenticator
1818
self.notificationCenter = notificationCenter
1919
}
20+
21+
func updateAuthenticator(_ authenticator: RequestAuthenticator) {
22+
requestAuthenticator = authenticator
23+
}
2024
}
2125

2226
// MARK: Request Authentication

Modules/Sources/NetworkingCore/Network/AlamofireNetwork.swift

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,19 @@ import Combine
22
import Foundation
33
import Alamofire
44

5+
/// Helper type to observe selected site info
6+
public struct JetpackSite: Equatable {
7+
let siteID: Int64
8+
let siteAddress: String
9+
let applicationPasswordAvailable: Bool
10+
11+
public init(siteID: Int64, siteAddress: String, applicationPasswordAvailable: Bool) {
12+
self.siteID = siteID
13+
self.siteAddress = siteAddress
14+
self.applicationPasswordAvailable = applicationPasswordAvailable
15+
}
16+
}
17+
518
extension Alamofire.MultipartFormData: MultipartFormData {
619
public func append(_ data: Data, withName name: String) {
720
self.append(data, withName: name, fileName: nil, mimeType: nil)
@@ -17,27 +30,59 @@ public class AlamofireNetwork: Network {
1730
return sessionManager
1831
}()
1932

33+
private let credentials: Credentials?
34+
35+
private let selectedSite: AnyPublisher<JetpackSite?, Never>?
36+
2037
/// Converter to convert Jetpack tunnel requests into REST API requests if applicable
2138
///
22-
private let requestConverter: RequestConverter
39+
private var requestConverter: RequestConverter
2340

2441
/// Authenticator to update requests authorization header if possible.
2542
///
2643
private let requestAuthenticator: RequestProcessor
2744

2845
public var session: URLSession { Session.default.session }
2946

47+
private var subscription: AnyCancellable?
48+
3049
/// Public Initializer
3150
///
3251
///
33-
public required init(credentials: Credentials?, sessionManager: Alamofire.Session? = nil) {
34-
self.requestConverter = RequestConverter(credentials: credentials)
52+
public required init(credentials: Credentials?,
53+
selectedSite: AnyPublisher<JetpackSite?, Never>? = nil,
54+
sessionManager: Alamofire.Session? = nil) {
55+
self.credentials = credentials
56+
self.selectedSite = selectedSite
57+
self.requestConverter = {
58+
let siteAddress: String? = {
59+
switch credentials {
60+
case let .wporg(_, _, siteAddress):
61+
return siteAddress
62+
case let .applicationPassword(_, _, siteAddress):
63+
return siteAddress
64+
default:
65+
return nil
66+
}
67+
}()
68+
return RequestConverter(siteAddress: siteAddress)
69+
}()
3570
self.requestAuthenticator = RequestProcessor(requestAuthenticator: DefaultRequestAuthenticator(credentials: credentials))
3671
if let sessionManager {
3772
self.alamofireSession = sessionManager
3873
}
3974
}
4075

76+
public func updateAppPasswordSwitching(enabled: Bool) {
77+
guard let credentials, case .wpcom = credentials else { return }
78+
if enabled, let selectedSite {
79+
observeSelectedSite(selectedSite)
80+
} else {
81+
requestConverter = RequestConverter(siteAddress: nil)
82+
requestAuthenticator.updateAuthenticator(DefaultRequestAuthenticator(credentials: credentials))
83+
}
84+
}
85+
4186
/// Executes the specified Network Request. Upon completion, the payload will be sent back to the caller as a Data instance.
4287
///
4388
/// - Important:
@@ -142,6 +187,27 @@ private extension AlamofireNetwork {
142187
func makeSession(configuration sessionConfiguration: URLSessionConfiguration) -> Alamofire.Session {
143188
Alamofire.Session(configuration: sessionConfiguration, interceptor: requestAuthenticator)
144189
}
190+
191+
/// Updates `requestConverter` and `requestAuthenticator` when selected site changes
192+
///
193+
func observeSelectedSite(_ selectedSite: AnyPublisher<JetpackSite?, Never>) {
194+
subscription = selectedSite
195+
.removeDuplicates()
196+
.sink { [weak self] site in
197+
guard let self else { return }
198+
guard let site, site.applicationPasswordAvailable else {
199+
requestConverter = RequestConverter(siteAddress: nil)
200+
requestAuthenticator.updateAuthenticator(DefaultRequestAuthenticator(credentials: credentials))
201+
return
202+
}
203+
requestConverter = RequestConverter(siteAddress: site.siteAddress)
204+
requestAuthenticator.updateAuthenticator(DefaultRequestAuthenticator(
205+
credentials: credentials,
206+
selectedSite: site,
207+
network: self
208+
))
209+
}
210+
}
145211
}
146212

147213
private extension DataRequest {

Modules/Tests/NetworkingTests/Network/DefaultRequestAuthenticatorTests.swift

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,28 @@ final class DefaultRequestAuthenticatorTests: XCTestCase {
134134
await fulfillment(of: [exp], timeout: Constants.expectationTimeout)
135135
}
136136

137+
func test_authenticate_returns_REST_request_with_authorization_header_when_authenticated_with_wpcom_and_selected_site_is_provided() throws {
138+
// Given
139+
let siteURL = "https://test.com/"
140+
let credentials: Credentials = .wpcom(username: "username", authToken: "auth_token", siteAddress: siteURL)
141+
let selectedSite = JetpackSite(siteID: 123, siteAddress: siteURL, applicationPasswordAvailable: true)
142+
let useCase = MockApplicationPasswordUseCase(mockApplicationPassword: applicationPassword)
143+
let authenticator = DefaultRequestAuthenticator(credentials: credentials, selectedSite: selectedSite, applicationPasswordUseCase: useCase)
144+
let wooAPIVersion = WooAPIVersion.mark1
145+
let basePath = RESTRequest.Settings.basePath
146+
let restRequest = RESTRequest(siteURL: siteURL, wooApiVersion: wooAPIVersion, method: .get, path: "test")
147+
148+
// When
149+
let request = try restRequest.asURLRequest()
150+
let updatedRequest = try authenticator.authenticate(request)
151+
152+
// Then
153+
let expectedURL = "https://test.com/\(basePath)\(wooAPIVersion.path)test"
154+
assertEqual(expectedURL, updatedRequest.url?.absoluteString)
155+
let authorizationValue = try XCTUnwrap(updatedRequest.allHTTPHeaderFields?["Authorization"])
156+
XCTAssertTrue(authorizationValue.hasPrefix("Basic"))
157+
}
158+
137159
func test_shouldRetry_returns_true_for_REST_request() throws {
138160
// Given
139161
let siteURL = "https://test.com/"
@@ -164,6 +186,23 @@ final class DefaultRequestAuthenticatorTests: XCTestCase {
164186
// Then
165187
XCTAssertFalse(authenticator.shouldRetry(request))
166188
}
189+
190+
func test_shouldRetry_returns_true_for_REST_request_when_authenticated_with_wpcom_credentials_and_selected_site_is_provided() throws {
191+
// Given
192+
let siteURL = "https://test.com/"
193+
let credentials: Credentials = .wpcom(username: "username", authToken: "auth_token", siteAddress: siteURL)
194+
let selectedSite = JetpackSite(siteID: 123, siteAddress: siteURL, applicationPasswordAvailable: true)
195+
let useCase = MockApplicationPasswordUseCase(mockApplicationPassword: applicationPassword)
196+
let authenticator = DefaultRequestAuthenticator(credentials: credentials, selectedSite: selectedSite, applicationPasswordUseCase: useCase)
197+
let wooAPIVersion = WooAPIVersion.mark1
198+
let restRequest = RESTRequest(siteURL: siteURL, wooApiVersion: wooAPIVersion, method: .get, path: "test")
199+
200+
// When
201+
let request = try restRequest.asURLRequest()
202+
203+
// Then
204+
XCTAssertTrue(authenticator.shouldRetry(request))
205+
}
167206
}
168207

169208
/// MOCK: application password use case

0 commit comments

Comments
 (0)