Skip to content
This repository was archived by the owner on Sep 15, 2025. It is now read-only.

Commit 5c14750

Browse files
author
Emily Laguna
authored
Merge pull request #513 from wordpress-mobile/feature/qrcode-login-networking
Add QR Login Remote Service
2 parents d1dff5b + adcf95f commit 5c14750

File tree

10 files changed

+313
-36
lines changed

10 files changed

+313
-36
lines changed

WordPressKit.podspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
Pod::Spec.new do |s|
44
s.name = 'WordPressKit'
5-
s.version = '4.54.0'
5+
s.version = '4.55.0-beta.1'
66

77
s.summary = 'WordPressKit offers a clean and simple WordPress.com and WordPress.org API.'
88
s.description = <<-DESC

WordPressKit.xcodeproj/project.pbxproj

Lines changed: 83 additions & 35 deletions
Large diffs are not rendered by default.
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
import Foundation
2+
import WordPressShared
3+
4+
public class QRLoginServiceRemote: ServiceRemoteWordPressComREST {
5+
/// Validates the incoming QR Login token and retrieves the requesting browser, and location
6+
public func validate(token: String, data: String, success: @escaping (QRLoginValidationResponse) -> Void, failure: @escaping (Error?, QRLoginError?) -> Void) {
7+
let path = self.path(forEndpoint: "auth/qr-code/validate", withVersion: ._2_0)
8+
let parameters = [ "token": token, "data": data ] as [String: AnyObject]
9+
10+
wordPressComRestApi.POST(path, parameters: parameters as [String: AnyObject], success: { (response, _) in
11+
do {
12+
let decoder = JSONDecoder.apiDecoder
13+
let data = try JSONSerialization.data(withJSONObject: response, options: [])
14+
let envelope = try decoder.decode(QRLoginValidationResponse.self, from: data)
15+
16+
success(envelope)
17+
} catch {
18+
failure(nil, .invalidData)
19+
}
20+
}, failure: { (error, response) in
21+
guard let response = response else {
22+
failure(error, .invalidData)
23+
return
24+
}
25+
26+
let statusCode = response.statusCode
27+
failure(error, QRLoginError(statusCode: statusCode))
28+
})
29+
}
30+
31+
/// Authenticates the users browser
32+
public func authenticate(token: String, data: String, success: @escaping(Bool) -> Void, failure: @escaping(Error) -> Void) {
33+
let path = self.path(forEndpoint: "auth/qr-code/authenticate", withVersion: ._2_0)
34+
let parameters = [ "token": token, "data": data ] as [String: AnyObject]
35+
36+
wordPressComRestApi.POST(path, parameters: parameters, success: { (response, _) in
37+
guard let authenticated = response["authenticated"] as? Bool else {
38+
success(false)
39+
return
40+
}
41+
42+
success(authenticated)
43+
}, failure: { (error, _) in
44+
failure(error)
45+
})
46+
}
47+
}
48+
49+
public enum QRLoginError {
50+
case invalidData
51+
case expired
52+
53+
init(statusCode: Int) {
54+
switch statusCode {
55+
case 401:
56+
self = .expired
57+
58+
default:
59+
self = .invalidData
60+
}
61+
}
62+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import Foundation
2+
3+
public struct QRLoginValidationResponse: Decodable {
4+
/// The name of the browser that the user has requested the login from
5+
/// IE: Chrome, Firefox
6+
/// This may be null if the browser could not be determined
7+
public var browser: String?
8+
9+
10+
/// The City, State the user has requested the login from
11+
/// IE: Columbus, Ohio
12+
public var location: String
13+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"authenticated": true,
3+
"success": true
4+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"code": "rest_invalid_param",
3+
"message": "Token does not match",
4+
"data": {
5+
"status": 400
6+
}
7+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"browser": "Chrome",
3+
"location": "Mount Laurel, New Jersey",
4+
"success": true
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"code": "rest_invalid_param",
3+
"message": "Token does not match",
4+
"data": {
5+
"status": 400
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"code": "data_invalid",
3+
"message": "QR code data expired",
4+
"data": {
5+
"status": 401
6+
}
7+
}
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import XCTest
2+
@testable import WordPressKit
3+
4+
class QRLoginServiceRemoteTests: RemoteTestCase, RESTTestable {
5+
let mockRemoteApi = MockWordPressComRestApi()
6+
var qrLoginServiceRemote: QRLoginServiceRemote!
7+
8+
override func setUp() {
9+
qrLoginServiceRemote = QRLoginServiceRemote(wordPressComRestApi: getRestApi())
10+
}
11+
12+
// MARK: - Validate Tests
13+
14+
// Calls the success block with valid data when the request succeeds
15+
//
16+
func testValidResponseObject() {
17+
let expect = expectation(description: "Validate the response object successfully")
18+
stubRemoteResponse("wpcom/v2/auth/qr-code/validate", filename: "qrlogin-validate-200.json", contentType: .ApplicationJSON)
19+
20+
let browser = "Chrome"
21+
let location = "Mount Laurel, New Jersey"
22+
23+
qrLoginServiceRemote.validate(token: "", data: "") { response in
24+
XCTAssertEqual(browser, response.browser)
25+
XCTAssertEqual(location, response.location)
26+
expect.fulfill()
27+
} failure: { _, _ in }
28+
29+
waitForExpectations(timeout: timeout, handler: nil)
30+
}
31+
32+
// Calls the failure block with invalidData when providing invalid token/data
33+
//
34+
func testValidateInvalidData() {
35+
let expect = expectation(description: "Validate the invalid data error is being handled")
36+
stubRemoteResponse("wpcom/v2/auth/qr-code/validate", filename: "qrlogin-validate-400.json", contentType: .ApplicationJSON, status: 400)
37+
38+
qrLoginServiceRemote.validate(token: "invalid_token", data: "invalid_data") { _ in
39+
XCTFail("This request should not succeed")
40+
} failure: { _, qrLoginError in
41+
XCTAssertEqual(qrLoginError!, .invalidData)
42+
expect.fulfill()
43+
}
44+
45+
waitForExpectations(timeout: timeout, handler: nil)
46+
}
47+
48+
// Calls the failure block with expired when providing expired token/data
49+
//
50+
func testValidateExpired() {
51+
let expect = expectation(description: "Validate the expired data error is being handled")
52+
stubRemoteResponse("wpcom/v2/auth/qr-code/validate", filename: "qrlogin-validate-expired-401.json", contentType: .ApplicationJSON, status: 401)
53+
54+
qrLoginServiceRemote.validate(token: "expired_token", data: "expired_data") { _ in
55+
XCTFail("This request should not succeed")
56+
} failure: { _, qrLoginError in
57+
XCTAssertEqual(qrLoginError!, .expired)
58+
expect.fulfill()
59+
}
60+
61+
waitForExpectations(timeout: timeout, handler: nil)
62+
}
63+
64+
// Calls the failure block with invalidData when parsing an invalid response
65+
//
66+
func testInvalidJSON() {
67+
let expect = expectation(description: "Validate the failure object is being returned")
68+
stubRemoteResponse("wpcom/v2/auth/qr-code/validate", data: "foo".data(using: .utf8)!, contentType: .ApplicationJSON)
69+
70+
qrLoginServiceRemote.validate(token: "expired_token", data: "expired_data") { _ in
71+
XCTFail("This request should not succeed")
72+
} failure: { _, qrLoginError in
73+
XCTAssertEqual(qrLoginError!, .invalidData)
74+
expect.fulfill()
75+
}
76+
77+
waitForExpectations(timeout: timeout, handler: nil)
78+
}
79+
80+
// MARK: - Authenticate Tests
81+
82+
// Calls the success block when authenticating valid data
83+
//
84+
func testAuthenticateSuccess() {
85+
let expect = expectation(description: "Successful Authentication")
86+
stubRemoteResponse("wpcom/v2/auth/qr-code/authenticate", filename: "qrlogin-authenticate-200.json", contentType: .ApplicationJSON)
87+
88+
qrLoginServiceRemote.authenticate(token: "valid_token", data: "valid_data") { authenticated in
89+
XCTAssertTrue(authenticated)
90+
expect.fulfill()
91+
} failure: { _ in }
92+
93+
waitForExpectations(timeout: timeout, handler: nil)
94+
}
95+
96+
// Calls the failure block when providing invalid data
97+
//
98+
func testAuthenticateFailure() {
99+
let expect = expectation(description: "Failed Authentication")
100+
stubRemoteResponse("wpcom/v2/auth/qr-code/authenticate", filename: "qrlogin-authenticate-failed-400.json", contentType: .ApplicationJSON, status: 400)
101+
102+
qrLoginServiceRemote.authenticate(token: "valid_token", data: "valid_data") { authenticated in
103+
XCTFail("This request should not succeed")
104+
} failure: { error in
105+
expect.fulfill()
106+
}
107+
108+
waitForExpectations(timeout: timeout, handler: nil)
109+
}
110+
111+
// Calls the failure block when parsing invalid JSON
112+
func testAuthenticateInvalidJSON() {
113+
let expect = expectation(description: "Failed Authentication")
114+
stubRemoteResponse("wpcom/v2/auth/qr-code/authenticate", data: "foo".data(using: .utf8)!, contentType: .ApplicationJSON)
115+
116+
qrLoginServiceRemote.authenticate(token: "valid_token", data: "valid_data") { authenticated in
117+
XCTFail("This request should not succeed")
118+
} failure: { error in
119+
expect.fulfill()
120+
}
121+
122+
waitForExpectations(timeout: timeout, handler: nil)
123+
}
124+
}

0 commit comments

Comments
 (0)