From b4f5497ac77e6ce8d8e23c270878402d28533ac6 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Wed, 22 Sep 2021 06:38:24 -0400 Subject: [PATCH 1/4] Don't encode emailVerified when saving user to server --- Sources/ParseSwift/Objects/ParseUser.swift | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index cd157ee2d..05866f985 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -20,7 +20,7 @@ public protocol ParseUser: ParseObject { Determines if the email is verified for the `ParseUser`. - note: This value can only be changed on the Parse Server. */ - var emailVerified: Bool? { get } + var emailVerified: Bool? { get set } /** The password for the `ParseUser`. @@ -944,12 +944,14 @@ extension ParseUser { } private func updateCommand() -> API.Command { + var mutableSelf = self + mutableSelf.emailVerified = nil let mapper = { (data) -> Self in try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: data).apply(to: self) } return API.Command(method: .PUT, path: endpoint, - body: self, + body: mutableSelf, mapper: mapper) } } From a3019dae448c93c0938a10fc0c0c5eecb7c20ba6 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Wed, 22 Sep 2021 07:32:01 -0400 Subject: [PATCH 2/4] Don't encode email if user didn't change it --- Sources/ParseSwift/Objects/ParseUser.swift | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 05866f985..42ed4f8f0 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -20,7 +20,7 @@ public protocol ParseUser: ParseObject { Determines if the email is verified for the `ParseUser`. - note: This value can only be changed on the Parse Server. */ - var emailVerified: Bool? { get set } + var emailVerified: Bool? { get } /** The password for the `ParseUser`. @@ -945,7 +945,11 @@ extension ParseUser { private func updateCommand() -> API.Command { var mutableSelf = self - mutableSelf.emailVerified = nil + if let currentUser = Self.current, + currentUser.hasSameObjectId(as: mutableSelf) == true, + currentUser.email == mutableSelf.email { + mutableSelf.email = nil + } let mapper = { (data) -> Self in try ParseCoding.jsonDecoder().decode(UpdateResponse.self, from: data).apply(to: self) } From e86925235d36d4b658a4334f5a1cdb5c409309f0 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Wed, 22 Sep 2021 09:24:41 -0400 Subject: [PATCH 3/4] Add testcases --- CHANGELOG.md | 3 + Sources/ParseSwift/Types/CloudViewModel.swift | 2 +- Sources/ParseSwift/Types/QueryViewModel.swift | 2 +- Tests/ParseSwiftTests/ParseUserTests.swift | 87 +++++++++++++++++++ 4 files changed, 92 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 580a63978..358c52e9f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.9.10...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ +__Fixes__ +- ParseUser shouldn't send email if it hasn't been modified or else email verification is resent ([#241](https://github.com/parse-community/Parse-Swift/pull/241)), thanks to [Corey Baker](https://github.com/cbaker6). + ### 1.9.10 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.9.9...1.9.10) __Fixes__ diff --git a/Sources/ParseSwift/Types/CloudViewModel.swift b/Sources/ParseSwift/Types/CloudViewModel.swift index 5d2b97959..d04bb7b0b 100644 --- a/Sources/ParseSwift/Types/CloudViewModel.swift +++ b/Sources/ParseSwift/Types/CloudViewModel.swift @@ -29,7 +29,7 @@ open class CloudViewModel: CloudObservable { } /// Updates and notifies when there is an error retrieving the results. - open var error: ParseError? = nil { + open var error: ParseError? { willSet { if newValue != nil { self.results = nil diff --git a/Sources/ParseSwift/Types/QueryViewModel.swift b/Sources/ParseSwift/Types/QueryViewModel.swift index 381e083e3..a3ba9cd1c 100644 --- a/Sources/ParseSwift/Types/QueryViewModel.swift +++ b/Sources/ParseSwift/Types/QueryViewModel.swift @@ -38,7 +38,7 @@ open class QueryViewModel: QueryObservable { } /// Updates and notifies when there is an error retrieving the results. - open var error: ParseError? = nil { + open var error: ParseError? { willSet { if newValue != nil { results.removeAll() diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index 9f45982d5..062a0eeb0 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -61,6 +61,16 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length self.email = "hello@parse.com" self.emailVerified = false } + + func createUser() -> User { + var user = User() + user.objectId = objectId + user.ACL = ACL + user.customKey = customKey + user.username = username + user.email = email + return user + } } let loginUserName = "hello10" @@ -454,6 +464,83 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNotNil(command.body) } + func userSignUp() throws { + let loginResponse = LoginSignupResponse() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + _ = try loginResponse.createUser().signup() + MockURLProtocol.removeAll() + guard let currentUser = User.current else { + XCTFail("Should have a current user after signup") + return + } + XCTAssertEqual(currentUser.objectId, loginResponse.objectId) + XCTAssertEqual(currentUser.username, loginResponse.username) + XCTAssertEqual(currentUser.email, loginResponse.email) + XCTAssertEqual(currentUser.ACL, loginResponse.ACL) + XCTAssertEqual(currentUser.customKey, loginResponse.customKey) + } + + func testUpdateCommandUnmodifiedEmail() throws { + try userSignUp() + guard let user = User.current, + let objectId = user.objectId else { + XCTFail("Should have current user.") + return + } + XCTAssertNotNil(user.email) + let command = try user.saveCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/users/\(objectId)") + XCTAssertEqual(command.method, API.Method.PUT) + XCTAssertNil(command.params) + XCTAssertNotNil(command.body) + XCTAssertNil(command.body?.email) + } + + func testUpdateCommandModifiedEmail() throws { + try userSignUp() + guard var user = User.current, + let objectId = user.objectId else { + XCTFail("Should have current user.") + return + } + let email = "peace@parse.com" + user.email = email + XCTAssertNotNil(user.email) + let command = try user.saveCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/users/\(objectId)") + XCTAssertEqual(command.method, API.Method.PUT) + XCTAssertNil(command.params) + XCTAssertNotNil(command.body) + XCTAssertEqual(command.body?.email, email) + } + + func testUpdateCommandNotCurrentModifiedEmail() throws { + try userSignUp() + var user = User() + let objectId = "yarr" + user.objectId = objectId + let email = "peace@parse.com" + user.email = email + XCTAssertNotNil(user.email) + let command = try user.saveCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/users/\(objectId)") + XCTAssertEqual(command.method, API.Method.PUT) + XCTAssertNil(command.params) + XCTAssertNotNil(command.body) + XCTAssertEqual(command.body?.email, email) + } + func testSaveAndUpdateCurrentUser() { // swiftlint:disable:this function_body_length XCTAssertNil(User.current?.objectId) testLogin() From 12309144cc4b98165717f311b9a9e679ef7bb604 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Wed, 22 Sep 2021 11:18:27 -0400 Subject: [PATCH 4/4] Improve testcases --- Tests/ParseSwiftTests/ParseUserTests.swift | 174 +++++++++++++++++++-- 1 file changed, 159 insertions(+), 15 deletions(-) diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index 062a0eeb0..db7ba3d0e 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -541,17 +541,82 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertEqual(command.body?.email, email) } - func testSaveAndUpdateCurrentUser() { // swiftlint:disable:this function_body_length + func testSaveAndUpdateCurrentUser() throws { // swiftlint:disable:this function_body_length XCTAssertNil(User.current?.objectId) - testLogin() - MockURLProtocol.removeAll() + try userSignUp() XCTAssertNotNil(User.current?.objectId) guard let user = User.current else { XCTFail("Should unwrap") return } + XCTAssertNotNil(user.email) + var userOnServer = user + userOnServer.createdAt = User.current?.createdAt + userOnServer.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) + + let encoded: Data! + do { + encoded = try userOnServer.getEncoder().encode(userOnServer, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + let saved = try user.save(options: [.useMasterKey]) + XCTAssert(saved.hasSameObjectId(as: userOnServer)) + XCTAssertEqual(saved.email, user.email) + guard let savedCreatedAt = saved.createdAt, + let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = user.createdAt, + let originalUpdatedAt = user.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(saved.ACL) + + //Should be updated in memory + XCTAssertEqual(User.current?.updatedAt, savedUpdatedAt) + XCTAssertEqual(User.current?.email, user.email) + + #if !os(Linux) && !os(Android) + //Should be updated in Keychain + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) else { + XCTFail("Should get object from Keychain") + return + } + XCTAssertEqual(keychainUser.currentUser?.updatedAt, savedUpdatedAt) + XCTAssertEqual(keychainUser.currentUser?.email, user.email) + #endif + + } catch { + XCTFail(error.localizedDescription) + } + } + func testSaveAndUpdateCurrentUserModifiedEmail() throws { // swiftlint:disable:this function_body_length + XCTAssertNil(User.current?.objectId) + try userSignUp() + XCTAssertNotNil(User.current?.objectId) + + guard var user = User.current else { + XCTFail("Should unwrap") + return + } + user.email = "pease@parse.com" + XCTAssertNotEqual(User.current?.email, user.email) var userOnServer = user userOnServer.createdAt = User.current?.createdAt userOnServer.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) @@ -572,6 +637,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length do { let saved = try user.save(options: [.useMasterKey]) XCTAssert(saved.hasSameObjectId(as: userOnServer)) + XCTAssertEqual(saved.email, user.email) guard let savedCreatedAt = saved.createdAt, let savedUpdatedAt = saved.updatedAt else { XCTFail("Should unwrap dates") @@ -588,6 +654,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length //Should be updated in memory XCTAssertEqual(User.current?.updatedAt, savedUpdatedAt) + XCTAssertEqual(User.current?.email, user.email) #if !os(Linux) && !os(Android) //Should be updated in Keychain @@ -597,6 +664,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length return } XCTAssertEqual(keychainUser.currentUser?.updatedAt, savedUpdatedAt) + XCTAssertEqual(keychainUser.currentUser?.email, user.email) #endif } catch { @@ -604,17 +672,90 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } } - func testSaveAsyncAndUpdateCurrentUser() { // swiftlint:disable:this function_body_length + func testSaveAsyncAndUpdateCurrentUser() throws { // swiftlint:disable:this function_body_length XCTAssertNil(User.current?.objectId) - testLogin() - MockURLProtocol.removeAll() + try userSignUp() XCTAssertNotNil(User.current?.objectId) guard let user = User.current else { XCTFail("Should unwrap") return } + XCTAssertNotNil(user.email) + var userOnServer = user + userOnServer.createdAt = User.current?.createdAt + userOnServer.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) + + let encoded: Data! + do { + encoded = try userOnServer.getEncoder().encode(userOnServer, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let expectation1 = XCTestExpectation(description: "Fetch user1") + user.save(options: [], callbackQueue: .global(qos: .background)) { result in + + switch result { + case .success(let saved): + XCTAssert(saved.hasSameObjectId(as: userOnServer)) + XCTAssertEqual(saved.email, user.email) + guard let savedCreatedAt = saved.createdAt, + let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalCreatedAt = user.createdAt, + let originalUpdatedAt = user.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(saved.ACL) + + //Should be updated in memory + XCTAssertEqual(User.current?.updatedAt, savedUpdatedAt) + XCTAssertEqual(User.current?.email, user.email) + + #if !os(Linux) && !os(Android) + //Should be updated in Keychain + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) else { + XCTFail("Should get object from Keychain") + return + } + XCTAssertEqual(keychainUser.currentUser?.updatedAt, savedUpdatedAt) + XCTAssertEqual(keychainUser.currentUser?.email, user.email) + #endif + + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + func testSaveAsyncAndUpdateCurrentUserModifiedEmail() throws { // swiftlint:disable:this function_body_length + XCTAssertNil(User.current?.objectId) + try userSignUp() + XCTAssertNotNil(User.current?.objectId) + + guard var user = User.current else { + XCTFail("Should unwrap") + return + } + user.email = "pease@parse.com" + XCTAssertNotEqual(User.current?.email, user.email) var userOnServer = user userOnServer.createdAt = User.current?.createdAt userOnServer.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) @@ -636,10 +777,11 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length user.save(options: [], callbackQueue: .global(qos: .background)) { result in switch result { - case .success(let fetched): - XCTAssert(fetched.hasSameObjectId(as: userOnServer)) - guard let fetchedCreatedAt = fetched.createdAt, - let fetchedUpdatedAt = fetched.updatedAt else { + case .success(let saved): + XCTAssert(saved.hasSameObjectId(as: userOnServer)) + XCTAssertEqual(saved.email, user.email) + guard let savedCreatedAt = saved.createdAt, + let savedUpdatedAt = saved.updatedAt else { XCTFail("Should unwrap dates") expectation1.fulfill() return @@ -650,12 +792,13 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length expectation1.fulfill() return } - XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) - XCTAssertGreaterThan(fetchedUpdatedAt, originalUpdatedAt) - XCTAssertNil(fetched.ACL) + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(saved.ACL) //Should be updated in memory - XCTAssertEqual(User.current?.updatedAt, fetchedUpdatedAt) + XCTAssertEqual(User.current?.updatedAt, savedUpdatedAt) + XCTAssertEqual(User.current?.email, user.email) #if !os(Linux) && !os(Android) //Should be updated in Keychain @@ -664,7 +807,8 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTFail("Should get object from Keychain") return } - XCTAssertEqual(keychainUser.currentUser?.updatedAt, fetchedUpdatedAt) + XCTAssertEqual(keychainUser.currentUser?.updatedAt, savedUpdatedAt) + XCTAssertEqual(keychainUser.currentUser?.email, user.email) #endif case .failure(let error):