Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7895ade
Inject siteID from `DefaultStoresManager` to `AlamofireNetwork`
itsmeichigo Dec 13, 2022
0099895
Add new request type for REST API requests
itsmeichigo Dec 13, 2022
3efefba
Configure application password use case in AlamofireNetwork
itsmeichigo Dec 13, 2022
f4e1519
Add new method to update headers for REST requests
itsmeichigo Dec 13, 2022
823607a
Update AlamofireNetwork to handle application password
itsmeichigo Dec 13, 2022
583d756
Use Task only for fetching application password
itsmeichigo Dec 14, 2022
763ba7f
Add a fallback Jetpack request to REST request to trigger when applic…
itsmeichigo Dec 14, 2022
168a6ac
Fix line limit violation
itsmeichigo Dec 14, 2022
9b0d019
Update comment for RESTRequest initializer
itsmeichigo Dec 14, 2022
17cae6a
Inject the complete use case to AlamofireNetwork from AuthenticatedState
itsmeichigo Dec 14, 2022
5e07911
Update comments for AlamofireNetwork
itsmeichigo Dec 14, 2022
85c31f8
Revert "Inject the complete use case to AlamofireNetwork from Authent…
itsmeichigo Dec 14, 2022
9740cbb
Separate authentication logic to a new class
itsmeichigo Dec 14, 2022
cc87a50
Make ApplicationPasswordUseCase injectable
itsmeichigo Dec 14, 2022
ad69556
Add unit tests for RequestAuthenticator
itsmeichigo Dec 14, 2022
4b5423e
Update comments for RESTRequest
itsmeichigo Dec 14, 2022
3ede699
Fix incorrect username and password when authenticating rest request
itsmeichigo Dec 15, 2022
d1ea84c
Reformat code for RequestAuthenticator
itsmeichigo Dec 15, 2022
5afc7d7
Update coupon mappers to support response without data envelope
itsmeichigo Dec 15, 2022
9e02521
Rename mock json file
itsmeichigo Dec 15, 2022
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
20 changes: 20 additions & 0 deletions Networking/Networking.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -709,6 +709,11 @@
DEC51AF92769A212009F3DF4 /* SystemStatus+Settings.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC51AF82769A212009F3DF4 /* SystemStatus+Settings.swift */; };
DEC51AFB2769C66B009F3DF4 /* SystemStatusMapperTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC51AFA2769C66B009F3DF4 /* SystemStatusMapperTests.swift */; };
DEC51B02276AFB35009F3DF4 /* SystemStatus+DropinMustUsePlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC51B01276AFB34009F3DF4 /* SystemStatus+DropinMustUsePlugin.swift */; };
DEFBA74E29485A7600C35BA9 /* RESTRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFBA74D29485A7600C35BA9 /* RESTRequest.swift */; };
DEFBA7542949CE6600C35BA9 /* RequestAuthenticator.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFBA7532949CE6600C35BA9 /* RequestAuthenticator.swift */; };
DEFBA7562949D17400C35BA9 /* RequestAuthenticatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEFBA7552949D17300C35BA9 /* RequestAuthenticatorTests.swift */; };
DEFBA759294AC3C200C35BA9 /* coupon-without-data.json in Resources */ = {isa = PBXBuildFile; fileRef = DEFBA757294AC3C200C35BA9 /* coupon-without-data.json */; };
DEFBA75A294AC3C200C35BA9 /* coupons-all-without-data.json in Resources */ = {isa = PBXBuildFile; fileRef = DEFBA758294AC3C200C35BA9 /* coupons-all-without-data.json */; };
E12552C526385B05001CEE70 /* ShippingLabelAddressValidationSuccess.swift in Sources */ = {isa = PBXBuildFile; fileRef = E12552C426385B05001CEE70 /* ShippingLabelAddressValidationSuccess.swift */; };
E137619929151C7400FD098F /* error-wp-rest-forbidden.json in Resources */ = {isa = PBXBuildFile; fileRef = E137619829151C7400FD098F /* error-wp-rest-forbidden.json */; };
E137619B2915222100FD098F /* WordPressApiValidatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E137619A2915222100FD098F /* WordPressApiValidatorTests.swift */; };
Expand Down Expand Up @@ -1467,6 +1472,11 @@
DEC51AF82769A212009F3DF4 /* SystemStatus+Settings.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SystemStatus+Settings.swift"; sourceTree = "<group>"; };
DEC51AFA2769C66B009F3DF4 /* SystemStatusMapperTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemStatusMapperTests.swift; sourceTree = "<group>"; };
DEC51B01276AFB34009F3DF4 /* SystemStatus+DropinMustUsePlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "SystemStatus+DropinMustUsePlugin.swift"; sourceTree = "<group>"; };
DEFBA74D29485A7600C35BA9 /* RESTRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RESTRequest.swift; sourceTree = "<group>"; };
DEFBA7532949CE6600C35BA9 /* RequestAuthenticator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestAuthenticator.swift; sourceTree = "<group>"; };
DEFBA7552949D17300C35BA9 /* RequestAuthenticatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RequestAuthenticatorTests.swift; sourceTree = "<group>"; };
DEFBA757294AC3C200C35BA9 /* coupon-without-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "coupon-without-data.json"; sourceTree = "<group>"; };
DEFBA758294AC3C200C35BA9 /* coupons-all-without-data.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "coupons-all-without-data.json"; sourceTree = "<group>"; };
E12552C426385B05001CEE70 /* ShippingLabelAddressValidationSuccess.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelAddressValidationSuccess.swift; sourceTree = "<group>"; };
E137619829151C7400FD098F /* error-wp-rest-forbidden.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "error-wp-rest-forbidden.json"; sourceTree = "<group>"; };
E137619A2915222100FD098F /* WordPressApiValidatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordPressApiValidatorTests.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1695,6 +1705,7 @@
B518662320A099BF00037A38 /* AlamofireNetwork.swift */,
B518662620A09BCC00037A38 /* MockNetwork.swift */,
D87F6150226591E10031A13B /* NullNetwork.swift */,
DEFBA7532949CE6600C35BA9 /* RequestAuthenticator.swift */,
);
path = Network;
sourceTree = "<group>";
Expand Down Expand Up @@ -1905,6 +1916,7 @@
B557D9FF209754FF005962F4 /* JetpackRequest.swift */,
DE34051228BDCA5100CF0D97 /* WordPressOrgRequest.swift */,
029C9E5B291507A40013E5EE /* UnauthenticatedRequest.swift */,
DEFBA74D29485A7600C35BA9 /* RESTRequest.swift */,
);
path = Requests;
sourceTree = "<group>";
Expand Down Expand Up @@ -2061,6 +2073,8 @@
74A7B4BD217A841400E85A8B /* broken-settings-general.json */,
03DCB7512624B3BE00C8953D /* coupons-all.json */,
03DCB77D262738E200C8953D /* coupon.json */,
DEFBA757294AC3C200C35BA9 /* coupon-without-data.json */,
DEFBA758294AC3C200C35BA9 /* coupons-all-without-data.json */,
DE2095C027966EC800171F1C /* coupon-reports.json */,
B58D10C72114D21C00107ED4 /* generic_error.json */,
B53EF53521813681003E146F /* generic_success.json */,
Expand Down Expand Up @@ -2373,6 +2387,7 @@
isa = PBXGroup;
children = (
B57B1E6621C916850046E764 /* NetworkErrorTests.swift */,
DEFBA7552949D17300C35BA9 /* RequestAuthenticatorTests.swift */,
);
path = Network;
sourceTree = "<group>";
Expand Down Expand Up @@ -2754,6 +2769,7 @@
31054728262E2FEE00C5C02B /* wcpay-payment-intent-canceled.json in Resources */,
31A451CC27863A2E00FE81AA /* stripe-account-rejected-fraud.json in Resources */,
31A451D827863A2E00FE81AA /* stripe-account-restricted-overdue.json in Resources */,
DEFBA75A294AC3C200C35BA9 /* coupons-all-without-data.json in Resources */,
D865CE69278CA245002C8520 /* stripe-payment-intent-unknown-status.json in Resources */,
0205021C27C86B9700FB1C6B /* inbox-note-without-isRead.json in Resources */,
24F98C622502EFF600F49B68 /* feature-flags-load-all.json in Resources */,
Expand Down Expand Up @@ -2834,6 +2850,7 @@
45CCFCEA27A2E59B0012E8CB /* inbox-note-list.json in Resources */,
025CA2C8238F4FF400B05C81 /* product-shipping-classes-load-all.json in Resources */,
74046E21217A73D0007DD7BF /* settings-general.json in Resources */,
DEFBA759294AC3C200C35BA9 /* coupon-without-data.json in Resources */,
03E8FEC427B40E3F00F5FC7D /* wcpay-charge-card-present-minimal.json in Resources */,
31054724262E2FC600C5C02B /* wcpay-payment-intent-requires-capture.json in Resources */,
7492FAE3217FBDBC00ED2C69 /* settings-general-alt.json in Resources */,
Expand Down Expand Up @@ -3044,6 +3061,7 @@
CE132BBC223859710029DB6C /* ProductTag.swift in Sources */,
26650332261FFA1A0079A159 /* ProductAddOnEnvelope.swift in Sources */,
D88D5A47230BC838007B6E01 /* ProductReview.swift in Sources */,
DEFBA74E29485A7600C35BA9 /* RESTRequest.swift in Sources */,
456930A9264EB576009ED69D /* ShippingLabelCarriersAndRates.swift in Sources */,
741B950120EBC8A700DD6E2D /* OrderCouponLine.swift in Sources */,
020D07BA23D8542000FD9580 /* UploadableMedia.swift in Sources */,
Expand All @@ -3057,6 +3075,7 @@
020D07B823D852BB00FD9580 /* Media.swift in Sources */,
B5BB1D0C20A2050300112D92 /* DateFormatter+Woo.swift in Sources */,
743E84EE2217244C00FAC9D7 /* ShipmentTrackingListMapper.swift in Sources */,
DEFBA7542949CE6600C35BA9 /* RequestAuthenticator.swift in Sources */,
451A97E5260B631E0059D135 /* ShippingLabelPredefinedPackage.swift in Sources */,
BAB373722795A1FB00837B4A /* OrderTaxLine.swift in Sources */,
EE54C8942947229800A9BF61 /* ApplicationPasswordUseCase.swift in Sources */,
Expand Down Expand Up @@ -3458,6 +3477,7 @@
0212683524C046CB00F8A892 /* MockNetwork+Path.swift in Sources */,
68BD37B328D9B8BD00C2A517 /* CustomerRemoteTests.swift in Sources */,
B554FA932180C17200C54DFF /* NoteHashListMapperTests.swift in Sources */,
DEFBA7562949D17400C35BA9 /* RequestAuthenticatorTests.swift in Sources */,
CC07866526790B1100BA9AC1 /* ShippingLabelPurchaseMapperTests.swift in Sources */,
74002D6A2118B26100A63C19 /* SiteVisitStatsMapperTests.swift in Sources */,
743E84FA221742E300FAC9D7 /* ShipmentsRemoteTests.swift in Sources */,
Expand Down
10 changes: 8 additions & 2 deletions Networking/Networking/Mapper/CouponListMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,14 @@ struct CouponListMapper: Mapper {
/// (Attempts) to convert a dictionary into `[Coupon]`.
///
func map(response: Data) throws -> [Coupon] {
let coupons = try Coupon.decoder.decode(CouponListEnvelope.self, from: response).coupons
return coupons.map { $0.copy(siteID: siteID) }
let decoder = Coupon.decoder
do {
let coupons = try decoder.decode(CouponListEnvelope.self, from: response).coupons
return coupons.map { $0.copy(siteID: siteID) }
} catch {
return try decoder.decode([Coupon].self, from: response)
.map { $0.copy(siteID: siteID) }
}
}
}

Expand Down
9 changes: 7 additions & 2 deletions Networking/Networking/Mapper/CouponMapper.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,13 @@ struct CouponMapper: Mapper {
/// (Attempts) to convert a dictionary into `Coupon`.
///
func map(response: Data) throws -> Coupon {
let coupon = try Coupon.decoder.decode(CouponEnvelope.self, from: response).coupon
return coupon.copy(siteID: siteID)
let decoder = Coupon.decoder
do {
let coupon = try decoder.decode(CouponEnvelope.self, from: response).coupon
return coupon.copy(siteID: siteID)
} catch {
return try decoder.decode(Coupon.self, from: response).copy(siteID: siteID)
}
}
}

Expand Down
71 changes: 40 additions & 31 deletions Networking/Networking/Network/AlamofireNetwork.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,28 @@ import Combine
import Foundation
import Alamofire


extension Alamofire.MultipartFormData: MultipartFormData {}

/// AlamofireWrapper: Encapsulates all of the Alamofire OP's
///
public class AlamofireNetwork: Network {
/// WordPress.com Credentials.
///
private let credentials: Credentials?

private let backgroundSessionManager: Alamofire.SessionManager

/// WordPress.com Credentials.
/// Authenticator to update requests authorization header if possible.
///
private let credentials: Credentials?
private let requestAuthenticator: RequestAuthenticator

public var session: URLSession { SessionManager.default.session }

/// Public Initializer
///
public required init(credentials: Credentials?) {
self.credentials = credentials
self.requestAuthenticator = RequestAuthenticator(credentials: credentials)

// A unique ID is included in the background session identifier so that the session does not get invalidated when the initializer is called multiple
// times (e.g. when logging in).
Expand All @@ -45,12 +48,12 @@ public class AlamofireNetwork: Network {
/// - Yes. We do the above because the Jetpack Tunnel endpoint doesn't properly relay the correct statusCode.
///
public func responseData(for request: URLRequestConvertible, completion: @escaping (Data?, Error?) -> Void) {
let request = createRequest(wrapping: request)

Alamofire.request(request)
.responseData { response in
completion(response.value, response.networkingError)
}
requestAuthenticator.authenticateRequest(request) { request in
Alamofire.request(request)
.responseData { response in
completion(response.value, response.networkingError)
}
}
}

/// Executes the specified Network Request. Upon completion, the payload will be sent back to the caller as a Data instance.
Expand All @@ -63,10 +66,10 @@ public class AlamofireNetwork: Network {
/// - completion: Closure to be executed upon completion.
///
public func responseData(for request: URLRequestConvertible, completion: @escaping (Swift.Result<Data, Error>) -> Void) {
let request = createRequest(wrapping: request)

Alamofire.request(request).responseData { response in
completion(response.result.toSwiftResult())
requestAuthenticator.authenticateRequest(request) { request in
Alamofire.request(request).responseData { response in
completion(response.result.toSwiftResult())
}
}
}

Expand All @@ -79,38 +82,44 @@ public class AlamofireNetwork: Network {
/// - Parameter request: Request that should be performed.
/// - Returns: A publisher that emits the result of the given request.
public func responseDataPublisher(for request: URLRequestConvertible) -> AnyPublisher<Swift.Result<Data, Error>, Never> {
let request = createRequest(wrapping: request)

return Future() { promise in
Alamofire.request(request).responseData { response in
let result = response.result.toSwiftResult()
promise(Swift.Result.success(result))
self.requestAuthenticator.authenticateRequest(request) { request in
Alamofire.request(request).responseData { response in
let result = response.result.toSwiftResult()
promise(Swift.Result.success(result))
}
}
}.eraseToAnyPublisher()
}

public func uploadMultipartFormData(multipartFormData: @escaping (MultipartFormData) -> Void,
to request: URLRequestConvertible,
completion: @escaping (Data?, Error?) -> Void) {
let request = createRequest(wrapping: request)

backgroundSessionManager.upload(multipartFormData: multipartFormData, with: request) { (encodingResult) in
switch encodingResult {
case .success(let upload, _, _):
upload.responseData { response in
completion(response.value, response.error)
requestAuthenticator.authenticateRequest(request) { [weak self] request in
guard let self else { return }
self.backgroundSessionManager.upload(multipartFormData: multipartFormData, with: request) { (encodingResult) in
switch encodingResult {
case .success(let upload, _, _):
upload.responseData { response in
completion(response.value, response.error)
}
case .failure(let error):
completion(nil, error)
}
case .failure(let error):
completion(nil, error)
}
}
}
}

private extension AlamofireNetwork {
func createRequest(wrapping request: URLRequestConvertible) -> URLRequestConvertible {
credentials.map { AuthenticatedRequest(credentials: $0, request: request) } ??
UnauthenticatedRequest(request: request)
public extension AlamofireNetwork {
/// Updates the application password use case with a new site ID.
///
func configureApplicationPasswordHandler(with siteID: Int64) {
guard let credentials else {
return
}
let applicationPasswordUseCase = TemporaryApplicationPasswordUseCase(siteID: siteID, credentials: credentials)
requestAuthenticator.updateApplicationPasswordHandler(with: applicationPasswordUseCase)
}
}

Expand Down
88 changes: 88 additions & 0 deletions Networking/Networking/Network/RequestAuthenticator.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import Alamofire
import Foundation

// TODO: Replace with actual implementation.
final class TemporaryApplicationPasswordUseCase: ApplicationPasswordUseCase {
init(siteID: Int64, credentials: Credentials) {
// no-op
}

var applicationPassword: ApplicationPassword? {
return nil
}

func generateNewPassword() async throws -> ApplicationPassword {
return .init(wpOrgUsername: "test", password: .init("12345"))
}

func deletePassword() async throws {
// no-op
}
}

/// Helper class to update requests with authorization header if possible.
///
final class RequestAuthenticator {
/// WordPress.com Credentials.
///
private let credentials: Credentials?

/// The use case to handle authentication with application passwords.
///
private var applicationPasswordUseCase: ApplicationPasswordUseCase?

init(credentials: Credentials?) {
self.credentials = credentials
}

/// Updates the application password use case with a new site ID.
///
func updateApplicationPasswordHandler(with useCase: ApplicationPasswordUseCase) {
applicationPasswordUseCase = useCase
}

/// Updates a request with application password or WPCOM token if possible.
///
func authenticateRequest(_ request: URLRequestConvertible, completion: @escaping (URLRequestConvertible) -> Void) {
guard let restRequest = request as? RESTRequest,
let useCase = applicationPasswordUseCase else {
// Handle non-REST requests as before
return completion(authenticateUsingWPCOMTokenIfPossible(request))
}
Task(priority: .medium) {
let result = await authenticateUsingApplicationPassword(restRequest, useCase: useCase)
await MainActor.run {
completion(result)
}
}
}

/// Attempts authenticating a request with application password.
///
private func authenticateUsingApplicationPassword(_ restRequest: RESTRequest, useCase: ApplicationPasswordUseCase) async -> URLRequestConvertible {
do {
let applicationPassword: ApplicationPassword = try await {
if let password = useCase.applicationPassword {
return password
}
return try await useCase.generateNewPassword()
}()
return try await MainActor.run {
return try restRequest.authenticateRequest(with: applicationPassword)
}
} catch {
DDLogWarn("⚠️ Error generating application password and update request: \(error)")
// TODO: add Tracks
// Get the fallback Jetpack request to handle if possible.
let fallbackRequest: URLRequestConvertible = restRequest.fallbackRequest ?? restRequest
return authenticateUsingWPCOMTokenIfPossible(fallbackRequest)
}
}

/// Attempts creating a request with WPCOM token if possible.
///
private func authenticateUsingWPCOMTokenIfPossible(_ request: URLRequestConvertible) -> URLRequestConvertible {
credentials.map { AuthenticatedRequest(credentials: $0, request: request) } ??
UnauthenticatedRequest(request: request)
}
}
Loading