Skip to content

Commit b9523bd

Browse files
authored
Merge pull request #8076 from woocommerce/issue/8075-remote-plugin-management-cookie-authentication
Login: Update Networking for plugin management endpoints with cookie authentication
2 parents 0451eec + 387a766 commit b9523bd

File tree

6 files changed

+248
-20
lines changed

6 files changed

+248
-20
lines changed

Networking/Networking.xcodeproj/project.pbxproj

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -684,6 +684,7 @@
684684
DE74F2A027E3137F0002FE59 /* setting-analytics.json in Resources */ = {isa = PBXBuildFile; fileRef = DE74F29F27E3137F0002FE59 /* setting-analytics.json */; };
685685
DE97C3922861B8E20042E973 /* CouponEncoderTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DE97C3912861B8E20042E973 /* CouponEncoderTests.swift */; };
686686
DE9D6BCC270D769C00BA6562 /* shipping-label-address-without-name-validation-success.json in Resources */ = {isa = PBXBuildFile; fileRef = DE9D6BCB270D769B00BA6562 /* shipping-label-address-without-name-validation-success.json */; };
687+
DE9DEEF5291CF1B40070AD7C /* site-plugin-without-envelope.json in Resources */ = {isa = PBXBuildFile; fileRef = DE9DEEF4291CF1B40070AD7C /* site-plugin-without-envelope.json */; };
687688
DEC2961C26BBE764005A056B /* ShippingLabelCustomsForm.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC2961B26BBE764005A056B /* ShippingLabelCustomsForm.swift */; };
688689
DEC51A95274CDA52009F3DF4 /* SitePluginMapper.swift in Sources */ = {isa = PBXBuildFile; fileRef = DEC51A94274CDA52009F3DF4 /* SitePluginMapper.swift */; };
689690
DEC51A97274DD962009F3DF4 /* plugin.json in Resources */ = {isa = PBXBuildFile; fileRef = DEC51A96274DD962009F3DF4 /* plugin.json */; };
@@ -1432,6 +1433,7 @@
14321433
DE74F29F27E3137F0002FE59 /* setting-analytics.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "setting-analytics.json"; sourceTree = "<group>"; };
14331434
DE97C3912861B8E20042E973 /* CouponEncoderTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CouponEncoderTests.swift; sourceTree = "<group>"; };
14341435
DE9D6BCB270D769B00BA6562 /* shipping-label-address-without-name-validation-success.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "shipping-label-address-without-name-validation-success.json"; sourceTree = "<group>"; };
1436+
DE9DEEF4291CF1B40070AD7C /* site-plugin-without-envelope.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = "site-plugin-without-envelope.json"; sourceTree = "<group>"; };
14351437
DEC2961B26BBE764005A056B /* ShippingLabelCustomsForm.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShippingLabelCustomsForm.swift; sourceTree = "<group>"; };
14361438
DEC51A94274CDA52009F3DF4 /* SitePluginMapper.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SitePluginMapper.swift; sourceTree = "<group>"; };
14371439
DEC51A96274DD962009F3DF4 /* plugin.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = plugin.json; sourceTree = "<group>"; };
@@ -1987,6 +1989,7 @@
19871989
B559EBA820A0B5B100836CD4 /* Responses */ = {
19881990
isa = PBXGroup;
19891991
children = (
1992+
DE9DEEF4291CF1B40070AD7C /* site-plugin-without-envelope.json */,
19901993
028CB714290223CB00331C09 /* account-username-suggestions.json */,
19911994
028CB71C2902589E00331C09 /* create-account-error-email-exists.json */,
19921995
028CB71D2902589E00331C09 /* create-account-error-invalid-email.json */,
@@ -2660,6 +2663,7 @@
26602663
D865CE5F278CA183002C8520 /* stripe-payment-intent-requires-action.json in Resources */,
26612664
CCF48B2C2628AE160034EA83 /* shipping-label-account-settings.json in Resources */,
26622665
31A451D927863A2E00FE81AA /* stripe-account-live-test.json in Resources */,
2666+
DE9DEEF5291CF1B40070AD7C /* site-plugin-without-envelope.json in Resources */,
26632667
0261F5A928D4641500B7AC72 /* products-sku-search.json in Resources */,
26642668
09885C8027C3FFD200910A62 /* product-variations-bulk-update.json in Resources */,
26652669
31054734262E36AB00C5C02B /* wcpay-payment-intent-error.json in Resources */,

Networking/Networking/Mapper/SitePluginMapper.swift

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,19 @@ struct SitePluginMapper: Mapper {
77
/// Site Identifier associated to the plugin that will be parsed.
88
/// We're injecting this field via `JSONDecoder.userInfo` because the remote endpoints don't return the SiteID in the plugin endpoint.
99
///
10-
let siteID: Int64
10+
private let siteID: Int64
11+
12+
private let withDataEnvelope: Bool
13+
14+
/// Initialized a mapper to serialize site plugins.
15+
/// - Parameters:
16+
/// - siteID: Identifier for the site. Only required in authenticated state.
17+
/// - withDataEnvelope: Whether site plugin details are wrapped inside a `data` field.
18+
///
19+
init(siteID: Int64 = -1, withDataEnvelope: Bool = true) {
20+
self.siteID = siteID
21+
self.withDataEnvelope = withDataEnvelope
22+
}
1123

1224
/// (Attempts) to convert a dictionary into SitePlugin.
1325
///
@@ -17,7 +29,11 @@ struct SitePluginMapper: Mapper {
1729
.siteID: siteID
1830
]
1931

20-
return try decoder.decode(SitePluginEnvelope.self, from: response).plugin
32+
if withDataEnvelope {
33+
return try decoder.decode(SitePluginEnvelope.self, from: response).plugin
34+
}
35+
36+
return try decoder.decode(SitePlugin.self, from: response)
2137
}
2238
}
2339

Networking/Networking/Remote/JetpackConnectionRemote.swift

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,34 @@ public final class JetpackConnectionRemote: Remote {
1313
super.init(network: network)
1414
}
1515

16+
/// Retrieves the information about Jetpack the plugin for the current site.
17+
///
18+
public func retrieveJetpackPluginDetails(completion: @escaping (Result<SitePlugin, Error>) -> Void) {
19+
let path = "\(Path.plugins)/\(Constants.jetpackPluginName)"
20+
let request = WordPressOrgRequest(baseURL: siteURL, method: .get, path: path)
21+
let mapper = SitePluginMapper(withDataEnvelope: false)
22+
enqueue(request, mapper: mapper, completion: completion)
23+
}
24+
25+
/// Installs Jetpack the plugin to the current site.
26+
///
27+
public func installJetpackPlugin(completion: @escaping (Result<SitePlugin, Error>) -> Void) {
28+
let parameters: [String: Any] = [Field.slug.rawValue: Constants.jetpackPluginSlug]
29+
let request = WordPressOrgRequest(baseURL: siteURL, method: .post, path: Path.plugins, parameters: parameters)
30+
let mapper = SitePluginMapper(withDataEnvelope: false)
31+
enqueue(request, mapper: mapper, completion: completion)
32+
}
33+
34+
/// Activates Jetpack the plugin to the current site
35+
///
36+
public func activateJetpackPlugin(completion: @escaping (Result<SitePlugin, Error>) -> Void) {
37+
let path = "\(Path.plugins)/\(Constants.jetpackPluginName)"
38+
let parameters: [String: Any] = [Field.status.rawValue: Constants.activeStatus]
39+
let request = WordPressOrgRequest(baseURL: siteURL, method: .put, path: path, parameters: parameters)
40+
let mapper = SitePluginMapper(withDataEnvelope: false)
41+
enqueue(request, mapper: mapper, completion: completion)
42+
}
43+
1644
/// Fetches the URL for setting up Jetpack connection.
1745
///
1846
public func fetchJetpackConnectionURL(completion: @escaping (Result<URL, Error>) -> Void) {
@@ -95,9 +123,18 @@ private extension JetpackConnectionRemote {
95123
enum Path {
96124
static let jetpackConnectionURL = "/jetpack/v4/connection/url"
97125
static let jetpackConnectionUser = "/jetpack/v4/connection/data"
126+
static let plugins = "/wp/v2/plugins"
127+
}
128+
129+
enum Field: String {
130+
case slug
131+
case status
98132
}
99133

100134
enum Constants {
101135
static let jetpackAccountConnectionURL = "https://jetpack.wordpress.com/jetpack.authorize"
136+
static let jetpackPluginName = "jetpack/jetpack"
137+
static let jetpackPluginSlug = "jetpack"
138+
static let activeStatus = "active"
102139
}
103140
}

Networking/NetworkingTests/Mapper/SitePluginMapperTests.swift

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,20 +11,38 @@ final class SitePluginMapperTests: XCTestCase {
1111

1212
/// Verifies the SitePlugin fields are parsed correctly.
1313
///
14-
func test_SitePlugin_fields_are_properly_parsed() {
15-
let plugin = mapPlugin(from: "plugin")
16-
XCTAssertNotNil(plugin)
17-
XCTAssertEqual(plugin?.plugin, "jetpack/jetpack")
18-
XCTAssertEqual(plugin?.siteID, dummySiteID)
19-
XCTAssertEqual(plugin?.status, .active)
20-
XCTAssertEqual(plugin?.name, "Jetpack by WordPress.com")
21-
XCTAssertEqual(plugin?.pluginUri, "https://jetpack.com")
22-
XCTAssertEqual(plugin?.author, "Automattic")
23-
XCTAssertEqual(plugin?.descriptionRaw, "Bring the power of the WordPress.com cloud to your self-hosted WordPress.")
24-
XCTAssertEqual(plugin?.descriptionRendered, "Bring the power of the WordPress.com cloud to your self-hosted WordPress. " +
14+
func test_SitePlugin_fields_are_properly_parsed() throws {
15+
let plugin = try XCTUnwrap(mapPlugin(from: "plugin"))
16+
XCTAssertEqual(plugin.plugin, "jetpack/jetpack")
17+
XCTAssertEqual(plugin.siteID, dummySiteID)
18+
XCTAssertEqual(plugin.status, .active)
19+
XCTAssertEqual(plugin.name, "Jetpack by WordPress.com")
20+
XCTAssertEqual(plugin.pluginUri, "https://jetpack.com")
21+
XCTAssertEqual(plugin.author, "Automattic")
22+
XCTAssertEqual(plugin.descriptionRaw, "Bring the power of the WordPress.com cloud to your self-hosted WordPress.")
23+
XCTAssertEqual(plugin.descriptionRendered, "Bring the power of the WordPress.com cloud to your self-hosted WordPress. " +
2524
"<cite>By <a href=\"https://jetpack.com\">Automattic</a>.</cite>")
26-
XCTAssertEqual(plugin?.version, "9.5")
27-
XCTAssertEqual(plugin?.textDomain, "jetpack")
25+
XCTAssertEqual(plugin.version, "9.5")
26+
XCTAssertEqual(plugin.textDomain, "jetpack")
27+
}
28+
29+
/// Verifies the SitePlugin fields are parsed correctly when there's no data envelope wrapping the response.
30+
///
31+
func test_SitePlugin_fields_are_properly_parsed_for_response_without_data_envelope() throws {
32+
let plugin = try XCTUnwrap(mapPluginWithoutEnvelope(from: "site-plugin-without-envelope"))
33+
XCTAssertEqual(plugin.plugin, "jetpack/jetpack")
34+
XCTAssertEqual(plugin.siteID, -1)
35+
XCTAssertEqual(plugin.status, .active)
36+
XCTAssertEqual(plugin.name, "Jetpack")
37+
XCTAssertEqual(plugin.pluginUri, "https://jetpack.com")
38+
XCTAssertEqual(plugin.author, "Automattic")
39+
XCTAssertEqual(plugin.descriptionRaw, "Security, performance, and marketing tools made by WordPress experts. " +
40+
"Jetpack keeps your site protected so you can focus on more important things.")
41+
XCTAssertEqual(plugin.descriptionRendered, "Security, performance, and marketing tools made by WordPress experts. " +
42+
"Jetpack keeps your site protected so you can focus on more important things. " +
43+
"<cite>By <a href=\"https://jetpack.com\">Automattic</a>.</cite>")
44+
XCTAssertEqual(plugin.version, "11.5.1")
45+
XCTAssertEqual(plugin.textDomain, "jetpack")
2846
}
2947
}
3048

@@ -42,4 +60,15 @@ private extension SitePluginMapperTests {
4260

4361
return try? SitePluginMapper(siteID: dummySiteID).map(response: response)
4462
}
63+
64+
/// Returns the SitePluginMapper output upon receiving `filename` (Data Encoded)
65+
/// The decoder should not include data envelope.
66+
///
67+
func mapPluginWithoutEnvelope(from filename: String) -> SitePlugin? {
68+
guard let response = Loader.contentsOf(filename) else {
69+
return nil
70+
}
71+
72+
return try? SitePluginMapper(withDataEnvelope: false).map(response: response)
73+
}
4574
}

Networking/NetworkingTests/Remote/JetpackConnectionRemoteTests.swift

Lines changed: 123 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,140 @@ import XCTest
33

44
final class JetpackConnectionRemoteTests: XCTestCase {
55

6+
private let siteURL = "http://test.com"
7+
68
/// Dummy Network Wrapper
79
///
8-
let network = MockNetwork()
10+
private let network = MockNetwork()
911

1012
/// Repeat always!
1113
///
1214
override func setUp() {
1315
network.removeAllSimulatedResponses()
1416
}
1517

18+
func test_retrieveJetpackPluginDetails_correctly_returns_parsed_plugin() throws {
19+
// Given
20+
let remote = JetpackConnectionRemote(siteURL: siteURL, network: network)
21+
let urlSuffix = "/wp/v2/plugins/jetpack/jetpack"
22+
network.simulateResponse(requestUrlSuffix: urlSuffix, filename: "site-plugin-without-envelope")
23+
24+
// When
25+
let result: Result<SitePlugin, Error> = waitFor { promise in
26+
remote.retrieveJetpackPluginDetails { result in
27+
promise(result)
28+
}
29+
}
30+
31+
// Then
32+
XCTAssertTrue(result.isSuccess)
33+
let plugin = try XCTUnwrap(result.get())
34+
assertEqual(plugin.plugin, "jetpack/jetpack")
35+
assertEqual(plugin.status, .active)
36+
assertEqual(plugin.name, "Jetpack")
37+
}
38+
39+
func test_retrieveJetpackPluginDetails_properly_relays_errors() {
40+
// Given
41+
let remote = JetpackConnectionRemote(siteURL: siteURL, network: network)
42+
let urlSuffix = "/wp/v2/plugins/jetpack/jetpack"
43+
let error = NetworkError.unacceptableStatusCode(statusCode: 500)
44+
network.simulateError(requestUrlSuffix: urlSuffix, error: error)
45+
46+
// When
47+
let result: Result<SitePlugin, Error> = waitFor { promise in
48+
remote.retrieveJetpackPluginDetails { result in
49+
promise(result)
50+
}
51+
}
52+
53+
// Then
54+
XCTAssertTrue(result.isFailure)
55+
XCTAssertEqual(result.failure as? NetworkError, error)
56+
}
57+
58+
func test_installJetpackPlugin_correctly_returns_parsed_plugin() throws {
59+
// Given
60+
let remote = JetpackConnectionRemote(siteURL: siteURL, network: network)
61+
let urlSuffix = "/wp/v2/plugins"
62+
network.simulateResponse(requestUrlSuffix: urlSuffix, filename: "site-plugin-without-envelope")
63+
64+
// When
65+
let result: Result<SitePlugin, Error> = waitFor { promise in
66+
remote.installJetpackPlugin { result in
67+
promise(result)
68+
}
69+
}
70+
71+
// Then
72+
XCTAssertTrue(result.isSuccess)
73+
let plugin = try XCTUnwrap(result.get())
74+
assertEqual(plugin.plugin, "jetpack/jetpack")
75+
assertEqual(plugin.status, .active)
76+
assertEqual(plugin.name, "Jetpack")
77+
}
78+
79+
func test_installJetpackPlugin_properly_relays_errors() {
80+
// Given
81+
let remote = JetpackConnectionRemote(siteURL: siteURL, network: network)
82+
let urlSuffix = "/wp/v2/plugins"
83+
let error = NetworkError.unacceptableStatusCode(statusCode: 500)
84+
network.simulateError(requestUrlSuffix: urlSuffix, error: error)
85+
86+
// When
87+
let result: Result<SitePlugin, Error> = waitFor { promise in
88+
remote.installJetpackPlugin { result in
89+
promise(result)
90+
}
91+
}
92+
93+
// Then
94+
XCTAssertTrue(result.isFailure)
95+
XCTAssertEqual(result.failure as? NetworkError, error)
96+
}
97+
98+
func test_activateJetpackPlugin_correctly_returns_parsed_plugin() throws {
99+
// Given
100+
let remote = JetpackConnectionRemote(siteURL: siteURL, network: network)
101+
let urlSuffix = "/wp/v2/plugins/jetpack/jetpack"
102+
network.simulateResponse(requestUrlSuffix: urlSuffix, filename: "site-plugin-without-envelope")
103+
104+
// When
105+
let result: Result<SitePlugin, Error> = waitFor { promise in
106+
remote.activateJetpackPlugin { result in
107+
promise(result)
108+
}
109+
}
110+
111+
// Then
112+
XCTAssertTrue(result.isSuccess)
113+
let plugin = try XCTUnwrap(result.get())
114+
assertEqual(plugin.plugin, "jetpack/jetpack")
115+
assertEqual(plugin.status, .active)
116+
assertEqual(plugin.name, "Jetpack")
117+
}
118+
119+
func test_activateJetpackPlugin_properly_relays_errors() {
120+
// Given
121+
let remote = JetpackConnectionRemote(siteURL: siteURL, network: network)
122+
let urlSuffix = "/wp/v2/plugins/jetpack/jetpack"
123+
let error = NetworkError.unacceptableStatusCode(statusCode: 500)
124+
network.simulateError(requestUrlSuffix: urlSuffix, error: error)
125+
126+
// When
127+
let result: Result<SitePlugin, Error> = waitFor { promise in
128+
remote.activateJetpackPlugin { result in
129+
promise(result)
130+
}
131+
}
132+
133+
// Then
134+
XCTAssertTrue(result.isFailure)
135+
XCTAssertEqual(result.failure as? NetworkError, error)
136+
}
137+
16138
func test_fetchJetpackConnectionURL_correctly_returns_parsed_url() throws {
17139
// Given
18-
let siteURL = "http://test.com"
19140
let remote = JetpackConnectionRemote(siteURL: siteURL, network: network)
20141
let urlSuffix = "/jetpack/v4/connection/url"
21142
network.simulateResponse(requestUrlSuffix: urlSuffix, filename: "jetpack-connection-url")
@@ -36,7 +157,6 @@ final class JetpackConnectionRemoteTests: XCTestCase {
36157

37158
func test_fetchJetpackConnectionURL_properly_relays_errors() {
38159
// Given
39-
let siteURL = "http://test.com"
40160
let remote = JetpackConnectionRemote(siteURL: siteURL, network: network)
41161
let urlSuffix = "/jetpack/v4/connection/url"
42162
let error = NetworkError.unacceptableStatusCode(statusCode: 500)
@@ -56,7 +176,6 @@ final class JetpackConnectionRemoteTests: XCTestCase {
56176

57177
func test_fetchJetpackUser_correctly_returns_parsed_user() throws {
58178
// Given
59-
let siteURL = "http://test.com"
60179
let remote = JetpackConnectionRemote(siteURL: siteURL, network: network)
61180
let urlSuffix = "/jetpack/v4/connection/data"
62181
network.simulateResponse(requestUrlSuffix: urlSuffix, filename: "jetpack-connected-user")
@@ -77,7 +196,6 @@ final class JetpackConnectionRemoteTests: XCTestCase {
77196

78197
func test_fetchJetpackUser_properly_relays_errors() {
79198
// Given
80-
let siteURL = "http://test.com"
81199
let remote = JetpackConnectionRemote(siteURL: siteURL, network: network)
82200
let urlSuffix = "/jetpack/v4/connection/data"
83201
let error = NetworkError.unacceptableStatusCode(statusCode: 500)
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"plugin": "jetpack/jetpack",
3+
"status": "active",
4+
"name": "Jetpack",
5+
"plugin_uri": "https://jetpack.com",
6+
"author": "Automattic",
7+
"author_uri": "https://jetpack.com",
8+
"description": {
9+
"raw": "Security, performance, and marketing tools made by WordPress experts. Jetpack keeps your site protected so you can focus on more important things.",
10+
"rendered": "Security, performance, and marketing tools made by WordPress experts. Jetpack keeps your site protected so you can focus on more important things. <cite>By <a href=\"https://jetpack.com\">Automattic</a>.</cite>"
11+
},
12+
"version": "11.5.1",
13+
"network_only": false,
14+
"requires_wp": "6.0",
15+
"requires_php": "5.6",
16+
"textdomain": "jetpack",
17+
"_links": {
18+
"self": [
19+
{
20+
"href": "https://test.com/wp-json/wp/v2/plugins/jetpack/jetpack"
21+
}
22+
]
23+
}
24+
}

0 commit comments

Comments
 (0)