Skip to content

Commit d2de796

Browse files
authored
feat: add verifyPassword to ParseUser (#333)
* WIP: add verifyPassword * test tweaks * feat: add verifyPassword to ParseUser * nits
1 parent a9dd890 commit d2de796

File tree

10 files changed

+424
-3
lines changed

10 files changed

+424
-3
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/3.1.2...4.0.0)
1010

1111
__New features__
12+
- 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).
1213
- (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).
1314
- (Breaking Change) Add the ability to merge updated ParseObject's with original objects when using the
1415
.mergeable property. To do this, developers need to add an implementation of merge() to
@@ -30,6 +31,9 @@ __Improvements__
3031
- (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).
3132
- (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).
3233

34+
__Fixes__
35+
- 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).
36+
3337
### 3.1.2
3438
[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/3.1.1...3.1.2)
3539

ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,5 +67,30 @@ User.signup(username: "hello", password: "world") { results in
6767
}
6868
}
6969

70+
//: You can verify the password of the user.
71+
//: Note that usingPost should be set to **true** on newer servers.
72+
User.verifyPassword(password: "world", usingPost: false) { results in
73+
74+
switch results {
75+
case .success(let user):
76+
print(user)
77+
78+
case .failure(let error):
79+
print("Error verifying password \(error)")
80+
}
81+
}
82+
83+
//: Check a bad password
84+
User.verifyPassword(password: "bad", usingPost: false) { results in
85+
86+
switch results {
87+
case .success(let user):
88+
print(user)
89+
90+
case .failure(let error):
91+
print("Error verifying password \(error)")
92+
}
93+
}
94+
7095
PlaygroundPage.current.finishExecution()
7196
//: [Next](@next)

Sources/ParseSwift/API/API.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ public struct API {
3737
case logout
3838
case file(fileName: String)
3939
case passwordReset
40+
case verifyPassword
4041
case verificationEmail
4142
case functions(name: String)
4243
case jobs(name: String)
@@ -81,6 +82,8 @@ public struct API {
8182
return "/files/\(fileName)"
8283
case .passwordReset:
8384
return "/requestPasswordReset"
85+
case .verifyPassword:
86+
return "/verifyPassword"
8487
case .verificationEmail:
8588
return "/verificationEmailRequest"
8689
case .functions(name: let name):

Sources/ParseSwift/Extensions/URLSession.swift

100755100644
File mode changed.

Sources/ParseSwift/Objects/ParseUser+async.swift

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,24 @@ public extension ParseUser {
120120
}
121121
}
122122

123+
/**
124+
Verifies *asynchronously* whether the specified password associated with the user account is valid.
125+
- parameter password: The password to be verified.
126+
- parameter usingPost: Set to **true** to use **POST** for sending. Will use **GET**
127+
otherwise. Defaults to **true**.
128+
- parameter options: A set of header options sent to the server. Defaults to an empty set.
129+
- throws: An error of type `ParseError`.
130+
*/
131+
static func verifyPassword(password: String,
132+
usingPost: Bool = true,
133+
options: API.Options = []) async throws -> Self {
134+
try await withCheckedThrowingContinuation { continuation in
135+
Self.verifyPassword(password: password,
136+
usingPost: usingPost,
137+
options: options, completion: continuation.resume)
138+
}
139+
}
140+
123141
/**
124142
Requests *asynchronously* a verification email be sent to the specified email address
125143
associated with the user account.

Sources/ParseSwift/Objects/ParseUser+combine.swift

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,26 @@ public extension ParseUser {
117117
}
118118
}
119119

120+
/**
121+
Verifies *asynchronously* whether the specified password associated with the user account is valid.
122+
Publishes when complete.
123+
- parameter password: The password to be verified.
124+
- parameter usingPost: Set to **true** to use **POST** for sending. Will use **GET**
125+
otherwise. Defaults to **true**.
126+
- parameter options: A set of header options sent to the server. Defaults to an empty set.
127+
- returns: A publisher that eventually produces a single value and then finishes or fails.
128+
*/
129+
static func verifyPasswordPublisher(password: String,
130+
usingPost: Bool = true,
131+
options: API.Options = []) -> Future<Self, ParseError> {
132+
Future { promise in
133+
Self.verifyPassword(password: password,
134+
usingPost: usingPost,
135+
options: options,
136+
completion: promise)
137+
}
138+
}
139+
120140
/**
121141
Requests *asynchronously* a verification email be sent to the specified email address
122142
associated with the user account. Publishes when complete.

Sources/ParseSwift/Objects/ParseUser.swift

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -323,7 +323,7 @@ extension ParseUser {
323323
internal func meCommand(sessionToken: String) throws -> API.Command<Self, Self> {
324324

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

329329
if let current = Self.current {
@@ -471,6 +471,84 @@ extension ParseUser {
471471
}
472472
}
473473

474+
// MARK: Verify Password
475+
extension ParseUser {
476+
477+
/**
478+
Verifies *asynchronously* whether the specified password associated with the user account is valid.
479+
- parameter password: The password to be verified.
480+
- parameter usingPost: Set to **true** to use **POST** for sending. Will use **GET**
481+
otherwise. Defaults to **true**.
482+
- parameter options: A set of header options sent to the server. Defaults to an empty set.
483+
- parameter callbackQueue: The queue to return to after completion. Default value of .main.
484+
- parameter completion: A block that will be called when the verification request completes or fails.
485+
- note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer
486+
desires a different policy, it should be inserted in `options`.
487+
- warning: `usePost == true`requires Parse Server > 5.0.0. Othewise you should set
488+
`userPost = false`.
489+
*/
490+
public static func verifyPassword(password: String,
491+
usingPost: Bool = true,
492+
options: API.Options = [],
493+
callbackQueue: DispatchQueue = .main,
494+
completion: @escaping (Result<Self, ParseError>) -> Void) {
495+
var options = options
496+
options.insert(.cachePolicy(.reloadIgnoringLocalCacheData))
497+
let username: String!
498+
if let current = BaseParseUser.current,
499+
let currentUsername = current.username {
500+
username = currentUsername
501+
} else {
502+
username = ""
503+
}
504+
var method: API.Method = .POST
505+
if !usingPost {
506+
method = .GET
507+
}
508+
verifyPasswordCommand(username: username,
509+
password: password,
510+
method: method)
511+
.executeAsync(options: options,
512+
callbackQueue: callbackQueue,
513+
completion: completion)
514+
}
515+
516+
internal static func verifyPasswordCommand(username: String,
517+
password: String,
518+
method: API.Method) -> API.Command<SignupLoginBody, Self> {
519+
let loginBody: SignupLoginBody?
520+
let params: [String: String]?
521+
522+
switch method {
523+
case .GET:
524+
loginBody = nil
525+
params = ["username": username, "password": password ]
526+
default:
527+
loginBody = SignupLoginBody(username: username, password: password)
528+
params = nil
529+
}
530+
531+
return API.Command(method: method,
532+
path: .verifyPassword,
533+
params: params,
534+
body: loginBody) { (data) -> Self in
535+
var sessionToken = ""
536+
if let currentSessionToken = BaseParseUser.current?.sessionToken {
537+
sessionToken = currentSessionToken
538+
}
539+
if let decodedSessionToken = try? ParseCoding.jsonDecoder()
540+
.decode(LoginSignupResponse.self, from: data).sessionToken {
541+
sessionToken = decodedSessionToken
542+
}
543+
let user = try ParseCoding.jsonDecoder().decode(Self.self, from: data)
544+
Self.currentContainer = .init(currentUser: user,
545+
sessionToken: sessionToken)
546+
Self.saveCurrentContainerToKeychain()
547+
return user
548+
}
549+
}
550+
}
551+
474552
// MARK: Verification Email Request
475553
extension ParseUser {
476554

Tests/ParseSwiftTests/ParseUserAsyncTests.swift

Lines changed: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ class ParseUserAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng
6767

6868
var objectId: String?
6969
var createdAt: Date?
70-
var sessionToken: String
70+
var sessionToken: String?
7171
var updatedAt: Date?
7272
var ACL: ParseACL?
7373
var originalData: Data?
@@ -320,7 +320,11 @@ class ParseUserAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng
320320
}
321321
}
322322

323-
let signedUp = try await user.become(sessionToken: serverResponse.sessionToken)
323+
guard let sessionToken = serverResponse.sessionToken else {
324+
XCTFail("Should have unwrapped")
325+
return
326+
}
327+
let signedUp = try await user.become(sessionToken: sessionToken)
324328
XCTAssertNotNil(signedUp)
325329
XCTAssertNotNil(signedUp.updatedAt)
326330
XCTAssertNotNil(signedUp.email)
@@ -487,6 +491,163 @@ class ParseUserAsyncTests: XCTestCase { // swiftlint:disable:this type_body_leng
487491
}
488492
}
489493

494+
@MainActor
495+
func testVerifyPasswordLoggedIn() async throws {
496+
login()
497+
MockURLProtocol.removeAll()
498+
XCTAssertNotNil(User.current?.objectId)
499+
500+
var serverResponse = LoginSignupResponse()
501+
serverResponse.sessionToken = nil
502+
503+
MockURLProtocol.mockRequests { _ in
504+
do {
505+
let encoded = try ParseCoding.jsonEncoder().encode(serverResponse)
506+
return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
507+
} catch {
508+
return nil
509+
}
510+
}
511+
512+
let currentUser = try await User.verifyPassword(password: "world", usingPost: true)
513+
XCTAssertNotNil(currentUser)
514+
XCTAssertNotNil(currentUser.createdAt)
515+
XCTAssertNotNil(currentUser.updatedAt)
516+
XCTAssertNotNil(currentUser.email)
517+
XCTAssertNotNil(currentUser.username)
518+
XCTAssertNil(currentUser.password)
519+
XCTAssertNotNil(currentUser.objectId)
520+
XCTAssertNotNil(currentUser.sessionToken)
521+
XCTAssertNotNil(currentUser.customKey)
522+
XCTAssertNil(currentUser.ACL)
523+
524+
guard let userFromKeychain = BaseParseUser.current else {
525+
XCTFail("Couldn't get CurrentUser from Keychain")
526+
return
527+
}
528+
529+
XCTAssertNotNil(userFromKeychain.createdAt)
530+
XCTAssertNotNil(userFromKeychain.updatedAt)
531+
XCTAssertNotNil(userFromKeychain.email)
532+
XCTAssertNotNil(userFromKeychain.username)
533+
XCTAssertNil(userFromKeychain.password)
534+
XCTAssertNotNil(userFromKeychain.objectId)
535+
XCTAssertNotNil(userFromKeychain.sessionToken)
536+
XCTAssertNil(userFromKeychain.ACL)
537+
}
538+
539+
func testVerifyPasswordLoggedInGET() async throws {
540+
login()
541+
MockURLProtocol.removeAll()
542+
XCTAssertNotNil(User.current?.objectId)
543+
544+
var serverResponse = LoginSignupResponse()
545+
serverResponse.sessionToken = nil
546+
547+
MockURLProtocol.mockRequests { _ in
548+
do {
549+
let encoded = try ParseCoding.jsonEncoder().encode(serverResponse)
550+
return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
551+
} catch {
552+
return nil
553+
}
554+
}
555+
556+
let currentUser = try await User.verifyPassword(password: "world", usingPost: false)
557+
XCTAssertNotNil(currentUser)
558+
XCTAssertNotNil(currentUser.createdAt)
559+
XCTAssertNotNil(currentUser.updatedAt)
560+
XCTAssertNotNil(currentUser.email)
561+
XCTAssertNotNil(currentUser.username)
562+
XCTAssertNil(currentUser.password)
563+
XCTAssertNotNil(currentUser.objectId)
564+
XCTAssertNotNil(currentUser.sessionToken)
565+
XCTAssertNotNil(currentUser.customKey)
566+
XCTAssertNil(currentUser.ACL)
567+
568+
guard let userFromKeychain = BaseParseUser.current else {
569+
XCTFail("Couldn't get CurrentUser from Keychain")
570+
return
571+
}
572+
573+
XCTAssertNotNil(userFromKeychain.createdAt)
574+
XCTAssertNotNil(userFromKeychain.updatedAt)
575+
XCTAssertNotNil(userFromKeychain.email)
576+
XCTAssertNotNil(userFromKeychain.username)
577+
XCTAssertNil(userFromKeychain.password)
578+
XCTAssertNotNil(userFromKeychain.objectId)
579+
XCTAssertNotNil(userFromKeychain.sessionToken)
580+
XCTAssertNil(userFromKeychain.ACL)
581+
}
582+
583+
@MainActor
584+
func testVerifyPasswordNotLoggedIn() async throws {
585+
let serverResponse = LoginSignupResponse()
586+
587+
MockURLProtocol.mockRequests { _ in
588+
do {
589+
let encoded = try ParseCoding.jsonEncoder().encode(serverResponse)
590+
return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
591+
} catch {
592+
return nil
593+
}
594+
}
595+
596+
let currentUser = try await User.verifyPassword(password: "world")
597+
XCTAssertNotNil(currentUser)
598+
XCTAssertNotNil(currentUser.createdAt)
599+
XCTAssertNotNil(currentUser.updatedAt)
600+
XCTAssertNotNil(currentUser.email)
601+
XCTAssertNotNil(currentUser.username)
602+
XCTAssertNil(currentUser.password)
603+
XCTAssertNotNil(currentUser.objectId)
604+
XCTAssertNotNil(currentUser.sessionToken)
605+
XCTAssertNotNil(currentUser.customKey)
606+
XCTAssertNil(currentUser.ACL)
607+
608+
guard let userFromKeychain = BaseParseUser.current else {
609+
XCTFail("Couldn't get CurrentUser from Keychain")
610+
return
611+
}
612+
613+
XCTAssertNotNil(userFromKeychain.createdAt)
614+
XCTAssertNotNil(userFromKeychain.updatedAt)
615+
XCTAssertNotNil(userFromKeychain.email)
616+
XCTAssertNotNil(userFromKeychain.username)
617+
XCTAssertNil(userFromKeychain.password)
618+
XCTAssertNotNil(userFromKeychain.objectId)
619+
XCTAssertNotNil(userFromKeychain.sessionToken)
620+
XCTAssertNil(userFromKeychain.ACL)
621+
}
622+
623+
@MainActor
624+
func testVerifyPasswordLoggedInError() async throws {
625+
login()
626+
MockURLProtocol.removeAll()
627+
XCTAssertNotNil(User.current?.objectId)
628+
629+
let parseError = ParseError(code: .userWithEmailNotFound,
630+
message: "User email is not verified.")
631+
632+
MockURLProtocol.mockRequests { _ in
633+
do {
634+
let encoded = try ParseCoding.jsonEncoder().encode(parseError)
635+
return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0)
636+
} catch {
637+
return nil
638+
}
639+
}
640+
do {
641+
_ = try await User.verifyPassword(password: "blue")
642+
} catch {
643+
guard let error = error as? ParseError else {
644+
XCTFail("Should be ParseError")
645+
return
646+
}
647+
XCTAssertEqual(error.code, parseError.code)
648+
}
649+
}
650+
490651
@MainActor
491652
func testVerificationEmail() async throws {
492653
let serverResponse = NoBody()

0 commit comments

Comments
 (0)