Skip to content

Commit

Permalink
feat: add verifyPassword to ParseUser (#333)
Browse files Browse the repository at this point in the history
* WIP: add verifyPassword

* test tweaks

* feat: add verifyPassword to ParseUser

* nits
  • Loading branch information
cbaker6 authored Jan 25, 2022
1 parent a9dd890 commit d2de796
Show file tree
Hide file tree
Showing 10 changed files with 424 additions and 3 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/3.1.2...4.0.0)

__New features__
- Add the verifyPassword to ParseUser. This method defaults to using POST though POST is not available on the current Parse Server. Change userPost == false to use GET on older Parse Servers ([#333](https://github.com/parse-community/Parse-Swift/pull/333)), thanks to [Corey Baker](https://github.com/cbaker6).
- (Breaking Change) Bump the SPM toolchain from 5.1 to 5.5. This is done to take advantage of features in the latest toolchain. For developers using < Xcode 13 and depending on the Swift SDK through SPM, this will cause a break. You can either upgrade your Xcode or use Cocoapods or Carthage to depend on ParseSwift ([#326](https://github.com/parse-community/Parse-Swift/pull/326)), thanks to [Corey Baker](https://github.com/cbaker6).
- (Breaking Change) Add the ability to merge updated ParseObject's with original objects when using the
.mergeable property. To do this, developers need to add an implementation of merge() to
Expand All @@ -30,6 +31,9 @@ __Improvements__
- (Breaking Change) Change the following method parameter names: isUsingMongoDB -> usingMongoDB, isIgnoreCustomObjectIdConfig -> ignoringCustomObjectIdConfig, isUsingEQ -> usingEqComparator ([#321](https://github.com/parse-community/Parse-Swift/pull/321)), thanks to [Corey Baker](https://github.com/cbaker6).
- (Breaking Change) Change the following method parameter names: isUsingTransactions -> usingTransactions, isAllowingCustomObjectIds -> allowingCustomObjectIds, isUsingEqualQueryConstraint -> usingEqualQueryConstraint, isMigratingFromObjcSDK -> migratingFromObjcSDK, isDeletingKeychainIfNeeded -> deletingKeychainIfNeeded ([#323](https://github.com/parse-community/Parse-Swift/pull/323)), thanks to [Corey Baker](https://github.com/cbaker6).

__Fixes__
- Always check for ParseError first when decoding responses from the server. Before this fix, this could cause issues depending on how calls are made from the Swift SDK ([#332](https://github.com/parse-community/Parse-Swift/pull/332)), thanks to [Corey Baker](https://github.com/cbaker6).

### 3.1.2
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/3.1.1...3.1.2)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,30 @@ User.signup(username: "hello", password: "world") { results in
}
}

//: You can verify the password of the user.
//: Note that usingPost should be set to **true** on newer servers.
User.verifyPassword(password: "world", usingPost: false) { results in

switch results {
case .success(let user):
print(user)

case .failure(let error):
print("Error verifying password \(error)")
}
}

//: Check a bad password
User.verifyPassword(password: "bad", usingPost: false) { results in

switch results {
case .success(let user):
print(user)

case .failure(let error):
print("Error verifying password \(error)")
}
}

PlaygroundPage.current.finishExecution()
//: [Next](@next)
3 changes: 3 additions & 0 deletions Sources/ParseSwift/API/API.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public struct API {
case logout
case file(fileName: String)
case passwordReset
case verifyPassword
case verificationEmail
case functions(name: String)
case jobs(name: String)
Expand Down Expand Up @@ -81,6 +82,8 @@ public struct API {
return "/files/\(fileName)"
case .passwordReset:
return "/requestPasswordReset"
case .verifyPassword:
return "/verifyPassword"
case .verificationEmail:
return "/verificationEmailRequest"
case .functions(name: let name):
Expand Down
Empty file modified Sources/ParseSwift/Extensions/URLSession.swift
100755 → 100644
Empty file.
18 changes: 18 additions & 0 deletions Sources/ParseSwift/Objects/ParseUser+async.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,24 @@ public extension ParseUser {
}
}

/**
Verifies *asynchronously* whether the specified password associated with the user account is valid.
- parameter password: The password to be verified.
- parameter usingPost: Set to **true** to use **POST** for sending. Will use **GET**
otherwise. Defaults to **true**.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- throws: An error of type `ParseError`.
*/
static func verifyPassword(password: String,
usingPost: Bool = true,
options: API.Options = []) async throws -> Self {
try await withCheckedThrowingContinuation { continuation in
Self.verifyPassword(password: password,
usingPost: usingPost,
options: options, completion: continuation.resume)
}
}

/**
Requests *asynchronously* a verification email be sent to the specified email address
associated with the user account.
Expand Down
20 changes: 20 additions & 0 deletions Sources/ParseSwift/Objects/ParseUser+combine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,26 @@ public extension ParseUser {
}
}

/**
Verifies *asynchronously* whether the specified password associated with the user account is valid.
Publishes when complete.
- parameter password: The password to be verified.
- parameter usingPost: Set to **true** to use **POST** for sending. Will use **GET**
otherwise. Defaults to **true**.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- returns: A publisher that eventually produces a single value and then finishes or fails.
*/
static func verifyPasswordPublisher(password: String,
usingPost: Bool = true,
options: API.Options = []) -> Future<Self, ParseError> {
Future { promise in
Self.verifyPassword(password: password,
usingPost: usingPost,
options: options,
completion: promise)
}
}

/**
Requests *asynchronously* a verification email be sent to the specified email address
associated with the user account. Publishes when complete.
Expand Down
80 changes: 79 additions & 1 deletion Sources/ParseSwift/Objects/ParseUser.swift
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ extension ParseUser {
internal func meCommand(sessionToken: String) throws -> API.Command<Self, Self> {

return API.Command(method: .GET,
path: endpoint) { (data) -> Self in
path: endpoint) { (data) -> Self in
let user = try ParseCoding.jsonDecoder().decode(Self.self, from: data)

if let current = Self.current {
Expand Down Expand Up @@ -471,6 +471,84 @@ extension ParseUser {
}
}

// MARK: Verify Password
extension ParseUser {

/**
Verifies *asynchronously* whether the specified password associated with the user account is valid.
- parameter password: The password to be verified.
- parameter usingPost: Set to **true** to use **POST** for sending. Will use **GET**
otherwise. Defaults to **true**.
- parameter options: A set of header options sent to the server. Defaults to an empty set.
- parameter callbackQueue: The queue to return to after completion. Default value of .main.
- parameter completion: A block that will be called when the verification request completes or fails.
- note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
desires a different policy, it should be inserted in `options`.
- warning: `usePost == true`requires Parse Server > 5.0.0. Othewise you should set
`userPost = false`.
*/
public static func verifyPassword(password: String,
usingPost: Bool = true,
options: API.Options = [],
callbackQueue: DispatchQueue = .main,
completion: @escaping (Result<Self, ParseError>) -> Void) {
var options = options
options.insert(.cachePolicy(.reloadIgnoringLocalCacheData))
let username: String!
if let current = BaseParseUser.current,
let currentUsername = current.username {
username = currentUsername
} else {
username = ""
}
var method: API.Method = .POST
if !usingPost {
method = .GET
}
verifyPasswordCommand(username: username,
password: password,
method: method)
.executeAsync(options: options,
callbackQueue: callbackQueue,
completion: completion)
}

internal static func verifyPasswordCommand(username: String,
password: String,
method: API.Method) -> API.Command<SignupLoginBody, Self> {
let loginBody: SignupLoginBody?
let params: [String: String]?

switch method {
case .GET:
loginBody = nil
params = ["username": username, "password": password ]
default:
loginBody = SignupLoginBody(username: username, password: password)
params = nil
}

return API.Command(method: method,
path: .verifyPassword,
params: params,
body: loginBody) { (data) -> Self in
var sessionToken = ""
if let currentSessionToken = BaseParseUser.current?.sessionToken {
sessionToken = currentSessionToken
}
if let decodedSessionToken = try? ParseCoding.jsonDecoder()
.decode(LoginSignupResponse.self, from: data).sessionToken {
sessionToken = decodedSessionToken
}
let user = try ParseCoding.jsonDecoder().decode(Self.self, from: data)
Self.currentContainer = .init(currentUser: user,
sessionToken: sessionToken)
Self.saveCurrentContainerToKeychain()
return user
}
}
}

// MARK: Verification Email Request
extension ParseUser {

Expand Down
165 changes: 163 additions & 2 deletions Tests/ParseSwiftTests/ParseUserAsyncTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ class ParseUserAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng

var objectId: String?
var createdAt: Date?
var sessionToken: String
var sessionToken: String?
var updatedAt: Date?
var ACL: ParseACL?
var originalData: Data?
Expand Down Expand Up @@ -320,7 +320,11 @@ class ParseUserAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng
}
}

let signedUp = try await user.become(sessionToken: serverResponse.sessionToken)
guard let sessionToken = serverResponse.sessionToken else {
XCTFail("Should have unwrapped")
return
}
let signedUp = try await user.become(sessionToken: sessionToken)
XCTAssertNotNil(signedUp)
XCTAssertNotNil(signedUp.updatedAt)
XCTAssertNotNil(signedUp.email)
Expand Down Expand Up @@ -487,6 +491,163 @@ class ParseUserAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng
}
}

@MainActor
func testVerifyPasswordLoggedIn() async throws {
login()
MockURLProtocol.removeAll()
XCTAssertNotNil(User.current?.objectId)

var serverResponse = LoginSignupResponse()
serverResponse.sessionToken = nil

MockURLProtocol.mockRequests { _ in
do {
let encoded = try ParseCoding.jsonEncoder().encode(serverResponse)
return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
} catch {
return nil
}
}

let currentUser = try await User.verifyPassword(password: "world", usingPost: true)
XCTAssertNotNil(currentUser)
XCTAssertNotNil(currentUser.createdAt)
XCTAssertNotNil(currentUser.updatedAt)
XCTAssertNotNil(currentUser.email)
XCTAssertNotNil(currentUser.username)
XCTAssertNil(currentUser.password)
XCTAssertNotNil(currentUser.objectId)
XCTAssertNotNil(currentUser.sessionToken)
XCTAssertNotNil(currentUser.customKey)
XCTAssertNil(currentUser.ACL)

guard let userFromKeychain = BaseParseUser.current else {
XCTFail("Couldn't get CurrentUser from Keychain")
return
}

XCTAssertNotNil(userFromKeychain.createdAt)
XCTAssertNotNil(userFromKeychain.updatedAt)
XCTAssertNotNil(userFromKeychain.email)
XCTAssertNotNil(userFromKeychain.username)
XCTAssertNil(userFromKeychain.password)
XCTAssertNotNil(userFromKeychain.objectId)
XCTAssertNotNil(userFromKeychain.sessionToken)
XCTAssertNil(userFromKeychain.ACL)
}

func testVerifyPasswordLoggedInGET() async throws {
login()
MockURLProtocol.removeAll()
XCTAssertNotNil(User.current?.objectId)

var serverResponse = LoginSignupResponse()
serverResponse.sessionToken = nil

MockURLProtocol.mockRequests { _ in
do {
let encoded = try ParseCoding.jsonEncoder().encode(serverResponse)
return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
} catch {
return nil
}
}

let currentUser = try await User.verifyPassword(password: "world", usingPost: false)
XCTAssertNotNil(currentUser)
XCTAssertNotNil(currentUser.createdAt)
XCTAssertNotNil(currentUser.updatedAt)
XCTAssertNotNil(currentUser.email)
XCTAssertNotNil(currentUser.username)
XCTAssertNil(currentUser.password)
XCTAssertNotNil(currentUser.objectId)
XCTAssertNotNil(currentUser.sessionToken)
XCTAssertNotNil(currentUser.customKey)
XCTAssertNil(currentUser.ACL)

guard let userFromKeychain = BaseParseUser.current else {
XCTFail("Couldn't get CurrentUser from Keychain")
return
}

XCTAssertNotNil(userFromKeychain.createdAt)
XCTAssertNotNil(userFromKeychain.updatedAt)
XCTAssertNotNil(userFromKeychain.email)
XCTAssertNotNil(userFromKeychain.username)
XCTAssertNil(userFromKeychain.password)
XCTAssertNotNil(userFromKeychain.objectId)
XCTAssertNotNil(userFromKeychain.sessionToken)
XCTAssertNil(userFromKeychain.ACL)
}

@MainActor
func testVerifyPasswordNotLoggedIn() async throws {
let serverResponse = LoginSignupResponse()

MockURLProtocol.mockRequests { _ in
do {
let encoded = try ParseCoding.jsonEncoder().encode(serverResponse)
return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
} catch {
return nil
}
}

let currentUser = try await User.verifyPassword(password: "world")
XCTAssertNotNil(currentUser)
XCTAssertNotNil(currentUser.createdAt)
XCTAssertNotNil(currentUser.updatedAt)
XCTAssertNotNil(currentUser.email)
XCTAssertNotNil(currentUser.username)
XCTAssertNil(currentUser.password)
XCTAssertNotNil(currentUser.objectId)
XCTAssertNotNil(currentUser.sessionToken)
XCTAssertNotNil(currentUser.customKey)
XCTAssertNil(currentUser.ACL)

guard let userFromKeychain = BaseParseUser.current else {
XCTFail("Couldn't get CurrentUser from Keychain")
return
}

XCTAssertNotNil(userFromKeychain.createdAt)
XCTAssertNotNil(userFromKeychain.updatedAt)
XCTAssertNotNil(userFromKeychain.email)
XCTAssertNotNil(userFromKeychain.username)
XCTAssertNil(userFromKeychain.password)
XCTAssertNotNil(userFromKeychain.objectId)
XCTAssertNotNil(userFromKeychain.sessionToken)
XCTAssertNil(userFromKeychain.ACL)
}

@MainActor
func testVerifyPasswordLoggedInError() async throws {
login()
MockURLProtocol.removeAll()
XCTAssertNotNil(User.current?.objectId)

let parseError = ParseError(code: .userWithEmailNotFound,
message: "User email is not verified.")

MockURLProtocol.mockRequests { _ in
do {
let encoded = try ParseCoding.jsonEncoder().encode(parseError)
return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
} catch {
return nil
}
}
do {
_ = try await User.verifyPassword(password: "blue")
} catch {
guard let error = error as? ParseError else {
XCTFail("Should be ParseError")
return
}
XCTAssertEqual(error.code, parseError.code)
}
}

@MainActor
func testVerificationEmail() async throws {
let serverResponse = NoBody()
Expand Down
Loading

0 comments on commit d2de796

Please sign in to comment.