diff --git a/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift b/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift index 6e10bfcb71f..b7221466180 100644 --- a/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift +++ b/Modules/Sources/Yosemite/Actions/JetpackConnectionAction.swift @@ -16,6 +16,16 @@ 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 the Jetpack connection by sending a request to WPCom. + 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/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..0363ed14ba4 100644 --- a/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift +++ b/Modules/Sources/Yosemite/Stores/JetpackConnectionStore.swift @@ -9,6 +9,9 @@ public final class JetpackConnectionStore: DeauthenticatedStore { 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) { super.init(dispatcher: dispatcher) } @@ -42,6 +45,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 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) } @@ -87,6 +96,49 @@ 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, + siteURL: String, + 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 { + 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/Tests/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift b/Modules/Tests/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift index ff24fd3eb0d..09d89c86e91 100644 --- a/Modules/Tests/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift +++ b/Modules/Tests/YosemiteTests/Mocks/Networking/Remote/MockSiteRemote.swift @@ -50,6 +50,7 @@ final class MockSiteRemote { func whenUpdatingSiteTitle(thenReturn result: Result) { updateSiteTitleResult = result } + } extension MockSiteRemote: SiteRemoteProtocol { diff --git a/Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift b/Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift index ab1b853db63..e2254e9262f 100644 --- a/Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift +++ b/Modules/Tests/YosemiteTests/Stores/JetpackConnectionStoreTests.swift @@ -285,4 +285,143 @@ 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_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 6c2ac1daabe..cdf0aecdf6d 100644 --- a/Modules/Tests/YosemiteTests/Stores/SiteStoreTests.swift +++ b/Modules/Tests/YosemiteTests/Stores/SiteStoreTests.swift @@ -364,6 +364,7 @@ final class SiteStoreTests: XCTestCase { let error = try XCTUnwrap(result.failure) XCTAssertEqual(error as? DotcomError, .unknown(code: "error", message: nil)) } + } private extension SiteStoreTests {