From 045cdf4b4afdf6d709c393da02742114000147a6 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Tue, 5 Aug 2025 18:41:21 +0700 Subject: [PATCH 1/8] Add new actions for new endpoints --- .../Actions/JetpackConnectionAction.swift | 6 + Modules/Sources/Yosemite/Model/Model.swift | 1 + .../Stores/JetpackConnectionStore.swift | 42 ++++++ .../Stores/JetpackConnectionStoreTests.swift | 141 ++++++++++++++++++ 4 files changed, 190 insertions(+) diff --git a/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift b/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift index 6e10bfcb71f..8de34595e52 100644 --- a/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift +++ b/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift @@ -16,6 +16,12 @@ public enum JetpackConnectionAction: Action { case fetchJetpackConnectionURL(completion: (Result) -> Void) /// Fetches connection state with the given site's Jetpack. case fetchJetpackConnectionData(completion: (Result) -> Void) + /// Establishes site-level connection and returns WordPress.com blog ID. + case registerSite(completion: (Result) -> Void) + /// Provisions connection and returns provision response with scope and secret. + case provisionConnection(completion: (Result) -> Void) + /// Finalizes connection to WordPress.com using provision response. + case finalizeConnection(siteID: Int64, provisionResponse: JetpackConnectionProvisionResponse, completion: (Result) -> Void) /// Fetches the WPCom account with the given network case loadWPComAccount(network: Network, onCompletion: (Account?) -> Void) } diff --git a/Modules/Sources/Yosemite/Model/Model.swift b/Modules/Sources/Yosemite/Model/Model.swift index 07bde778884..39277cd92cf 100644 --- a/Modules/Sources/Yosemite/Model/Model.swift +++ b/Modules/Sources/Yosemite/Model/Model.swift @@ -51,6 +51,7 @@ public typealias GoogleAdsCampaignStatsItem = Networking.GoogleAdsCampaignStatsI public typealias InboxNote = Networking.InboxNote public typealias InboxAction = Networking.InboxAction public typealias JetpackConnectionData = Networking.JetpackConnectionData +public typealias JetpackConnectionProvisionResponse = Networking.JetpackConnectionProvisionResponse public typealias JustInTimeMessageHook = Networking.JustInTimeMessagesRemote.MessagePath.Hook public typealias Media = Networking.Media public typealias MetaContainer = Networking.MetaContainer diff --git a/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift b/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift index 395135ebe84..82e0e65004d 100644 --- a/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift +++ b/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift @@ -42,6 +42,12 @@ public final class JetpackConnectionStore: DeauthenticatedStore { fetchJetpackConnectionURL(completion: completion) case .fetchJetpackConnectionData(let completion): fetchJetpackConnectionData(completion: completion) + case .registerSite(let completion): + registerSite(completion: completion) + case .provisionConnection(let completion): + provisionConnection(completion: completion) + case .finalizeConnection(let siteID, let provisionResponse, let completion): + finalizeConnection(siteID: siteID, provisionResponse: provisionResponse, completion: completion) case .loadWPComAccount(let network, let onCompletion): loadWPComAccount(network: network, onCompletion: onCompletion) } @@ -87,6 +93,42 @@ private extension JetpackConnectionStore { jetpackConnectionRemote?.fetchJetpackConnectionData(completion: completion) } + func registerSite(completion: @escaping (Result) -> Void) { + guard let jetpackConnectionRemote else { return } + Task { @MainActor in + do { + let blogID = try await jetpackConnectionRemote.registerSite() + completion(.success(blogID)) + } catch { + completion(.failure(error)) + } + } + } + + func provisionConnection(completion: @escaping (Result) -> Void) { + guard let jetpackConnectionRemote else { return } + Task { @MainActor in + do { + let response = try await jetpackConnectionRemote.provisionConnection() + completion(.success(response)) + } catch { + completion(.failure(error)) + } + } + } + + func finalizeConnection(siteID: Int64, provisionResponse: JetpackConnectionProvisionResponse, completion: @escaping (Result) -> Void) { + guard let jetpackConnectionRemote else { return } + Task { @MainActor in + do { + try await jetpackConnectionRemote.finalizeConnection(siteID: siteID, provisionResponse: provisionResponse) + completion(.success(())) + } catch { + completion(.failure(error)) + } + } + } + func loadWPComAccount(network: Network, onCompletion: @escaping (Account?) -> Void) { let remote = AccountRemote(network: network) remote.loadAccount { result in diff --git a/Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift b/Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift index ab1b853db63..80ce2b3022b 100644 --- a/Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift +++ b/Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift @@ -285,4 +285,145 @@ final class JetpackConnectionStoreTests: XCTestCase { // Then XCTAssertNil(result) } + + func test_registerSite_returns_correct_blogID() throws { + // Given + let urlSuffix = "/jetpack/v4/connection/register" + network.simulateResponse(requestUrlSuffix: urlSuffix, filename: "jetpack-connection-registration") + let store = JetpackConnectionStore(dispatcher: dispatcher) + + let setupAction = JetpackConnectionAction.authenticate(siteURL: siteURL, network: network) + store.onAction(setupAction) + + // When + let result: Result = waitFor { promise in + let action = JetpackConnectionAction.registerSite { result in + promise(result) + } + store.onAction(action) + } + + // Then + XCTAssertTrue(result.isSuccess) + let blogID = try XCTUnwrap(result.get()) + XCTAssertEqual(blogID, 1234567890) + } + + func test_registerSite_properly_relays_errors() { + // Given + let urlSuffix = "/jetpack/v4/connection/register" + let error = NetworkError.unacceptableStatusCode(statusCode: 500) + network.simulateError(requestUrlSuffix: urlSuffix, error: error) + let store = JetpackConnectionStore(dispatcher: dispatcher) + + let setupAction = JetpackConnectionAction.authenticate(siteURL: siteURL, network: network) + store.onAction(setupAction) + + // When + let result: Result = waitFor { promise in + let action = JetpackConnectionAction.registerSite { result in + promise(result) + } + store.onAction(action) + } + + // Then + XCTAssertTrue(result.isFailure) + XCTAssertEqual(result.failure as? NetworkError, error) + } + + func test_provisionConnection_returns_correct_provision_response() throws { + // Given + let urlSuffix = "/jetpack/v4/remote_provision" + network.simulateResponse(requestUrlSuffix: urlSuffix, filename: "jetpack-connection-provision") + let store = JetpackConnectionStore(dispatcher: dispatcher) + + let setupAction = JetpackConnectionAction.authenticate(siteURL: siteURL, network: network) + store.onAction(setupAction) + + // When + let result: Result = waitFor { promise in + let action = JetpackConnectionAction.provisionConnection { result in + promise(result) + } + store.onAction(action) + } + + // Then + XCTAssertTrue(result.isSuccess) + let response = try XCTUnwrap(result.get()) + XCTAssertEqual(response.userId, 123456789) + XCTAssertEqual(response.scope, "administrator") + XCTAssertEqual(response.secret, "secret_token_12345") + } + + func test_provisionConnection_properly_relays_errors() { + // Given + let urlSuffix = "/jetpack/v4/remote_provision" + let error = NetworkError.unacceptableStatusCode(statusCode: 500) + network.simulateError(requestUrlSuffix: urlSuffix, error: error) + let store = JetpackConnectionStore(dispatcher: dispatcher) + + let setupAction = JetpackConnectionAction.authenticate(siteURL: siteURL, network: network) + store.onAction(setupAction) + + // When + let result: Result = waitFor { promise in + let action = JetpackConnectionAction.provisionConnection { result in + promise(result) + } + store.onAction(action) + } + + // Then + XCTAssertTrue(result.isFailure) + XCTAssertEqual(result.failure as? NetworkError, error) + } + + func test_finalizeConnection_completes_successfully() throws { + // Given + let urlSuffix = "jetpack-remote-connect-user" + network.simulateResponse(requestUrlSuffix: urlSuffix, filename: "jetpack-connection-finalize-success") + let store = JetpackConnectionStore(dispatcher: dispatcher) + let siteID: Int64 = 12345 + let provisionResponse = JetpackConnectionProvisionResponse(userId: 123456789, scope: "administrator", secret: "secret_token_12345") + + let setupAction = JetpackConnectionAction.authenticate(siteURL: siteURL, network: network) + store.onAction(setupAction) + + // When + let result: Result = waitFor { promise in + let action = JetpackConnectionAction.finalizeConnection(siteID: siteID, provisionResponse: provisionResponse) { result in + promise(result) + } + store.onAction(action) + } + + // Then + XCTAssertTrue(result.isSuccess) + } + + func test_finalizeConnection_properly_relays_errors() { + // Given + let urlSuffix = "jetpack-remote-connect-user" + network.simulateResponse(requestUrlSuffix: urlSuffix, filename: "jetpack-connection-finalize-error") + let store = JetpackConnectionStore(dispatcher: dispatcher) + let siteID: Int64 = 12345 + let provisionResponse = JetpackConnectionProvisionResponse(userId: 123456789, scope: "administrator", secret: "secret_token_12345") + + let setupAction = JetpackConnectionAction.authenticate(siteURL: siteURL, network: network) + store.onAction(setupAction) + + // When + let result: Result = waitFor { promise in + let action = JetpackConnectionAction.finalizeConnection(siteID: siteID, provisionResponse: provisionResponse) { result in + promise(result) + } + store.onAction(action) + } + + // Then + XCTAssertTrue(result.isFailure) + XCTAssertEqual(result.failure as? JetpackConnectionRemote.ConnectionError, .alreadyConnected) + } } From 40bb80712353db333cb192552c3fdbc75bfd8293 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 6 Aug 2025 12:47:36 +0700 Subject: [PATCH 2/8] Move finalizeConnection to SiteAction --- .../Mapper/JetpackConnectionURLMapper.swift | 2 +- .../Actions/JetpackConnectionAction.swift | 2 - .../Sources/Yosemite/Actions/SiteAction.swift | 11 +++++ .../Stores/JetpackConnectionStore.swift | 12 ----- .../Sources/Yosemite/Stores/SiteStore.swift | 16 +++++++ .../Networking/Remote/MockSiteRemote.swift | 15 +++++- .../Stores/JetpackConnectionStoreTests.swift | 47 ------------------- .../YosemiteTests/Stores/SiteStoreTests.swift | 40 ++++++++++++++++ 8 files changed, 82 insertions(+), 63 deletions(-) diff --git a/Modules/Sources/Networking/Mapper/JetpackConnectionURLMapper.swift b/Modules/Sources/Networking/Mapper/JetpackConnectionURLMapper.swift index e1ba251fe0b..c312ed702c8 100644 --- a/Modules/Sources/Networking/Mapper/JetpackConnectionURLMapper.swift +++ b/Modules/Sources/Networking/Mapper/JetpackConnectionURLMapper.swift @@ -8,7 +8,7 @@ struct JetpackConnectionURLMapper: Mapper { /// func map(response: Data) throws -> URL { guard let escapedString = String(data: response, encoding: .utf8) else { - throw JetpackConnectionRemote.ConnectionError.malformedURL + throw JetpackConnectionError.malformedURL } // The API returns an escaped string with double quotes, so we need to clean it up. let urlString = escapedString diff --git a/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift b/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift index 8de34595e52..6e16a72afe3 100644 --- a/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift +++ b/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift @@ -20,8 +20,6 @@ public enum JetpackConnectionAction: Action { case registerSite(completion: (Result) -> Void) /// Provisions connection and returns provision response with scope and secret. case provisionConnection(completion: (Result) -> Void) - /// Finalizes connection to WordPress.com using provision response. - case finalizeConnection(siteID: Int64, provisionResponse: JetpackConnectionProvisionResponse, completion: (Result) -> Void) /// Fetches the WPCom account with the given network case loadWPComAccount(network: Network, onCompletion: (Account?) -> Void) } diff --git a/Modules/Sources/Yosemite/Actions/SiteAction.swift b/Modules/Sources/Yosemite/Actions/SiteAction.swift index 2bd41b7e884..2c3ffcd944b 100644 --- a/Modules/Sources/Yosemite/Actions/SiteAction.swift +++ b/Modules/Sources/Yosemite/Actions/SiteAction.swift @@ -46,6 +46,17 @@ public enum SiteAction: Action { /// Upload store profiler answers /// case uploadStoreProfilerAnswers(siteID: Int64, answers: StoreProfilerAnswers, completion: (Result) -> Void) + + /// Finalizes the Jetpack connection by sending a request to WPCom. + /// - Parameters: + /// - siteID: ID of the site + /// - siteURL: URL of the site + /// - provisionResponse: Response from the provision connection call + /// - completion: Called when the result of the finalization is available. + case finalizeJetpackConnection(siteID: Int64, + siteURL: String, + provisionResponse: JetpackConnectionProvisionResponse, + completion: (Result) -> Void) } /// The result of site creation including necessary site information. diff --git a/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift b/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift index 82e0e65004d..4829b89333f 100644 --- a/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift +++ b/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift @@ -117,18 +117,6 @@ private extension JetpackConnectionStore { } } - func finalizeConnection(siteID: Int64, provisionResponse: JetpackConnectionProvisionResponse, completion: @escaping (Result) -> Void) { - guard let jetpackConnectionRemote else { return } - Task { @MainActor in - do { - try await jetpackConnectionRemote.finalizeConnection(siteID: siteID, provisionResponse: provisionResponse) - completion(.success(())) - } catch { - completion(.failure(error)) - } - } - } - func loadWPComAccount(network: Network, onCompletion: @escaping (Account?) -> Void) { let remote = AccountRemote(network: network) remote.loadAccount { result in diff --git a/Modules/Sources/Yosemite/Stores/SiteStore.swift b/Modules/Sources/Yosemite/Stores/SiteStore.swift index 3cd68049900..fbde28b6938 100644 --- a/Modules/Sources/Yosemite/Stores/SiteStore.swift +++ b/Modules/Sources/Yosemite/Stores/SiteStore.swift @@ -56,6 +56,8 @@ public final class SiteStore: Store { updateSiteTitle(siteID: siteID, title: title, completion: completion) case let .uploadStoreProfilerAnswers(siteID, answers, completion): uploadStoreProfilerAnswers(siteID: siteID, answers: answers, completion: completion) + case let .finalizeJetpackConnection(siteID, siteURL, provisionResponse, completion): + finalizeJetpackConnection(siteID: siteID, siteURL: siteURL, provisionResponse: provisionResponse, completion: completion) } } } @@ -159,6 +161,20 @@ private extension SiteStore { } } } + + func finalizeJetpackConnection(siteID: Int64, + siteURL: String, + provisionResponse: JetpackConnectionProvisionResponse, + completion: @escaping (Result) -> Void) { + Task { @MainActor in + do { + try await remote.finalizeJetpackConnection(siteID: siteID, siteURL: siteURL, provisionResponse: provisionResponse) + completion(.success(())) + } catch { + completion(.failure(error)) + } + } + } } private extension SiteStore { diff --git a/Modules/Tests/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift b/Modules/Tests/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift index ff24fd3eb0d..0595f4d9750 100644 --- a/Modules/Tests/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift +++ b/Modules/Tests/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift @@ -22,6 +22,9 @@ final class MockSiteRemote { /// The result to return in `updateSiteTitle` private var updateSiteTitleResult: Result? + /// The result to return in `finalizeJetpackConnection` + private var finalizeJetpackConnectionResult: Result? + /// Returns the value when `createSite` is called. func whenCreatingSite(thenReturn result: Result) { createSiteResult = result @@ -50,11 +53,21 @@ final class MockSiteRemote { func whenUpdatingSiteTitle(thenReturn result: Result) { updateSiteTitleResult = result } + + /// Returns the value when `finalizeJetpackConnection` is called. + func whenFinalizingJetpackConnection(thenReturn result: Result) { + finalizeJetpackConnectionResult = result + } } extension MockSiteRemote: SiteRemoteProtocol { func finalizeJetpackConnection(siteID: Int64, siteURL: String, provisionResponse: JetpackConnectionProvisionResponse) async throws { - // no-op + guard let result = finalizeJetpackConnectionResult else { + XCTFail("Could not find result for finalizing Jetpack connection.") + throw NetworkError.notFound() + } + + return try result.get() } func createSite(name: String, flow: SiteCreationFlow) async throws -> SiteCreationResponse { diff --git a/Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift b/Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift index 80ce2b3022b..89568777808 100644 --- a/Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift +++ b/Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift @@ -379,51 +379,4 @@ final class JetpackConnectionStoreTests: XCTestCase { XCTAssertTrue(result.isFailure) XCTAssertEqual(result.failure as? NetworkError, error) } - - func test_finalizeConnection_completes_successfully() throws { - // Given - let urlSuffix = "jetpack-remote-connect-user" - network.simulateResponse(requestUrlSuffix: urlSuffix, filename: "jetpack-connection-finalize-success") - let store = JetpackConnectionStore(dispatcher: dispatcher) - let siteID: Int64 = 12345 - let provisionResponse = JetpackConnectionProvisionResponse(userId: 123456789, scope: "administrator", secret: "secret_token_12345") - - let setupAction = JetpackConnectionAction.authenticate(siteURL: siteURL, network: network) - store.onAction(setupAction) - - // When - let result: Result = waitFor { promise in - let action = JetpackConnectionAction.finalizeConnection(siteID: siteID, provisionResponse: provisionResponse) { result in - promise(result) - } - store.onAction(action) - } - - // Then - XCTAssertTrue(result.isSuccess) - } - - func test_finalizeConnection_properly_relays_errors() { - // Given - let urlSuffix = "jetpack-remote-connect-user" - network.simulateResponse(requestUrlSuffix: urlSuffix, filename: "jetpack-connection-finalize-error") - let store = JetpackConnectionStore(dispatcher: dispatcher) - let siteID: Int64 = 12345 - let provisionResponse = JetpackConnectionProvisionResponse(userId: 123456789, scope: "administrator", secret: "secret_token_12345") - - let setupAction = JetpackConnectionAction.authenticate(siteURL: siteURL, network: network) - store.onAction(setupAction) - - // When - let result: Result = waitFor { promise in - let action = JetpackConnectionAction.finalizeConnection(siteID: siteID, provisionResponse: provisionResponse) { result in - promise(result) - } - store.onAction(action) - } - - // Then - XCTAssertTrue(result.isFailure) - XCTAssertEqual(result.failure as? JetpackConnectionRemote.ConnectionError, .alreadyConnected) - } } diff --git a/Modules/Tests/YosemiteTests/Stores/SiteStoreTests.swift b/Modules/Tests/YosemiteTests/Stores/SiteStoreTests.swift index 6c2ac1daabe..bf3254db464 100644 --- a/Modules/Tests/YosemiteTests/Stores/SiteStoreTests.swift +++ b/Modules/Tests/YosemiteTests/Stores/SiteStoreTests.swift @@ -2,6 +2,7 @@ import XCTest import enum Networking.DotcomError import enum Networking.SiteCreationError import enum Networking.WordPressApiError +import enum Networking.JetpackConnectionError import struct Networking.Site @testable import class Networking.MockNetwork @testable import Yosemite @@ -364,6 +365,45 @@ final class SiteStoreTests: XCTestCase { let error = try XCTUnwrap(result.failure) XCTAssertEqual(error as? DotcomError, .unknown(code: "error", message: nil)) } + + // MARK: - `finalizeJetpackConnection` + + func test_finalizeJetpackConnection_returns_success_on_success() throws { + // Given + remote.whenFinalizingJetpackConnection(thenReturn: .success(())) + + // When + let result = waitFor { promise in + let provisionResponse = JetpackConnectionProvisionResponse(userId: 123456789, scope: "administrator", secret: "secret_token_12345") + self.store.onAction(SiteAction.finalizeJetpackConnection(siteID: 134, + siteURL: "http://test.com", + provisionResponse: provisionResponse) { result in + promise(result) + }) + } + + // Then + XCTAssertTrue(result.isSuccess) + } + + func test_finalizeJetpackConnection_returns_error_on_failure() throws { + // Given + remote.whenFinalizingJetpackConnection(thenReturn: .failure(JetpackConnectionError.alreadyConnected)) + + // When + let result = waitFor { promise in + let provisionResponse = JetpackConnectionProvisionResponse(userId: 123456789, scope: "administrator", secret: "secret_token_12345") + self.store.onAction(SiteAction.finalizeJetpackConnection(siteID: 134, + siteURL: "http://test.com", + provisionResponse: provisionResponse) { result in + promise(result) + }) + } + + // Then + let error = try XCTUnwrap(result.failure) + XCTAssertEqual(error as? JetpackConnectionError, .alreadyConnected) + } } private extension SiteStoreTests { From e3805a59d8cca2942bab3d4287afddd373badd7f Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 6 Aug 2025 14:09:34 +0700 Subject: [PATCH 3/8] Remove outdated code --- Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift | 2 -- 1 file changed, 2 deletions(-) diff --git a/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift b/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift index 4829b89333f..7f09f9de9e7 100644 --- a/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift +++ b/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift @@ -46,8 +46,6 @@ public final class JetpackConnectionStore: DeauthenticatedStore { registerSite(completion: completion) case .provisionConnection(let completion): provisionConnection(completion: completion) - case .finalizeConnection(let siteID, let provisionResponse, let completion): - finalizeConnection(siteID: siteID, provisionResponse: provisionResponse, completion: completion) case .loadWPComAccount(let network, let onCompletion): loadWPComAccount(network: network, onCompletion: onCompletion) } From afa88ff34f0b265223de2822ea73bd2b6ce9437a Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 6 Aug 2025 14:47:28 +0700 Subject: [PATCH 4/8] Move finalizeConnection back to JetpackConnectionAction --- .../Actions/JetpackConnectionAction.swift | 12 +++++ .../Sources/Yosemite/Actions/SiteAction.swift | 11 ----- .../Stores/JetpackConnectionStore.swift | 20 +++++++++ .../Sources/Yosemite/Stores/SiteStore.swift | 16 ------- .../Networking/Remote/MockSiteRemote.swift | 13 +----- .../Stores/JetpackConnectionStoreTests.swift | 45 +++++++++++++++++++ .../YosemiteTests/Stores/SiteStoreTests.swift | 39 ---------------- 7 files changed, 78 insertions(+), 78 deletions(-) diff --git a/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift b/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift index 6e16a72afe3..c5eb66e95e3 100644 --- a/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift +++ b/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift @@ -20,6 +20,18 @@ public enum JetpackConnectionAction: Action { case registerSite(completion: (Result) -> Void) /// Provisions connection and returns provision response with scope and secret. case provisionConnection(completion: (Result) -> Void) + /// Finalizes the Jetpack connection by sending a request to WPCom. + /// - Parameters: + /// - siteID: ID of the site + /// - siteURL: URL of the site + /// - provisionResponse: Response from the provision connection call + /// - network: Network instance to create SiteRemote + /// - completion: Called when the result of the finalization is available. + case finalizeConnection(siteID: Int64, + siteURL: String, + provisionResponse: JetpackConnectionProvisionResponse, + network: Network, + completion: (Result) -> Void) /// Fetches the WPCom account with the given network case loadWPComAccount(network: Network, onCompletion: (Account?) -> Void) } diff --git a/Modules/Sources/Yosemite/Actions/SiteAction.swift b/Modules/Sources/Yosemite/Actions/SiteAction.swift index 2c3ffcd944b..2bd41b7e884 100644 --- a/Modules/Sources/Yosemite/Actions/SiteAction.swift +++ b/Modules/Sources/Yosemite/Actions/SiteAction.swift @@ -46,17 +46,6 @@ public enum SiteAction: Action { /// Upload store profiler answers /// case uploadStoreProfilerAnswers(siteID: Int64, answers: StoreProfilerAnswers, completion: (Result) -> Void) - - /// Finalizes the Jetpack connection by sending a request to WPCom. - /// - Parameters: - /// - siteID: ID of the site - /// - siteURL: URL of the site - /// - provisionResponse: Response from the provision connection call - /// - completion: Called when the result of the finalization is available. - case finalizeJetpackConnection(siteID: Int64, - siteURL: String, - provisionResponse: JetpackConnectionProvisionResponse, - completion: (Result) -> Void) } /// The result of site creation including necessary site information. diff --git a/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift b/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift index 7f09f9de9e7..216b1d803f3 100644 --- a/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift +++ b/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift @@ -8,6 +8,7 @@ public final class JetpackConnectionStore: DeauthenticatedStore { // Keep strong references to remotes to keep requests alive private var jetpackConnectionRemote: JetpackConnectionRemote? private var accountRemote: AccountRemote? + private var siteRemote: SiteRemote? public override init(dispatcher: Dispatcher) { super.init(dispatcher: dispatcher) @@ -46,6 +47,8 @@ public final class JetpackConnectionStore: DeauthenticatedStore { registerSite(completion: completion) case .provisionConnection(let completion): provisionConnection(completion: completion) + case .finalizeConnection(let siteID, let siteURL, let provisionResponse, let network, let completion): + finalizeConnection(siteID: siteID, siteURL: siteURL, provisionResponse: provisionResponse, network: network, completion: completion) case .loadWPComAccount(let network, let onCompletion): loadWPComAccount(network: network, onCompletion: onCompletion) } @@ -115,6 +118,23 @@ private extension JetpackConnectionStore { } } + func finalizeConnection(siteID: Int64, + siteURL: String, + provisionResponse: JetpackConnectionProvisionResponse, + network: Network, + completion: @escaping (Result) -> Void) { + let remote = SiteRemote(network: network, dotcomClientID: "", dotcomClientSecret: "") + Task { @MainActor in + do { + try await remote.finalizeJetpackConnection(siteID: siteID, siteURL: siteURL, provisionResponse: provisionResponse) + completion(.success(())) + } catch { + completion(.failure(error)) + } + } + self.siteRemote = remote + } + func loadWPComAccount(network: Network, onCompletion: @escaping (Account?) -> Void) { let remote = AccountRemote(network: network) remote.loadAccount { result in diff --git a/Modules/Sources/Yosemite/Stores/SiteStore.swift b/Modules/Sources/Yosemite/Stores/SiteStore.swift index fbde28b6938..3cd68049900 100644 --- a/Modules/Sources/Yosemite/Stores/SiteStore.swift +++ b/Modules/Sources/Yosemite/Stores/SiteStore.swift @@ -56,8 +56,6 @@ public final class SiteStore: Store { updateSiteTitle(siteID: siteID, title: title, completion: completion) case let .uploadStoreProfilerAnswers(siteID, answers, completion): uploadStoreProfilerAnswers(siteID: siteID, answers: answers, completion: completion) - case let .finalizeJetpackConnection(siteID, siteURL, provisionResponse, completion): - finalizeJetpackConnection(siteID: siteID, siteURL: siteURL, provisionResponse: provisionResponse, completion: completion) } } } @@ -161,20 +159,6 @@ private extension SiteStore { } } } - - func finalizeJetpackConnection(siteID: Int64, - siteURL: String, - provisionResponse: JetpackConnectionProvisionResponse, - completion: @escaping (Result) -> Void) { - Task { @MainActor in - do { - try await remote.finalizeJetpackConnection(siteID: siteID, siteURL: siteURL, provisionResponse: provisionResponse) - completion(.success(())) - } catch { - completion(.failure(error)) - } - } - } } private extension SiteStore { diff --git a/Modules/Tests/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift b/Modules/Tests/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift index 0595f4d9750..8bb0e4962b1 100644 --- a/Modules/Tests/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift +++ b/Modules/Tests/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift @@ -22,8 +22,6 @@ final class MockSiteRemote { /// The result to return in `updateSiteTitle` private var updateSiteTitleResult: Result? - /// The result to return in `finalizeJetpackConnection` - private var finalizeJetpackConnectionResult: Result? /// Returns the value when `createSite` is called. func whenCreatingSite(thenReturn result: Result) { @@ -54,20 +52,11 @@ final class MockSiteRemote { updateSiteTitleResult = result } - /// Returns the value when `finalizeJetpackConnection` is called. - func whenFinalizingJetpackConnection(thenReturn result: Result) { - finalizeJetpackConnectionResult = result - } } extension MockSiteRemote: SiteRemoteProtocol { func finalizeJetpackConnection(siteID: Int64, siteURL: String, provisionResponse: JetpackConnectionProvisionResponse) async throws { - guard let result = finalizeJetpackConnectionResult else { - XCTFail("Could not find result for finalizing Jetpack connection.") - throw NetworkError.notFound() - } - - return try result.get() + // no-op } func createSite(name: String, flow: SiteCreationFlow) async throws -> SiteCreationResponse { diff --git a/Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift b/Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift index 89568777808..e2254e9262f 100644 --- a/Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift +++ b/Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift @@ -379,4 +379,49 @@ final class JetpackConnectionStoreTests: XCTestCase { XCTAssertTrue(result.isFailure) XCTAssertEqual(result.failure as? NetworkError, error) } + + func test_finalizeJetpackConnection_returns_success_on_success() throws { + // Given + let urlSuffix = "sites/134/jetpack-remote-connect-user" + network.simulateResponse(requestUrlSuffix: urlSuffix, filename: "jetpack-connection-finalize-success") + let store = JetpackConnectionStore(dispatcher: dispatcher) + + // When + let result: Result = waitFor { promise in + let provisionResponse = JetpackConnectionProvisionResponse(userId: 123456789, scope: "administrator", secret: "secret_token_12345") + let action = JetpackConnectionAction.finalizeConnection(siteID: 134, + siteURL: "http://test.com", + provisionResponse: provisionResponse, + network: self.network) { result in + promise(result) + } + store.onAction(action) + } + + // Then + XCTAssertTrue(result.isSuccess) + } + + func test_finalizeJetpackConnection_returns_error_on_failure() throws { + // Given + let urlSuffix = "sites/134/jetpack-remote-connect-user" + network.simulateResponse(requestUrlSuffix: urlSuffix, filename: "jetpack-connection-finalize-error") + let store = JetpackConnectionStore(dispatcher: dispatcher) + + // When + let result: Result = waitFor { promise in + let provisionResponse = JetpackConnectionProvisionResponse(userId: 123456789, scope: "administrator", secret: "secret_token_12345") + let action = JetpackConnectionAction.finalizeConnection(siteID: 134, + siteURL: "http://test.com", + provisionResponse: provisionResponse, + network: self.network) { result in + promise(result) + } + store.onAction(action) + } + + // Then + XCTAssertTrue(result.isFailure) + XCTAssertEqual(result.failure as? JetpackConnectionError, .alreadyConnected) + } } diff --git a/Modules/Tests/YosemiteTests/Stores/SiteStoreTests.swift b/Modules/Tests/YosemiteTests/Stores/SiteStoreTests.swift index bf3254db464..cdf0aecdf6d 100644 --- a/Modules/Tests/YosemiteTests/Stores/SiteStoreTests.swift +++ b/Modules/Tests/YosemiteTests/Stores/SiteStoreTests.swift @@ -2,7 +2,6 @@ import XCTest import enum Networking.DotcomError import enum Networking.SiteCreationError import enum Networking.WordPressApiError -import enum Networking.JetpackConnectionError import struct Networking.Site @testable import class Networking.MockNetwork @testable import Yosemite @@ -366,44 +365,6 @@ final class SiteStoreTests: XCTestCase { XCTAssertEqual(error as? DotcomError, .unknown(code: "error", message: nil)) } - // MARK: - `finalizeJetpackConnection` - - func test_finalizeJetpackConnection_returns_success_on_success() throws { - // Given - remote.whenFinalizingJetpackConnection(thenReturn: .success(())) - - // When - let result = waitFor { promise in - let provisionResponse = JetpackConnectionProvisionResponse(userId: 123456789, scope: "administrator", secret: "secret_token_12345") - self.store.onAction(SiteAction.finalizeJetpackConnection(siteID: 134, - siteURL: "http://test.com", - provisionResponse: provisionResponse) { result in - promise(result) - }) - } - - // Then - XCTAssertTrue(result.isSuccess) - } - - func test_finalizeJetpackConnection_returns_error_on_failure() throws { - // Given - remote.whenFinalizingJetpackConnection(thenReturn: .failure(JetpackConnectionError.alreadyConnected)) - - // When - let result = waitFor { promise in - let provisionResponse = JetpackConnectionProvisionResponse(userId: 123456789, scope: "administrator", secret: "secret_token_12345") - self.store.onAction(SiteAction.finalizeJetpackConnection(siteID: 134, - siteURL: "http://test.com", - provisionResponse: provisionResponse) { result in - promise(result) - }) - } - - // Then - let error = try XCTUnwrap(result.failure) - XCTAssertEqual(error as? JetpackConnectionError, .alreadyConnected) - } } private extension SiteStoreTests { From dcc2f24831c8227837bf8c029c33a12fc608a06c Mon Sep 17 00:00:00 2001 From: Huong Do Date: Wed, 6 Aug 2025 14:48:48 +0700 Subject: [PATCH 5/8] Remove redundant comments --- .../Sources/Yosemite/Actions/JetpackConnectionAction.swift | 6 ------ 1 file changed, 6 deletions(-) diff --git a/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift b/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift index c5eb66e95e3..b7221466180 100644 --- a/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift +++ b/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift @@ -21,12 +21,6 @@ public enum JetpackConnectionAction: Action { /// Provisions connection and returns provision response with scope and secret. case provisionConnection(completion: (Result) -> Void) /// Finalizes the Jetpack connection by sending a request to WPCom. - /// - Parameters: - /// - siteID: ID of the site - /// - siteURL: URL of the site - /// - provisionResponse: Response from the provision connection call - /// - network: Network instance to create SiteRemote - /// - completion: Called when the result of the finalization is available. case finalizeConnection(siteID: Int64, siteURL: String, provisionResponse: JetpackConnectionProvisionResponse, From 3c37259e3ac01a94b61e23628745344bf5f060bb Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 7 Aug 2025 08:27:30 +0700 Subject: [PATCH 6/8] Add comment regarding empty IDs in SiteRemote --- Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift b/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift index 216b1d803f3..86bf4c37fad 100644 --- a/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift +++ b/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift @@ -123,6 +123,8 @@ private extension JetpackConnectionStore { provisionResponse: JetpackConnectionProvisionResponse, network: Network, completion: @escaping (Result) -> Void) { + /// Intentionally leaving `dotcomClientID` and `dotcomClientSecret` empty + /// as these are not needed for the `finalizeJetpackConnection` method we're using here. let remote = SiteRemote(network: network, dotcomClientID: "", dotcomClientSecret: "") Task { @MainActor in do { From 0479497ccdbe26fbf14a560c71aed67d1edf6d30 Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 7 Aug 2025 08:28:49 +0700 Subject: [PATCH 7/8] Remove redundant white space --- .../YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Modules/Tests/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift b/Modules/Tests/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift index 8bb0e4962b1..09d89c86e91 100644 --- a/Modules/Tests/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift +++ b/Modules/Tests/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift @@ -22,7 +22,6 @@ final class MockSiteRemote { /// The result to return in `updateSiteTitle` private var updateSiteTitleResult: Result? - /// Returns the value when `createSite` is called. func whenCreatingSite(thenReturn result: Result) { createSiteResult = result From 403d2e6141bc608f43c6f22e5bd334eb19f5b5dc Mon Sep 17 00:00:00 2001 From: Huong Do Date: Thu, 7 Aug 2025 13:39:26 +0700 Subject: [PATCH 8/8] Ignore periphery warning --- Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift b/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift index 86bf4c37fad..0363ed14ba4 100644 --- a/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift +++ b/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift @@ -8,6 +8,8 @@ public final class JetpackConnectionStore: DeauthenticatedStore { // Keep strong references to remotes to keep requests alive private var jetpackConnectionRemote: JetpackConnectionRemote? private var accountRemote: AccountRemote? + + /// periphery: ignore - kept with strong reference to keep network requests alive. private var siteRemote: SiteRemote? public override init(dispatcher: Dispatcher) {