From c36431904d1b54c148cc286413a1114090bc6322 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 11 Sep 2022 11:38:30 -0400 Subject: [PATCH 01/11] fix: login with ObjC keychain --- .../Objects/ParseInstallation+async.swift | 7 ++++++- .../Objects/ParseInstallation+combine.swift | 3 ++- .../ParseSwift/Objects/ParseInstallation.swift | 4 +++- Sources/ParseSwift/Objects/ParseUser.swift | 6 +++--- .../MigrateObjCSDKCombineTests.swift | 17 ++++++++++------- Tests/ParseSwiftTests/MigrateObjCSDKTests.swift | 17 ++++++++++------- 6 files changed, 34 insertions(+), 20 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseInstallation+async.swift b/Sources/ParseSwift/Objects/ParseInstallation+async.swift index 9cefaa8ea..09af7de45 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+async.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+async.swift @@ -338,9 +338,14 @@ public extension ParseInstallation { - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: Returns saved `ParseInstallation`. - throws: An error of type `ParseError`. + - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer + desires a different policy, it should be inserted in `options`. + - warning: When initializing the Swift SDK, `migratingFromObjcSDK` should be set to **false** + when calling this method. - warning: It is recommended to only use this method after a succesfful migration. Calling this method will destroy the entire Objective-C Keychain and `ParseInstallation` on the Parse - Server. + Server. This method assumes **PFInstallation.installationId** is saved to the Keychain. If the + **installationId** is not saved to the Keychain, this method will not work. */ static func deleteObjCKeychain(options: API.Options = []) async throws { let result = try await withCheckedThrowingContinuation { continuation in diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift index b7dae2d0d..882c37e79 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -313,7 +313,8 @@ public extension ParseInstallation { - warning: When initializing the Swift SDK, `migratingFromObjcSDK` should be set to **false** when calling this method. - warning: The latest **PFInstallation** from the Objective-C SDK should be saved to your - Parse Server before calling this method. + Parse Server before calling this method. This method assumes **PFInstallation.installationId** is saved + to the Keychain. If the **installationId** is not saved to the Keychain, this method will not work. */ static func migrateFromObjCKeychainPublisher(copyEntireInstallation: Bool = true, options: API.Options = []) -> Future { diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index dda76297e..22659af44 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -1533,7 +1533,9 @@ public extension ParseInstallation { - warning: When initializing the Swift SDK, `migratingFromObjcSDK` should be set to **false** when calling this method. - warning: The latest **PFInstallation** from the Objective-C SDK should be saved to your - Parse Server before calling this method. + Parse Server before calling this method. This method assumes **PFInstallation.installationId** + is saved to the Keychain. If the **installationId** is not saved to the Keychain, this method will + not work. */ static func migrateFromObjCKeychain(copyEntireInstallation: Bool = true, options: API.Options = [], diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 121b48220..a69e1110b 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -372,10 +372,10 @@ extension ParseUser { completion: @escaping (Result) -> Void) { let objcParseKeychain = KeychainStore.objectiveC - let objcParseSessionToken: String? = objcParseKeychain?.object(forKey: "sessionToken") ?? - objcParseKeychain?.object(forKey: "session_token") - guard let sessionToken = objcParseSessionToken else { + guard let objcParseUser: [String: String] = objcParseKeychain?.object(forKey: "currentUser"), + let sessionToken: String = objcParseUser["sessionToken"] ?? + objcParseUser["session_token"] else { let error = ParseError(code: .unknownError, message: "Could not find a session token in the Parse Objective-C SDK Keychain.") callbackQueue.async { diff --git a/Tests/ParseSwiftTests/MigrateObjCSDKCombineTests.swift b/Tests/ParseSwiftTests/MigrateObjCSDKCombineTests.swift index 1cc8e9086..9835c664d 100644 --- a/Tests/ParseSwiftTests/MigrateObjCSDKCombineTests.swift +++ b/Tests/ParseSwiftTests/MigrateObjCSDKCombineTests.swift @@ -146,14 +146,17 @@ class MigrateObjCSDKCombineTests: XCTestCase { return } + let currentUserDictionary = ["sessionToken": objcSessionToken] + let currentUserDictionary2 = ["session_token": objcSessionToken2] + let currentUserDictionary3 = ["sessionToken": objcSessionToken, + "session_token": objcSessionToken2] _ = objcParseKeychain.set(object: installationId, forKey: "installationId") if useBothTokens { - _ = objcParseKeychain.set(object: objcSessionToken, forKey: "sessionToken") - _ = objcParseKeychain.set(object: objcSessionToken2, forKey: "session_token") + _ = objcParseKeychain.set(object: currentUserDictionary3, forKey: "currentUser") } else if !useOldObjCToken { - _ = objcParseKeychain.set(object: objcSessionToken, forKey: "sessionToken") + _ = objcParseKeychain.set(object: currentUserDictionary, forKey: "currentUser") } else { - _ = objcParseKeychain.set(object: objcSessionToken, forKey: "session_token") + _ = objcParseKeychain.set(object: currentUserDictionary2, forKey: "currentUser") } } @@ -240,7 +243,7 @@ class MigrateObjCSDKCombineTests: XCTestCase { var serverResponse = LoginSignupResponse() serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) - serverResponse.sessionToken = objcSessionToken + serverResponse.sessionToken = objcSessionToken2 serverResponse.username = loginUserName MockURLProtocol.mockRequests { _ in @@ -267,7 +270,7 @@ class MigrateObjCSDKCombineTests: XCTestCase { XCTAssertEqual(loggedIn.username, self.loginUserName) XCTAssertNil(loggedIn.password) XCTAssertEqual(loggedIn.objectId, serverResponse.objectId) - XCTAssertEqual(loggedIn.sessionToken, self.objcSessionToken) + XCTAssertEqual(loggedIn.sessionToken, self.objcSessionToken2) XCTAssertEqual(loggedIn.customKey, serverResponse.customKey) XCTAssertNil(loggedIn.ACL) @@ -282,7 +285,7 @@ class MigrateObjCSDKCombineTests: XCTestCase { XCTAssertEqual(userFromKeychain.username, self.loginUserName) XCTAssertNil(userFromKeychain.password) XCTAssertEqual(loggedIn.objectId, userFromKeychain.objectId) - XCTAssertEqual(userFromKeychain.sessionToken, self.objcSessionToken) + XCTAssertEqual(userFromKeychain.sessionToken, self.objcSessionToken2) XCTAssertEqual(loggedIn.customKey, userFromKeychain.customKey) XCTAssertNil(userFromKeychain.ACL) expectation1.fulfill() diff --git a/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift b/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift index 71e9a9896..89d0041bf 100644 --- a/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift +++ b/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift @@ -145,14 +145,17 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng return } + let currentUserDictionary = ["sessionToken": objcSessionToken] + let currentUserDictionary2 = ["session_token": objcSessionToken2] + let currentUserDictionary3 = ["sessionToken": objcSessionToken, + "session_token": objcSessionToken2] _ = objcParseKeychain.set(object: installationId, forKey: "installationId") if useBothTokens { - _ = objcParseKeychain.set(object: objcSessionToken, forKey: "sessionToken") - _ = objcParseKeychain.set(object: objcSessionToken2, forKey: "session_token") + _ = objcParseKeychain.set(object: currentUserDictionary3, forKey: "currentUser") } else if !useOldObjCToken { - _ = objcParseKeychain.set(object: objcSessionToken, forKey: "sessionToken") + _ = objcParseKeychain.set(object: currentUserDictionary, forKey: "currentUser") } else { - _ = objcParseKeychain.set(object: objcSessionToken, forKey: "session_token") + _ = objcParseKeychain.set(object: currentUserDictionary2, forKey: "currentUser") } } @@ -222,7 +225,7 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng var serverResponse = LoginSignupResponse() serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) - serverResponse.sessionToken = objcSessionToken + serverResponse.sessionToken = objcSessionToken2 serverResponse.username = loginUserName MockURLProtocol.mockRequests { _ in @@ -241,7 +244,7 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertEqual(loggedIn.username, loginUserName) XCTAssertNil(loggedIn.password) XCTAssertEqual(loggedIn.objectId, serverResponse.objectId) - XCTAssertEqual(loggedIn.sessionToken, objcSessionToken) + XCTAssertEqual(loggedIn.sessionToken, objcSessionToken2) XCTAssertEqual(loggedIn.customKey, serverResponse.customKey) XCTAssertNil(loggedIn.ACL) @@ -255,7 +258,7 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertEqual(userFromKeychain.username, loginUserName) XCTAssertNil(userFromKeychain.password) XCTAssertEqual(loggedIn.objectId, userFromKeychain.objectId) - XCTAssertEqual(userFromKeychain.sessionToken, objcSessionToken) + XCTAssertEqual(userFromKeychain.sessionToken, objcSessionToken2) XCTAssertEqual(loggedIn.customKey, userFromKeychain.customKey) XCTAssertNil(userFromKeychain.ACL) } From 0b20b264e3e486e18636a4fe38e9b6b516a9d08a Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 11 Sep 2022 11:44:49 -0400 Subject: [PATCH 02/11] add changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b48906285..d3ed7870c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ ### 4.11.0 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.10.0...4.11.0) +__Fixes__ +- Properly get the session token from the Parse Objective-C Keychain when using ParseUser.loginUsingObjCKeychain ([#407](https://github.com/parse-community/Parse-Swift/pull/407)), thanks to [Corey Baker](https://github.com/cbaker6). + __New features__ - Add a set method that developers can call on their ParseObjects which automatically sends updated properties to a Parse Server and merges those updates with the original ParseObject locally. The feature removes the requirement to call mergeable and implement merge(), but comes at additional computational overhead ([#406](https://github.com/parse-community/Parse-Swift/pull/406)), thanks to [Corey Baker](https://github.com/cbaker6). From f58c10491f97481f9e7304288494b4a830fc3dd9 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 11 Sep 2022 16:05:03 -0400 Subject: [PATCH 03/11] add become method to installation --- CHANGELOG.md | 7 +- .../Objects/ParseInstallation+async.swift | 29 +++++ .../Objects/ParseInstallation+combine.swift | 28 +++++ .../Objects/ParseInstallation.swift | 119 +++++++++++------- Sources/ParseSwift/Objects/ParseUser.swift | 15 ++- .../ParseSwiftTests/MigrateObjCSDKTests.swift | 8 +- .../ParseInstallationAsyncTests.swift | 70 +++++++++++ .../ParseInstallationCombineTests.swift | 84 +++++++++++++ 8 files changed, 301 insertions(+), 59 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3ed7870c..139a23597 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,12 +4,15 @@ [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.10.0...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ -### 4.11.0 -[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.10.0...4.11.0) +__New features__ +- Add the become method to ParseInstallation which allows any ParseInstallation to be copied to the current installation. This method can be used to migrate any ParseInstallation to the current installation in the Swift SDK ([#407](https://github.com/parse-community/Parse-Swift/pull/407)), thanks to [Corey Baker](https://github.com/cbaker6). __Fixes__ - Properly get the session token from the Parse Objective-C Keychain when using ParseUser.loginUsingObjCKeychain ([#407](https://github.com/parse-community/Parse-Swift/pull/407)), thanks to [Corey Baker](https://github.com/cbaker6). +### 4.11.0 +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/4.10.0...4.11.0) + __New features__ - Add a set method that developers can call on their ParseObjects which automatically sends updated properties to a Parse Server and merges those updates with the original ParseObject locally. The feature removes the requirement to call mergeable and implement merge(), but comes at additional computational overhead ([#406](https://github.com/parse-community/Parse-Swift/pull/406)), thanks to [Corey Baker](https://github.com/cbaker6). diff --git a/Sources/ParseSwift/Objects/ParseInstallation+async.swift b/Sources/ParseSwift/Objects/ParseInstallation+async.swift index 09af7de45..7b2ca757a 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+async.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+async.swift @@ -120,8 +120,37 @@ public extension ParseInstallation { throw error } } + + /** + Copy the `ParseInstallation` *asynchronously* based on the `installationId`. + On success, this saves the `ParseInstallation` to the keychain, so you can retrieve + the current installation using *current*. + + - parameter installationId: The **id** of the `ParseInstallation` to become. + - parameter copyEntireInstallation: When **true**, copies the entire `ParseInstallation`. + When **false**, only the `channels` and `deviceToken` are copied; resulting in a new + `ParseInstallation` for original `sessionToken`. 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: The block to execute. + It should have the following argument signature: `(Result)`. + - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer + desires a different policy, it should be inserted in `options`. + */ + @discardableResult static func become(installationId: String, + copyEntireInstallation: Bool = true, + deleteObjectiveCKeychain: Bool = false, + options: API.Options = []) async throws -> Self { + try await withCheckedThrowingContinuation { continuation in + Self.become(installationId: installationId, + copyEntireInstallation: copyEntireInstallation, + options: options, + completion: continuation.resume) + } + } } +// MARK: Batch Support public extension Sequence where Element: ParseInstallation { /** Fetches a collection of installations *aynchronously* with the current data from the server and sets diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift index 882c37e79..8c6fa3f0e 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -117,8 +117,36 @@ public extension ParseInstallation { self.delete(options: options, completion: promise) } } + + /** + Copies the `ParseInstallation` *asynchronously* based on the `installationId` and publishes + when complete. On success, this saves the `ParseInstallation` to the keychain, so you can retrieve + the current installation using *current*. + + - parameter installationId: The **id** of the `ParseInstallation` to become. + - parameter copyEntireInstallation: When **true**, copies the entire `ParseInstallation`. + When **false**, only the `channels` and `deviceToken` are copied; resulting in a new + `ParseInstallation` for original `sessionToken`. 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: The block to execute. + It should have the following argument signature: `(Result)`. + - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer + desires a different policy, it should be inserted in `options`. + */ + static func becomePublisher(installationId: String, + copyEntireInstallation: Bool = true, + options: API.Options = []) -> Future { + Future { promise in + Self.become(installationId: installationId, + copyEntireInstallation: copyEntireInstallation, + options: options, + completion: promise) + } + } } +// MARK: Batch Support public extension Sequence where Element: ParseInstallation { /** Fetches a collection of installations *aynchronously* with the current data from the server and sets diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 22659af44..9312d263a 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -303,6 +303,76 @@ public extension ParseInstallation { Self.updateInternalFieldsCorrectly() } } + + /** + Copy the `ParseInstallation` *asynchronously* based on the `installationId`. + On success, this saves the `ParseInstallation` to the keychain, so you can retrieve + the current installation using *current*. + + - parameter installationId: The **id** of the `ParseInstallation` to become. + - parameter copyEntireInstallation: When **true**, copies the entire `ParseInstallation`. + When **false**, only the `channels` and `deviceToken` are copied; resulting in a new + `ParseInstallation` for original `sessionToken`. 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: The block to execute. + It should have the following argument signature: `(Result)`. + - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer + desires a different policy, it should be inserted in `options`. + */ + static func become(installationId: String, + copyEntireInstallation: Bool = true, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + guard var currentInstallation = Self.current else { + let error = ParseError(code: .unknownError, + message: "Current installation does not exist") + callbackQueue.async { + completion(.failure(error)) + } + return + } + guard currentInstallation.installationId != installationId else { + // If the installationId's are the same, assume successful replacement already occured. + callbackQueue.async { + completion(.success(currentInstallation)) + } + return + } + currentInstallation.installationId = installationId + currentInstallation.fetch(options: options, callbackQueue: callbackQueue) { result in + switch result { + case .success(var updatedInstallation): + if copyEntireInstallation { + updatedInstallation.updateAutomaticInfo() + Self.currentContainer.installationId = updatedInstallation.installationId + Self.currentContainer.currentInstallation = updatedInstallation + } else { + Self.current?.channels = updatedInstallation.channels + if Self.current?.deviceToken == nil { + Self.current?.deviceToken = updatedInstallation.deviceToken + } + } + Self.saveCurrentContainerToKeychain() + guard let latestInstallation = Self.current else { + let error = ParseError(code: .unknownError, + message: "Had trouble migrating the installation") + callbackQueue.async { + completion(.failure(error)) + } + return + } + latestInstallation.save(options: options, + callbackQueue: callbackQueue, + completion: completion) + case .failure(let error): + callbackQueue.async { + completion(.failure(error)) + } + } + } + } } // MARK: Automatic Info @@ -1550,53 +1620,8 @@ public extension ParseInstallation { } return } - guard var currentInstallation = Self.current else { - let error = ParseError(code: .unknownError, - message: "Current installation does not exist") - callbackQueue.async { - completion(.failure(error)) - } - return - } - guard currentInstallation.installationId != oldInstallationId else { - // If the installationId's are the same, assume successful migration already occured. - callbackQueue.async { - completion(.success(currentInstallation)) - } - return - } - currentInstallation.installationId = oldInstallationId - currentInstallation.fetch(options: options, callbackQueue: callbackQueue) { result in - switch result { - case .success(var updatedInstallation): - if copyEntireInstallation { - updatedInstallation.updateAutomaticInfo() - Self.currentContainer.installationId = updatedInstallation.installationId - Self.currentContainer.currentInstallation = updatedInstallation - } else { - Self.current?.channels = updatedInstallation.channels - if Self.current?.deviceToken == nil { - Self.current?.deviceToken = updatedInstallation.deviceToken - } - } - Self.saveCurrentContainerToKeychain() - guard let latestInstallation = Self.current else { - let error = ParseError(code: .unknownError, - message: "Had trouble migrating the installation") - callbackQueue.async { - completion(.failure(error)) - } - return - } - latestInstallation.save(options: options, - callbackQueue: callbackQueue, - completion: completion) - case .failure(let error): - callbackQueue.async { - completion(.failure(error)) - } - } - } + become(installationId: oldInstallationId, + completion: completion) } /** diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index a69e1110b..2aa11f18d 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -263,8 +263,9 @@ extension ParseUser { } /** - Logs in a `ParseUser` *synchronously* with a session token. On success, this saves the session - to the keychain, so you can retrieve the currently logged in user using *current*. + Logs in a `ParseUser` *synchronously* with a session token. On success, this saves the logged in + `ParseUser`with this session to the keychain, so you can retrieve the currently logged in user using + *current*. - parameter sessionToken: The sessionToken of the user to login. - parameter options: A set of header options sent to the server. Defaults to an empty set. @@ -283,8 +284,9 @@ extension ParseUser { } /** - Logs in a `ParseUser` *asynchronously* with a session token. On success, this saves the session - to the keychain, so you can retrieve the currently logged in user using *current*. + Logs in a `ParseUser` *asynchronously* with a session token. On success, this saves the logged in + `ParseUser`with this session to the keychain, so you can retrieve the currently logged in user using + *current*. - parameter sessionToken: The sessionToken of the user to login. - parameter options: A set of header options sent to the server. Defaults to an empty set. @@ -306,8 +308,9 @@ extension ParseUser { } /** - Logs in a `ParseUser` *asynchronously* with a session token. On success, this saves the session - to the keychain, so you can retrieve the currently logged in user using *current*. + Logs in a `ParseUser` *asynchronously* with a session token. On success, this saves the logged in + `ParseUser`with this session to the keychain, so you can retrieve the currently logged in user using + *current*. - parameter sessionToken: The sessionToken of the user to login. - parameter options: A set of header options sent to the server. Defaults to an empty set. diff --git a/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift b/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift index 89d0041bf..0fab74f4f 100644 --- a/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift +++ b/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift @@ -434,12 +434,12 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertTrue(fetched.hasSameObjectId(as: installationOnServer)) XCTAssertTrue(fetched.hasSameInstallationId(as: installationOnServer)) guard let fetchedCreatedAt = fetched.createdAt else { - XCTFail("Should unwrap dates") - return + XCTFail("Should unwrap dates") + return } guard let originalCreatedAt = installationOnServer.createdAt else { - XCTFail("Should unwrap dates") - return + XCTFail("Should unwrap dates") + return } XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) XCTAssertEqual(fetched.channels, installationOnServer.channels) diff --git a/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift b/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift index 45855659d..5b9bb7fef 100644 --- a/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift @@ -1119,5 +1119,75 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b } } } + + @MainActor + func testBecome() async throws { + try saveCurrentInstallation() + MockURLProtocol.removeAll() + + guard let installation = Installation.current, + let savedObjectId = installation.objectId else { + XCTFail("Should unwrap") + return + } + XCTAssertEqual(savedObjectId, self.testInstallationObjectId) + + var installationOnServer = installation + installationOnServer.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + installationOnServer.customKey = "newValue" + installationOnServer.installationId = "wowsers" + installationOnServer.channels = ["yo"] + installationOnServer.deviceToken = "no" + + let encoded: Data! + do { + encoded = try installationOnServer.getEncoder().encode(installationOnServer, skipKeys: .none) + // Get dates in correct format from ParseDecoding strategy + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let fetched = try await Installation.become(installationId: "wowsers") + guard let currentInstallation = Installation.current else { + XCTFail("Should have current installation") + return + } + XCTAssertTrue(fetched.hasSameObjectId(as: currentInstallation)) + XCTAssertTrue(fetched.hasSameInstallationId(as: currentInstallation)) + XCTAssertTrue(fetched.hasSameObjectId(as: installationOnServer)) + XCTAssertTrue(fetched.hasSameInstallationId(as: installationOnServer)) + guard let fetchedCreatedAt = fetched.createdAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = installationOnServer.createdAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) + XCTAssertEqual(fetched.channels, installationOnServer.channels) + XCTAssertEqual(fetched.deviceToken, installationOnServer.deviceToken) + + // Should be updated in memory + XCTAssertEqual(Installation.current?.installationId, "wowsers") + XCTAssertEqual(Installation.current?.customKey, installationOnServer.customKey) + XCTAssertEqual(Installation.current?.channels, installationOnServer.channels) + XCTAssertEqual(Installation.current?.deviceToken, installationOnServer.deviceToken) + + // Should be updated in Keychain + guard let keychainInstallation: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { + XCTFail("Should get object from Keychain") + return + } + XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, "wowsers") + XCTAssertEqual(keychainInstallation.currentInstallation?.channels, installationOnServer.channels) + XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) + } } #endif diff --git a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift index 68df37d81..994faae57 100644 --- a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift @@ -881,6 +881,90 @@ class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type publisher.store(in: &subscriptions) wait(for: [expectation1], timeout: 20.0) } + + func testBecome() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Migrate Installation") + + try saveCurrentInstallation() + MockURLProtocol.removeAll() + + guard let installation = Installation.current, + let savedObjectId = installation.objectId else { + XCTFail("Should unwrap") + return + } + XCTAssertEqual(savedObjectId, self.testInstallationObjectId) + + var installationOnServer = installation + installationOnServer.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + installationOnServer.customKey = "newValue" + installationOnServer.installationId = "wowsers" + installationOnServer.channels = ["yo"] + installationOnServer.deviceToken = "no" + + let encoded: Data! + do { + encoded = try installationOnServer.getEncoder().encode(installationOnServer, skipKeys: .none) + // Get dates in correct format from ParseDecoding strategy + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = Installation.becomePublisher(installationId: "wowsers") + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + guard let currentInstallation = Installation.current else { + XCTFail("Should have current installation") + return + } + XCTAssertTrue(fetched.hasSameObjectId(as: currentInstallation)) + XCTAssertTrue(fetched.hasSameInstallationId(as: currentInstallation)) + XCTAssertTrue(fetched.hasSameObjectId(as: installationOnServer)) + XCTAssertTrue(fetched.hasSameInstallationId(as: installationOnServer)) + guard let fetchedCreatedAt = fetched.createdAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = installationOnServer.createdAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) + XCTAssertEqual(fetched.channels, installationOnServer.channels) + XCTAssertEqual(fetched.deviceToken, installationOnServer.deviceToken) + + // Should be updated in memory + XCTAssertEqual(Installation.current?.installationId, "wowsers") + XCTAssertEqual(Installation.current?.customKey, installationOnServer.customKey) + XCTAssertEqual(Installation.current?.channels, installationOnServer.channels) + XCTAssertEqual(Installation.current?.deviceToken, installationOnServer.deviceToken) + + // Should be updated in Keychain + guard let keychainInstallation: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { + XCTFail("Should get object from Keychain") + return + } + XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, "wowsers") + XCTAssertEqual(keychainInstallation.currentInstallation?.channels, installationOnServer.channels) + XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) + expectation1.fulfill() + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } } #endif From 4d3d2346dd14506aea97ed3f1ed369a052c807da Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 11 Sep 2022 16:17:22 -0400 Subject: [PATCH 04/11] pass copyEntireInstallation to become --- Sources/ParseSwift/Objects/ParseInstallation.swift | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 9312d263a..7eb1b6bb0 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -1621,7 +1621,8 @@ public extension ParseInstallation { return } become(installationId: oldInstallationId, - completion: completion) + copyEntireInstallation: copyEntireInstallation, + completion: completion) } /** From dcd12213c4e82876fe7280c2153718ec9c03deaf Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 11 Sep 2022 16:32:45 -0400 Subject: [PATCH 05/11] fix method labels --- Sources/ParseSwift/Objects/ParseInstallation+async.swift | 6 ++---- Sources/ParseSwift/Objects/ParseInstallation+combine.swift | 4 ++-- Sources/ParseSwift/Objects/ParseInstallation.swift | 4 ++-- Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift | 4 +++- Tests/ParseSwiftTests/ParseInstallationCombineTests.swift | 4 +++- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseInstallation+async.swift b/Sources/ParseSwift/Objects/ParseInstallation+async.swift index 7b2ca757a..7f27ab626 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+async.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+async.swift @@ -137,12 +137,11 @@ public extension ParseInstallation { - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer desires a different policy, it should be inserted in `options`. */ - @discardableResult static func become(installationId: String, + @discardableResult static func become(_ installationId: String, copyEntireInstallation: Bool = true, - deleteObjectiveCKeychain: Bool = false, options: API.Options = []) async throws -> Self { try await withCheckedThrowingContinuation { continuation in - Self.become(installationId: installationId, + Self.become(installationId, copyEntireInstallation: copyEntireInstallation, options: options, completion: continuation.resume) @@ -351,7 +350,6 @@ public extension ParseInstallation { Parse Server before calling this method. */ @discardableResult static func migrateFromObjCKeychain(copyEntireInstallation: Bool = true, - deleteObjectiveCKeychain: Bool = false, options: API.Options = []) async throws -> Self { try await withCheckedThrowingContinuation { continuation in Self.migrateFromObjCKeychain(copyEntireInstallation: copyEntireInstallation, diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift index 8c6fa3f0e..262a882c8 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -134,11 +134,11 @@ public extension ParseInstallation { - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer desires a different policy, it should be inserted in `options`. */ - static func becomePublisher(installationId: String, + static func becomePublisher(_ installationId: String, copyEntireInstallation: Bool = true, options: API.Options = []) -> Future { Future { promise in - Self.become(installationId: installationId, + Self.become(installationId, copyEntireInstallation: copyEntireInstallation, options: options, completion: promise) diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 7eb1b6bb0..1be8d1b69 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -320,7 +320,7 @@ public extension ParseInstallation { - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer desires a different policy, it should be inserted in `options`. */ - static func become(installationId: String, + static func become(_ installationId: String, copyEntireInstallation: Bool = true, options: API.Options = [], callbackQueue: DispatchQueue = .main, @@ -1620,7 +1620,7 @@ public extension ParseInstallation { } return } - become(installationId: oldInstallationId, + become(oldInstallationId, copyEntireInstallation: copyEntireInstallation, completion: completion) } diff --git a/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift b/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift index 5b9bb7fef..e174ba727 100644 --- a/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift @@ -1152,7 +1152,7 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } - let fetched = try await Installation.become(installationId: "wowsers") + let fetched = try await Installation.become("wowsers") guard let currentInstallation = Installation.current else { XCTFail("Should have current installation") return @@ -1179,6 +1179,7 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b XCTAssertEqual(Installation.current?.channels, installationOnServer.channels) XCTAssertEqual(Installation.current?.deviceToken, installationOnServer.deviceToken) + #if !os(Linux) && !os(Android) && !os(Windows) // Should be updated in Keychain guard let keychainInstallation: CurrentInstallationContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { @@ -1188,6 +1189,7 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, "wowsers") XCTAssertEqual(keychainInstallation.currentInstallation?.channels, installationOnServer.channels) XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) + #endif } } #endif diff --git a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift index 994faae57..dedd5919a 100644 --- a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift @@ -916,7 +916,7 @@ class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } - let publisher = Installation.becomePublisher(installationId: "wowsers") + let publisher = Installation.becomePublisher("wowsers") .sink(receiveCompletion: { result in if case let .failure(error) = result { @@ -951,6 +951,7 @@ class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type XCTAssertEqual(Installation.current?.channels, installationOnServer.channels) XCTAssertEqual(Installation.current?.deviceToken, installationOnServer.deviceToken) + #if !os(Linux) && !os(Android) && !os(Windows) // Should be updated in Keychain guard let keychainInstallation: CurrentInstallationContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { @@ -960,6 +961,7 @@ class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, "wowsers") XCTAssertEqual(keychainInstallation.currentInstallation?.channels, installationOnServer.channels) XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) + #endif expectation1.fulfill() }) publisher.store(in: &subscriptions) From ab5f257ff84fd73246cf22045049b1b89b27b944 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 11 Sep 2022 20:22:47 -0400 Subject: [PATCH 06/11] fix querying objc keychain --- .../Objects/ParseInstallation.swift | 4 +- Sources/ParseSwift/Objects/ParseUser.swift | 2 +- Sources/ParseSwift/Parse.swift | 4 +- .../ParseSwift/Storage/KeychainStore.swift | 213 ++++++++++++------ .../ParseSwiftTests/InitializeSDKTests.swift | 14 +- .../MigrateObjCSDKCombineTests.swift | 10 +- .../ParseSwiftTests/MigrateObjCSDKTests.swift | 10 +- 7 files changed, 170 insertions(+), 87 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 1be8d1b69..0c951a429 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -1612,7 +1612,7 @@ public extension ParseInstallation { callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { guard let objcParseKeychain = KeychainStore.objectiveC, - let oldInstallationId: String = objcParseKeychain.object(forKey: "installationId") else { + let oldInstallationId: String = objcParseKeychain.objectObjectiveC(forKey: "installationId") else { let error = ParseError(code: .unknownError, message: "Could not find Installation in the Objective-C SDK Keychain") callbackQueue.async { @@ -1644,7 +1644,7 @@ public extension ParseInstallation { callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { guard let objcParseKeychain = KeychainStore.objectiveC, - let oldInstallationId: String = objcParseKeychain.object(forKey: "installationId") else { + let oldInstallationId: String = objcParseKeychain.objectObjectiveC(forKey: "installationId") else { let error = ParseError(code: .unknownError, message: "Could not find Installation in the Objective-C SDK Keychain") callbackQueue.async { diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 2aa11f18d..94b8ce2c8 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -376,7 +376,7 @@ extension ParseUser { let objcParseKeychain = KeychainStore.objectiveC - guard let objcParseUser: [String: String] = objcParseKeychain?.object(forKey: "currentUser"), + guard let objcParseUser: [String: String] = objcParseKeychain?.objectObjectiveC(forKey: "currentUser"), let sessionToken: String = objcParseUser["sessionToken"] ?? objcParseUser["session_token"] else { let error = ParseError(code: .unknownError, diff --git a/Sources/ParseSwift/Parse.swift b/Sources/ParseSwift/Parse.swift index 5f764296b..6768ff83c 100644 --- a/Sources/ParseSwift/Parse.swift +++ b/Sources/ParseSwift/Parse.swift @@ -160,7 +160,7 @@ public func initialize(configuration: ParseConfiguration) { #if !os(Linux) && !os(Android) && !os(Windows) if configuration.isMigratingFromObjcSDK { if let objcParseKeychain = KeychainStore.objectiveC { - guard let installationId: String = objcParseKeychain.object(forKey: "installationId"), + guard let installationId: String = objcParseKeychain.objectObjectiveC(forKey: "installationId"), BaseParseInstallation.current?.installationId != installationId else { return } @@ -383,7 +383,7 @@ public func clearCache() { - warning: The keychain cannot be recovered after deletion. */ public func deleteObjectiveCKeychain() throws { - try KeychainStore.objectiveC?.deleteAll() + try KeychainStore.objectiveC?.deleteAllObjectiveC() } /** diff --git a/Sources/ParseSwift/Storage/KeychainStore.swift b/Sources/ParseSwift/Storage/KeychainStore.swift index 46f8125b7..3373005cb 100644 --- a/Sources/ParseSwift/Storage/KeychainStore.swift +++ b/Sources/ParseSwift/Storage/KeychainStore.swift @@ -22,13 +22,15 @@ struct KeychainStore: SecureStorage { let synchronizationQueue: DispatchQueue let service: String + static var objectiveCService: String { + guard let identifier = Bundle.main.bundleIdentifier else { + return "" + } + return "\(identifier).com.parse.sdk" + } static var shared = KeychainStore() static var objectiveC: KeychainStore? { - if let identifier = Bundle.main.bundleIdentifier { - return KeychainStore(service: "\(identifier).com.parse.sdk") - } else { - return nil - } + KeychainStore(service: objectiveCService) } // This Keychain was used by SDK <= 1.9.7 static var old = KeychainStore(service: "shared") @@ -59,6 +61,15 @@ struct KeychainStore: SecureStorage { return query } + func getObjectiveCKeychainQueryTemplate() -> [String: Any] { + var query = [String: Any]() + if !Self.objectiveCService.isEmpty { + query[kSecAttrService as String] = Self.objectiveCService + } + query[kSecClass as String] = kSecClassGenericPassword as String + return query + } + func copy(_ keychain: KeychainStore, oldAccessGroup: ParseKeychainAccessGroup, newAccessGroup: ParseKeychainAccessGroup) throws { @@ -113,33 +124,43 @@ struct KeychainStore: SecureStorage { } func keychainQuery(forKey key: String, + useObjectiveCKeychain: Bool = false, accessGroup: ParseKeychainAccessGroup) -> [String: Any] { - var query: [String: Any] = getKeychainQueryTemplate() - query[kSecAttrAccount as String] = key - if let keychainAccessGroup = accessGroup.accessGroup { - query[kSecAttrAccessGroup as String] = keychainAccessGroup - if accessGroup.isSyncingKeychainAcrossDevices && isSyncableKey(key) { - query[kSecAttrSynchronizable as String] = kCFBooleanTrue - query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock as String + if !useObjectiveCKeychain { + var query: [String: Any] = getKeychainQueryTemplate() + query[kSecAttrAccount as String] = key + if let keychainAccessGroup = accessGroup.accessGroup { + query[kSecAttrAccessGroup as String] = keychainAccessGroup + if accessGroup.isSyncingKeychainAcrossDevices && isSyncableKey(key) { + query[kSecAttrSynchronizable as String] = kCFBooleanTrue + query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock as String + } else { + query[kSecAttrSynchronizable as String] = kCFBooleanFalse + query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String + } } else { query[kSecAttrSynchronizable as String] = kCFBooleanFalse query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String } + #if os(macOS) + if Parse.configuration.isUsingDataProtectionKeychain { + query[kSecUseDataProtectionKeychain as String] = kCFBooleanTrue + } + #endif + return query } else { - query[kSecAttrSynchronizable as String] = kCFBooleanFalse - query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String + var query: [String: Any] = getKeychainQueryTemplate() + query[kSecAttrAccount as String] = key + query[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock as String + return query } - #if os(macOS) - if Parse.configuration.isUsingDataProtectionKeychain { - query[kSecUseDataProtectionKeychain as String] = kCFBooleanTrue - } - #endif - return query } func data(forKey key: String, + useObjectiveCKeychain: Bool = false, accessGroup: ParseKeychainAccessGroup) -> Data? { var query: [String: Any] = keychainQuery(forKey: key, + useObjectiveCKeychain: useObjectiveCKeychain, accessGroup: accessGroup) query[kSecMatchLimit as String] = kSecMatchLimitOne query[kSecReturnData as String] = kCFBooleanTrue @@ -159,6 +180,7 @@ struct KeychainStore: SecureStorage { private func set(_ data: Data, forKey key: String, + useObjectiveCKeychain: Bool = false, oldAccessGroup: ParseKeychainAccessGroup, newAccessGroup: ParseKeychainAccessGroup) throws { var query = keychainQuery(forKey: key, @@ -167,19 +189,23 @@ struct KeychainStore: SecureStorage { kSecValueData as String: data ] - if let newKeychainAccessGroup = newAccessGroup.accessGroup { - update[kSecAttrAccessGroup as String] = newKeychainAccessGroup - if newAccessGroup.isSyncingKeychainAcrossDevices && isSyncableKey(key) { - update[kSecAttrSynchronizable as String] = kCFBooleanTrue - update[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock as String + if !useObjectiveCKeychain { + if let newKeychainAccessGroup = newAccessGroup.accessGroup { + update[kSecAttrAccessGroup as String] = newKeychainAccessGroup + if newAccessGroup.isSyncingKeychainAcrossDevices && isSyncableKey(key) { + update[kSecAttrSynchronizable as String] = kCFBooleanTrue + update[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock as String + } else { + update[kSecAttrSynchronizable as String] = kCFBooleanFalse + update[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String + } } else { + query.removeValue(forKey: kSecAttrAccessGroup as String) update[kSecAttrSynchronizable as String] = kCFBooleanFalse update[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String } } else { - query.removeValue(forKey: kSecAttrAccessGroup as String) - update[kSecAttrSynchronizable as String] = kCFBooleanFalse - update[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String + update[kSecAttrAccessible as String] = kSecAttrAccessibleAfterFirstUnlock as String } let status = synchronizationQueue.sync(flags: .barrier) { () -> OSStatus in @@ -202,9 +228,11 @@ struct KeychainStore: SecureStorage { } private func removeObject(forKey key: String, + useObjectiveCKeychain: Bool = false, accessGroup: ParseKeychainAccessGroup) -> Bool { dispatchPrecondition(condition: .onQueue(synchronizationQueue)) let query = keychainQuery(forKey: key, + useObjectiveCKeychain: useObjectiveCKeychain, accessGroup: accessGroup) as CFDictionary return SecItemDelete(query) == errSecSuccess } @@ -236,6 +264,44 @@ struct KeychainStore: SecureStorage { return true } } + + func removeAllObjects(useObjectiveCKeychain: Bool) -> Bool { + var query = useObjectiveCKeychain ? getObjectiveCKeychainQueryTemplate() : getKeychainQueryTemplate() + query[kSecReturnAttributes as String] = kCFBooleanTrue + query[kSecMatchLimit as String] = kSecMatchLimitAll + + return synchronizationQueue.sync(flags: .barrier) { () -> Bool in + var result: AnyObject? + let status = withUnsafeMutablePointer(to: &result) { + SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) + } + if status != errSecSuccess { return true } + + guard let results = result as? [[String: Any]] else { return false } + + for item in results { + guard let key = item[kSecAttrAccount as String] as? String else { + continue + } + let removedDefaultObject = self.removeObject(forKey: key, + useObjectiveCKeychain: useObjectiveCKeychain, + accessGroup: Parse.configuration.keychainAccessGroup) + if !useObjectiveCKeychain { + var mutatedKeychainAccessGroup = Parse.configuration.keychainAccessGroup + mutatedKeychainAccessGroup.isSyncingKeychainAcrossDevices.toggle() + let removedToggledObject = self.removeObject(forKey: key, + accessGroup: mutatedKeychainAccessGroup) + mutatedKeychainAccessGroup.accessGroup = nil + let removedNoAccessGroupObject = self.removeObject(forKey: key, + accessGroup: mutatedKeychainAccessGroup) + if !(removedDefaultObject || removedToggledObject || removedNoAccessGroupObject) { + return false + } + } + } + return true + } + } } // MARK: SecureStorage @@ -248,8 +314,7 @@ extension KeychainStore { return nil } do { - let object = try ParseCoding.jsonDecoder().decode(T.self, from: data) - return object + return try ParseCoding.jsonDecoder().decode(T.self, from: data) } catch { return nil } @@ -273,7 +338,7 @@ extension KeychainStore { subscript(key: String) -> T? where T: Codable { get { - return object(forKey: key) + object(forKey: key) } set (object) { _ = set(object: object, forKey: key) @@ -288,38 +353,7 @@ extension KeychainStore { } func removeAllObjects() -> Bool { - var query = getKeychainQueryTemplate() - query[kSecReturnAttributes as String] = kCFBooleanTrue - query[kSecMatchLimit as String] = kSecMatchLimitAll - - return synchronizationQueue.sync(flags: .barrier) { () -> Bool in - var result: AnyObject? - let status = withUnsafeMutablePointer(to: &result) { - SecItemCopyMatching(query as CFDictionary, UnsafeMutablePointer($0)) - } - if status != errSecSuccess { return true } - - guard let results = result as? [[String: Any]] else { return false } - - for item in results { - guard let key = item[kSecAttrAccount as String] as? String else { - continue - } - let removedDefaultObject = self.removeObject(forKey: key, - accessGroup: Parse.configuration.keychainAccessGroup) - var mutatedKeychainAccessGroup = Parse.configuration.keychainAccessGroup - mutatedKeychainAccessGroup.isSyncingKeychainAcrossDevices.toggle() - let removedToggledObject = self.removeObject(forKey: key, - accessGroup: mutatedKeychainAccessGroup) - mutatedKeychainAccessGroup.accessGroup = nil - let removedNoAccessGroupObject = self.removeObject(forKey: key, - accessGroup: mutatedKeychainAccessGroup) - if !(removedDefaultObject || removedToggledObject || removedNoAccessGroupObject) { - return false - } - } - return true - } + removeAllObjects(useObjectiveCKeychain: false) } } @@ -327,7 +361,7 @@ extension KeychainStore { extension KeychainStore { subscript(string key: String) -> String? { get { - return object(forKey: key) + object(forKey: key) } set (object) { _ = set(object: object, forKey: key) @@ -336,7 +370,7 @@ extension KeychainStore { subscript(bool key: String) -> Bool? { get { - return object(forKey: key) + object(forKey: key) } set (object) { _ = set(object: object, forKey: key) @@ -344,4 +378,53 @@ extension KeychainStore { } } +// MARK: Objective-C SDK Keychain +extension KeychainStore { + func objectObjectiveC(forKey key: String) -> T? where T: Decodable { + guard let data = synchronizationQueue.sync(execute: { () -> Data? in + return self.data(forKey: key, + useObjectiveCKeychain: true, + accessGroup: Parse.configuration.keychainAccessGroup) + }) else { + return nil + } + do { + return try NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data) as? T + } catch { + return nil + } + } + + func removeObjectObjectiveC(forKey key: String) -> Bool { + return synchronizationQueue.sync { + return removeObject(forKey: key, + useObjectiveCKeychain: true, + accessGroup: Parse.configuration.keychainAccessGroup) + } + } + + func setObjectiveC(object: T?, forKey key: String) -> Bool where T: Encodable { + guard let object = object else { + return removeObjectObjectiveC(forKey: key) + } + do { + let data = try NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: false) + try set(data, + forKey: key, + useObjectiveCKeychain: true, + oldAccessGroup: Parse.configuration.keychainAccessGroup, + newAccessGroup: Parse.configuration.keychainAccessGroup) + return true + } catch { + return false + } + } + + func deleteAllObjectiveC() throws { + if !removeAllObjects(useObjectiveCKeychain: true) { + throw ParseError(code: .objectNotFound, message: "Could not delete all objects in Keychain") + } + } +} + #endif diff --git a/Tests/ParseSwiftTests/InitializeSDKTests.swift b/Tests/ParseSwiftTests/InitializeSDKTests.swift index 7824009d3..b6ecce658 100644 --- a/Tests/ParseSwiftTests/InitializeSDKTests.swift +++ b/Tests/ParseSwiftTests/InitializeSDKTests.swift @@ -51,7 +51,7 @@ class InitializeSDKTests: XCTestCase { try super.tearDownWithError() #if !os(Linux) && !os(Android) && !os(Windows) try KeychainStore.shared.deleteAll() - try KeychainStore.objectiveC?.deleteAll() + try KeychainStore.objectiveC?.deleteAllObjectiveC() try KeychainStore.old.deleteAll() URLSession.shared.configuration.urlCache?.removeAllCachedResponses() #endif @@ -534,7 +534,7 @@ class InitializeSDKTests: XCTestCase { return } let objcInstallationId = "helloWorld" - _ = objcParseKeychain.set(object: objcInstallationId, forKey: "installationId") + _ = objcParseKeychain.setObjectiveC(object: objcInstallationId, forKey: "installationId") guard let url = URL(string: "http://localhost:1337/1") else { XCTFail("Should create valid URL") @@ -563,7 +563,7 @@ class InitializeSDKTests: XCTestCase { return } let objcInstallationId = "helloWorld" - _ = objcParseKeychain.set(object: objcInstallationId, forKey: "installationId") + _ = objcParseKeychain.setObjectiveC(object: objcInstallationId, forKey: "installationId") guard let url = URL(string: "http://localhost:1337/1") else { XCTFail("Should create valid URL") @@ -607,15 +607,15 @@ class InitializeSDKTests: XCTestCase { return } let objcInstallationId = "helloWorld" - _ = objcParseKeychain.set(object: objcInstallationId, forKey: "installationId") + _ = objcParseKeychain.setObjectiveC(object: objcInstallationId, forKey: "installationId") - guard let retrievedInstallationId: String? = try objcParseKeychain.get(valueFor: "installationId") else { + guard let retrievedInstallationId: String? = objcParseKeychain.objectObjectiveC(forKey: "installationId") else { XCTFail("Should have unwrapped") return } XCTAssertEqual(retrievedInstallationId, objcInstallationId) XCTAssertNoThrow(try ParseSwift.deleteObjectiveCKeychain()) - let retrievedInstallationId2: String? = try objcParseKeychain.get(valueFor: "installationId") + let retrievedInstallationId2: String? = objcParseKeychain.objectObjectiveC(forKey: "installationId") XCTAssertNil(retrievedInstallationId2) //This is needed for tear down @@ -638,7 +638,7 @@ class InitializeSDKTests: XCTestCase { return } let objcInstallationId = "helloWorld" - _ = objcParseKeychain.set(object: objcInstallationId, forKey: "anotherPlace") + _ = objcParseKeychain.setObjectiveC(object: objcInstallationId, forKey: "anotherPlace") guard let url = URL(string: "http://localhost:1337/1") else { XCTFail("Should create valid URL") diff --git a/Tests/ParseSwiftTests/MigrateObjCSDKCombineTests.swift b/Tests/ParseSwiftTests/MigrateObjCSDKCombineTests.swift index 9835c664d..efeebe605 100644 --- a/Tests/ParseSwiftTests/MigrateObjCSDKCombineTests.swift +++ b/Tests/ParseSwiftTests/MigrateObjCSDKCombineTests.swift @@ -131,7 +131,7 @@ class MigrateObjCSDKCombineTests: XCTestCase { MockURLProtocol.removeAll() #if !os(Linux) && !os(Android) && !os(Windows) try KeychainStore.shared.deleteAll() - try KeychainStore.objectiveC?.deleteAll() + try KeychainStore.objectiveC?.deleteAllObjectiveC() #endif try ParseStorage.shared.deleteAll() } @@ -150,13 +150,13 @@ class MigrateObjCSDKCombineTests: XCTestCase { let currentUserDictionary2 = ["session_token": objcSessionToken2] let currentUserDictionary3 = ["sessionToken": objcSessionToken, "session_token": objcSessionToken2] - _ = objcParseKeychain.set(object: installationId, forKey: "installationId") + _ = objcParseKeychain.setObjectiveC(object: installationId, forKey: "installationId") if useBothTokens { - _ = objcParseKeychain.set(object: currentUserDictionary3, forKey: "currentUser") + _ = objcParseKeychain.setObjectiveC(object: currentUserDictionary3, forKey: "currentUser") } else if !useOldObjCToken { - _ = objcParseKeychain.set(object: currentUserDictionary, forKey: "currentUser") + _ = objcParseKeychain.setObjectiveC(object: currentUserDictionary, forKey: "currentUser") } else { - _ = objcParseKeychain.set(object: currentUserDictionary2, forKey: "currentUser") + _ = objcParseKeychain.setObjectiveC(object: currentUserDictionary2, forKey: "currentUser") } } diff --git a/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift b/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift index 0fab74f4f..1059ec9af 100644 --- a/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift +++ b/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift @@ -130,7 +130,7 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng MockURLProtocol.removeAll() #if !os(Linux) && !os(Android) && !os(Windows) try KeychainStore.shared.deleteAll() - try KeychainStore.objectiveC?.deleteAll() + try KeychainStore.objectiveC?.deleteAllObjectiveC() #endif try ParseStorage.shared.deleteAll() } @@ -149,13 +149,13 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng let currentUserDictionary2 = ["session_token": objcSessionToken2] let currentUserDictionary3 = ["sessionToken": objcSessionToken, "session_token": objcSessionToken2] - _ = objcParseKeychain.set(object: installationId, forKey: "installationId") + _ = objcParseKeychain.setObjectiveC(object: installationId, forKey: "installationId") if useBothTokens { - _ = objcParseKeychain.set(object: currentUserDictionary3, forKey: "currentUser") + _ = objcParseKeychain.setObjectiveC(object: currentUserDictionary3, forKey: "currentUser") } else if !useOldObjCToken { - _ = objcParseKeychain.set(object: currentUserDictionary, forKey: "currentUser") + _ = objcParseKeychain.setObjectiveC(object: currentUserDictionary, forKey: "currentUser") } else { - _ = objcParseKeychain.set(object: currentUserDictionary2, forKey: "currentUser") + _ = objcParseKeychain.setObjectiveC(object: currentUserDictionary2, forKey: "currentUser") } } From 3156408cbfacbe37d9f8bf85107df1cdc8482af4 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 11 Sep 2022 22:37:59 -0400 Subject: [PATCH 07/11] fix installation become --- .../Objects/ParseInstallation.swift | 6 +- .../MigrateObjCSDKCombineTests.swift | 181 ++++++++++-------- .../ParseSwiftTests/MigrateObjCSDKTests.swift | 70 ++++--- .../ParseInstallationAsyncTests.swift | 35 ++-- .../ParseInstallationCombineTests.swift | 96 ++++++---- 5 files changed, 237 insertions(+), 151 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 0c951a429..11bbc373e 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -325,7 +325,7 @@ public extension ParseInstallation { options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { - guard var currentInstallation = Self.current else { + guard let currentInstallation = Self.current else { let error = ParseError(code: .unknownError, message: "Current installation does not exist") callbackQueue.async { @@ -340,8 +340,8 @@ public extension ParseInstallation { } return } - currentInstallation.installationId = installationId - currentInstallation.fetch(options: options, callbackQueue: callbackQueue) { result in + let query = Self.query("installationId" == installationId) + query.first(options: options, callbackQueue: callbackQueue) { result in switch result { case .success(var updatedInstallation): if copyEntireInstallation { diff --git a/Tests/ParseSwiftTests/MigrateObjCSDKCombineTests.swift b/Tests/ParseSwiftTests/MigrateObjCSDKCombineTests.swift index efeebe605..627893959 100644 --- a/Tests/ParseSwiftTests/MigrateObjCSDKCombineTests.swift +++ b/Tests/ParseSwiftTests/MigrateObjCSDKCombineTests.swift @@ -484,17 +484,22 @@ class MigrateObjCSDKCombineTests: XCTestCase { setupObjcKeychainSDK(installationId: objcInstallationId) var installationOnServer = installation + installationOnServer.createdAt = installation.updatedAt installationOnServer.updatedAt = installation.updatedAt?.addingTimeInterval(+300) installationOnServer.customKey = "newValue" installationOnServer.installationId = objcInstallationId installationOnServer.channels = ["yo"] installationOnServer.deviceToken = "no" + let results = QueryResponse(results: [installationOnServer], count: 1) let encoded: Data! do { - encoded = try installationOnServer.getEncoder().encode(installationOnServer, skipKeys: .none) + encoded = try ParseCoding.jsonEncoder().encode(results) // Get dates in correct format from ParseDecoding strategy - installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + let encodedInstallation = try installationOnServer.getEncoder().encode(installationOnServer, + skipKeys: .none) + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, + from: encodedInstallation) } catch { XCTFail("Should encode/decode. Error \(error)") return @@ -506,47 +511,61 @@ class MigrateObjCSDKCombineTests: XCTestCase { let publisher = Installation.migrateFromObjCKeychainPublisher() .sink(receiveCompletion: { result in + // This will throw error because of QueryResponse cannot be used for save. if case let .failure(error) = result { - XCTFail(error.localizedDescription) + guard error.message.contains("updatedAt of nil") else { + XCTFail("Should have had proper error") + expectation1.fulfill() + return + } + } else { + XCTFail("Should have thrown error") + expectation1.fulfill() + return } - expectation1.fulfill() - - }, receiveValue: { fetched in - guard let currentInstallation = Installation.current else { - XCTFail("Should have current installation") - return - } - XCTAssertTrue(fetched.hasSameObjectId(as: currentInstallation)) - XCTAssertTrue(fetched.hasSameInstallationId(as: currentInstallation)) - XCTAssertTrue(fetched.hasSameObjectId(as: installationOnServer)) - XCTAssertTrue(fetched.hasSameInstallationId(as: installationOnServer)) - guard let fetchedCreatedAt = fetched.createdAt else { + guard let currentInstallation = Installation.current else { + XCTFail("Should have current installation") + expectation1.fulfill() + return + } + XCTAssertTrue(installationOnServer.hasSameObjectId(as: currentInstallation)) + XCTAssertTrue(installationOnServer.hasSameInstallationId(as: currentInstallation)) + guard let savedCreatedAt = currentInstallation.createdAt else { XCTFail("Should unwrap dates") + expectation1.fulfill() return - } - guard let originalCreatedAt = installationOnServer.createdAt else { + } + guard let originalCreatedAt = installationOnServer.createdAt else { XCTFail("Should unwrap dates") + expectation1.fulfill() return - } - XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) - XCTAssertEqual(fetched.channels, installationOnServer.channels) - XCTAssertEqual(fetched.deviceToken, installationOnServer.deviceToken) - - // Should be updated in memory - XCTAssertEqual(Installation.current?.installationId, self.objcInstallationId) - XCTAssertEqual(Installation.current?.customKey, installationOnServer.customKey) - XCTAssertEqual(Installation.current?.channels, installationOnServer.channels) - XCTAssertEqual(Installation.current?.deviceToken, installationOnServer.deviceToken) + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(currentInstallation.channels, installationOnServer.channels) + XCTAssertEqual(currentInstallation.deviceToken, installationOnServer.deviceToken) + + // Should be updated in memory + XCTAssertEqual(Installation.current?.installationId, self.objcInstallationId) + XCTAssertEqual(Installation.current?.customKey, installationOnServer.customKey) + XCTAssertEqual(Installation.current?.channels, installationOnServer.channels) + XCTAssertEqual(Installation.current?.deviceToken, installationOnServer.deviceToken) + + #if !os(Linux) && !os(Android) && !os(Windows) + // Should be updated in Keychain + guard let keychainInstallation: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, self.objcInstallationId) + XCTAssertEqual(keychainInstallation.currentInstallation?.channels, installationOnServer.channels) + XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) + #endif + expectation1.fulfill() - // Should be updated in Keychain - guard let keychainInstallation: CurrentInstallationContainer - = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { - XCTFail("Should get object from Keychain") - return - } - XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, self.objcInstallationId) - XCTAssertEqual(keychainInstallation.currentInstallation?.channels, installationOnServer.channels) - XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) + }, receiveValue: { _ in + XCTFail("Should have thrown error") expectation1.fulfill() }) publisher.store(in: &subscriptions) @@ -570,17 +589,22 @@ class MigrateObjCSDKCombineTests: XCTestCase { setupObjcKeychainSDK(installationId: objcInstallationId) var installationOnServer = installation + installationOnServer.createdAt = installation.updatedAt installationOnServer.updatedAt = installation.updatedAt?.addingTimeInterval(+300) installationOnServer.customKey = "newValue" installationOnServer.installationId = objcInstallationId installationOnServer.channels = ["yo"] installationOnServer.deviceToken = "no" + let results = QueryResponse(results: [installationOnServer], count: 1) let encoded: Data! do { - encoded = try installationOnServer.getEncoder().encode(installationOnServer, skipKeys: .none) + encoded = try ParseCoding.jsonEncoder().encode(results) // Get dates in correct format from ParseDecoding strategy - installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + let encodedInstallation = try installationOnServer.getEncoder().encode(installationOnServer, + skipKeys: .none) + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, + from: encodedInstallation) } catch { XCTFail("Should encode/decode. Error \(error)") return @@ -591,48 +615,55 @@ class MigrateObjCSDKCombineTests: XCTestCase { let publisher = Installation.migrateFromObjCKeychainPublisher(copyEntireInstallation: false) .sink(receiveCompletion: { result in - + // This will throw error because of QueryResponse cannot be used for save. if case let .failure(error) = result { - XCTFail(error.localizedDescription) + guard error.message.contains("updatedAt of nil") else { + XCTFail("Should have had proper error") + expectation1.fulfill() + return + } + } else { + XCTFail("Should have thrown error") + expectation1.fulfill() + return } - expectation1.fulfill() - - }, receiveValue: { fetched in - guard let currentInstallation = Installation.current else { - XCTFail("Should have current installation") - return - } - XCTAssertTrue(fetched.hasSameObjectId(as: currentInstallation)) - XCTAssertTrue(fetched.hasSameInstallationId(as: currentInstallation)) - XCTAssertTrue(fetched.hasSameObjectId(as: installationOnServer)) - XCTAssertTrue(fetched.hasSameInstallationId(as: installation)) - guard let fetchedCreatedAt = fetched.createdAt else { - XCTFail("Should unwrap dates") + guard let currentInstallation = Installation.current else { + XCTFail("Should have current installation") return - } - guard let originalCreatedAt = installationOnServer.createdAt else { - XCTFail("Should unwrap dates") + } + XCTAssertTrue(installationOnServer.hasSameObjectId(as: currentInstallation)) + XCTAssertTrue(installation.hasSameInstallationId(as: currentInstallation)) + guard let savedCreatedAt = currentInstallation.createdAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = installationOnServer.createdAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(currentInstallation.channels, installationOnServer.channels) + XCTAssertEqual(currentInstallation.deviceToken, installationOnServer.deviceToken) + + // Should be updated in memory + XCTAssertEqual(Installation.current?.installationId, installation.installationId) + XCTAssertNotEqual(Installation.current?.customKey, installationOnServer.customKey) + XCTAssertEqual(Installation.current?.channels, installationOnServer.channels) + XCTAssertEqual(Installation.current?.deviceToken, installationOnServer.deviceToken) + + // Should be updated in Keychain + guard let keychainInstallation: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { + XCTFail("Should get object from Keychain") return - } - XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) - XCTAssertEqual(fetched.channels, installationOnServer.channels) - XCTAssertEqual(fetched.deviceToken, installationOnServer.deviceToken) - - // Should be updated in memory - XCTAssertEqual(Installation.current?.installationId, installation.installationId) - XCTAssertNotEqual(Installation.current?.customKey, installationOnServer.customKey) - XCTAssertEqual(Installation.current?.channels, installationOnServer.channels) - XCTAssertEqual(Installation.current?.deviceToken, installationOnServer.deviceToken) + } + XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, installation.installationId) + XCTAssertEqual(keychainInstallation.currentInstallation?.channels, installationOnServer.channels) + XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) + expectation1.fulfill() - // Should be updated in Keychain - guard let keychainInstallation: CurrentInstallationContainer - = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { - XCTFail("Should get object from Keychain") - return - } - XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, installation.installationId) - XCTAssertEqual(keychainInstallation.currentInstallation?.channels, installationOnServer.channels) - XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) + }, receiveValue: { _ in + XCTFail("Should have thrown error") expectation1.fulfill() }) publisher.store(in: &subscriptions) diff --git a/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift b/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift index 1059ec9af..d5268620b 100644 --- a/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift +++ b/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift @@ -405,17 +405,22 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng setupObjcKeychainSDK(installationId: objcInstallationId) var installationOnServer = installation + installationOnServer.createdAt = installation.updatedAt installationOnServer.updatedAt = installation.updatedAt?.addingTimeInterval(+300) installationOnServer.customKey = "newValue" installationOnServer.installationId = objcInstallationId installationOnServer.channels = ["yo"] installationOnServer.deviceToken = "no" + let results = QueryResponse(results: [installationOnServer], count: 1) let encoded: Data! do { - encoded = try installationOnServer.getEncoder().encode(installationOnServer, skipKeys: .none) + encoded = try ParseCoding.jsonEncoder().encode(results) // Get dates in correct format from ParseDecoding strategy - installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + let encodedInstallation = try installationOnServer.getEncoder().encode(installationOnServer, + skipKeys: .none) + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, + from: encodedInstallation) } catch { XCTFail("Should encode/decode. Error \(error)") return @@ -424,16 +429,24 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } - let fetched = try await Installation.migrateFromObjCKeychain() + do { + // This will throw error because of QueryResponse cannot be used for save. + _ = try await Installation.migrateFromObjCKeychain() + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError, + parseError.message.contains("updatedAt of nil") else { + XCTFail("Should have had proper error") + return + } + } guard let currentInstallation = Installation.current else { XCTFail("Should have current installation") return } - XCTAssertTrue(fetched.hasSameObjectId(as: currentInstallation)) - XCTAssertTrue(fetched.hasSameInstallationId(as: currentInstallation)) - XCTAssertTrue(fetched.hasSameObjectId(as: installationOnServer)) - XCTAssertTrue(fetched.hasSameInstallationId(as: installationOnServer)) - guard let fetchedCreatedAt = fetched.createdAt else { + XCTAssertTrue(installationOnServer.hasSameObjectId(as: currentInstallation)) + XCTAssertTrue(installationOnServer.hasSameInstallationId(as: currentInstallation)) + guard let savedCreatedAt = currentInstallation.createdAt else { XCTFail("Should unwrap dates") return } @@ -441,9 +454,9 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTFail("Should unwrap dates") return } - XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) - XCTAssertEqual(fetched.channels, installationOnServer.channels) - XCTAssertEqual(fetched.deviceToken, installationOnServer.deviceToken) + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(currentInstallation.channels, installationOnServer.channels) + XCTAssertEqual(currentInstallation.deviceToken, installationOnServer.deviceToken) // Should be updated in memory XCTAssertEqual(Installation.current?.installationId, objcInstallationId) @@ -553,17 +566,22 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng setupObjcKeychainSDK(installationId: objcInstallationId) var installationOnServer = installation + installationOnServer.createdAt = installation.updatedAt installationOnServer.updatedAt = installation.updatedAt?.addingTimeInterval(+300) installationOnServer.customKey = "newValue" installationOnServer.installationId = objcInstallationId installationOnServer.channels = ["yo"] installationOnServer.deviceToken = "no" + let results = QueryResponse(results: [installationOnServer], count: 1) let encoded: Data! do { - encoded = try installationOnServer.getEncoder().encode(installationOnServer, skipKeys: .none) + encoded = try ParseCoding.jsonEncoder().encode(results) // Get dates in correct format from ParseDecoding strategy - installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + let encodedInstallation = try installationOnServer.getEncoder().encode(installationOnServer, + skipKeys: .none) + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, + from: encodedInstallation) } catch { XCTFail("Should encode/decode. Error \(error)") return @@ -572,16 +590,24 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } - let fetched = try await Installation.migrateFromObjCKeychain(copyEntireInstallation: false) + do { + // This will throw error because of QueryResponse cannot be used for save. + _ = try await Installation.migrateFromObjCKeychain(copyEntireInstallation: false) + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError, + parseError.message.contains("updatedAt of nil") else { + XCTFail("Should have had proper error") + return + } + } guard let currentInstallation = Installation.current else { XCTFail("Should have current installation") return } - XCTAssertTrue(fetched.hasSameObjectId(as: currentInstallation)) - XCTAssertTrue(fetched.hasSameInstallationId(as: currentInstallation)) - XCTAssertTrue(fetched.hasSameObjectId(as: installationOnServer)) - XCTAssertTrue(fetched.hasSameInstallationId(as: installation)) - guard let fetchedCreatedAt = fetched.createdAt else { + XCTAssertTrue(installationOnServer.hasSameObjectId(as: currentInstallation)) + XCTAssertTrue(installation.hasSameInstallationId(as: currentInstallation)) + guard let savedCreatedAt = currentInstallation.createdAt else { XCTFail("Should unwrap dates") return } @@ -589,9 +615,9 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTFail("Should unwrap dates") return } - XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) - XCTAssertEqual(fetched.channels, installationOnServer.channels) - XCTAssertEqual(fetched.deviceToken, installationOnServer.deviceToken) + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(currentInstallation.channels, installationOnServer.channels) + XCTAssertEqual(currentInstallation.deviceToken, installationOnServer.deviceToken) // Should be updated in memory XCTAssertEqual(Installation.current?.installationId, installation.installationId) diff --git a/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift b/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift index e174ba727..f322b5f66 100644 --- a/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift @@ -1133,17 +1133,22 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b XCTAssertEqual(savedObjectId, self.testInstallationObjectId) var installationOnServer = installation + installationOnServer.createdAt = installation.updatedAt installationOnServer.updatedAt = installation.updatedAt?.addingTimeInterval(+300) installationOnServer.customKey = "newValue" installationOnServer.installationId = "wowsers" installationOnServer.channels = ["yo"] installationOnServer.deviceToken = "no" + let results = QueryResponse(results: [installationOnServer], count: 1) let encoded: Data! do { - encoded = try installationOnServer.getEncoder().encode(installationOnServer, skipKeys: .none) + encoded = try ParseCoding.jsonEncoder().encode(results) // Get dates in correct format from ParseDecoding strategy - installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + let encodedInstallation = try installationOnServer.getEncoder().encode(installationOnServer, + skipKeys: .none) + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, + from: encodedInstallation) } catch { XCTFail("Should encode/decode. Error \(error)") return @@ -1152,16 +1157,24 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } - let fetched = try await Installation.become("wowsers") + do { + // This will throw error because of QueryResponse cannot be used for save. + _ = try await Installation.become("wowsers") + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError, + parseError.message.contains("updatedAt of nil") else { + XCTFail("Should have had proper error") + return + } + } guard let currentInstallation = Installation.current else { XCTFail("Should have current installation") return } - XCTAssertTrue(fetched.hasSameObjectId(as: currentInstallation)) - XCTAssertTrue(fetched.hasSameInstallationId(as: currentInstallation)) - XCTAssertTrue(fetched.hasSameObjectId(as: installationOnServer)) - XCTAssertTrue(fetched.hasSameInstallationId(as: installationOnServer)) - guard let fetchedCreatedAt = fetched.createdAt else { + XCTAssertTrue(installationOnServer.hasSameObjectId(as: currentInstallation)) + XCTAssertTrue(installationOnServer.hasSameInstallationId(as: currentInstallation)) + guard let savedCreatedAt = currentInstallation.createdAt else { XCTFail("Should unwrap dates") return } @@ -1169,9 +1182,9 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b XCTFail("Should unwrap dates") return } - XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) - XCTAssertEqual(fetched.channels, installationOnServer.channels) - XCTAssertEqual(fetched.deviceToken, installationOnServer.deviceToken) + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(currentInstallation.channels, installationOnServer.channels) + XCTAssertEqual(currentInstallation.deviceToken, installationOnServer.deviceToken) // Should be updated in memory XCTAssertEqual(Installation.current?.installationId, "wowsers") diff --git a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift index dedd5919a..6e2a63429 100644 --- a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift @@ -897,17 +897,22 @@ class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type XCTAssertEqual(savedObjectId, self.testInstallationObjectId) var installationOnServer = installation + installationOnServer.createdAt = installation.updatedAt installationOnServer.updatedAt = installation.updatedAt?.addingTimeInterval(+300) installationOnServer.customKey = "newValue" installationOnServer.installationId = "wowsers" installationOnServer.channels = ["yo"] installationOnServer.deviceToken = "no" + let results = QueryResponse(results: [installationOnServer], count: 1) let encoded: Data! do { - encoded = try installationOnServer.getEncoder().encode(installationOnServer, skipKeys: .none) + encoded = try ParseCoding.jsonEncoder().encode(results) // Get dates in correct format from ParseDecoding strategy - installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + let encodedInstallation = try installationOnServer.getEncoder().encode(installationOnServer, + skipKeys: .none) + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, + from: encodedInstallation) } catch { XCTFail("Should encode/decode. Error \(error)") return @@ -918,50 +923,61 @@ class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type let publisher = Installation.becomePublisher("wowsers") .sink(receiveCompletion: { result in - + // This will throw error because of QueryResponse cannot be used for save. if case let .failure(error) = result { - XCTFail(error.localizedDescription) + guard error.message.contains("updatedAt of nil") else { + XCTFail("Should have had proper error") + expectation1.fulfill() + return + } + } else { + XCTFail("Should have thrown error") + expectation1.fulfill() + return } - expectation1.fulfill() - - }, receiveValue: { fetched in - guard let currentInstallation = Installation.current else { - XCTFail("Should have current installation") - return - } - XCTAssertTrue(fetched.hasSameObjectId(as: currentInstallation)) - XCTAssertTrue(fetched.hasSameInstallationId(as: currentInstallation)) - XCTAssertTrue(fetched.hasSameObjectId(as: installationOnServer)) - XCTAssertTrue(fetched.hasSameInstallationId(as: installationOnServer)) - guard let fetchedCreatedAt = fetched.createdAt else { + guard let currentInstallation = Installation.current else { + XCTFail("Should have current installation") + expectation1.fulfill() + return + } + XCTAssertTrue(installationOnServer.hasSameObjectId(as: currentInstallation)) + XCTAssertTrue(installationOnServer.hasSameInstallationId(as: currentInstallation)) + guard let savedCreatedAt = currentInstallation.createdAt else { XCTFail("Should unwrap dates") + expectation1.fulfill() return - } - guard let originalCreatedAt = installationOnServer.createdAt else { + } + guard let originalCreatedAt = installationOnServer.createdAt else { XCTFail("Should unwrap dates") + expectation1.fulfill() return - } - XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) - XCTAssertEqual(fetched.channels, installationOnServer.channels) - XCTAssertEqual(fetched.deviceToken, installationOnServer.deviceToken) - - // Should be updated in memory - XCTAssertEqual(Installation.current?.installationId, "wowsers") - XCTAssertEqual(Installation.current?.customKey, installationOnServer.customKey) - XCTAssertEqual(Installation.current?.channels, installationOnServer.channels) - XCTAssertEqual(Installation.current?.deviceToken, installationOnServer.deviceToken) - - #if !os(Linux) && !os(Android) && !os(Windows) - // Should be updated in Keychain - guard let keychainInstallation: CurrentInstallationContainer - = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { - XCTFail("Should get object from Keychain") - return - } - XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, "wowsers") - XCTAssertEqual(keychainInstallation.currentInstallation?.channels, installationOnServer.channels) - XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) - #endif + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(currentInstallation.channels, installationOnServer.channels) + XCTAssertEqual(currentInstallation.deviceToken, installationOnServer.deviceToken) + + // Should be updated in memory + XCTAssertEqual(Installation.current?.installationId, "wowsers") + XCTAssertEqual(Installation.current?.customKey, installationOnServer.customKey) + XCTAssertEqual(Installation.current?.channels, installationOnServer.channels) + XCTAssertEqual(Installation.current?.deviceToken, installationOnServer.deviceToken) + + #if !os(Linux) && !os(Android) && !os(Windows) + // Should be updated in Keychain + guard let keychainInstallation: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, "wowsers") + XCTAssertEqual(keychainInstallation.currentInstallation?.channels, installationOnServer.channels) + XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) + #endif + expectation1.fulfill() + + }, receiveValue: { _ in + XCTFail("Should have thrown error") expectation1.fulfill() }) publisher.store(in: &subscriptions) From c926a7d6e89189a973a48076bee7de94891b2daa Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 11 Sep 2022 23:31:27 -0400 Subject: [PATCH 08/11] become uses fetch objectId --- .../Objects/ParseInstallation+async.swift | 9 +- .../Objects/ParseInstallation+combine.swift | 1 + .../Objects/ParseInstallation.swift | 15 +- .../MigrateObjCSDKCombineTests.swift | 219 ++++++------------ .../ParseSwiftTests/MigrateObjCSDKTests.swift | 109 ++------- .../ParseInstallationAsyncTests.swift | 65 ++++-- .../ParseInstallationCombineTests.swift | 121 +++++----- 7 files changed, 215 insertions(+), 324 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseInstallation+async.swift b/Sources/ParseSwift/Objects/ParseInstallation+async.swift index 7f27ab626..f55d7a7e3 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+async.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+async.swift @@ -122,11 +122,11 @@ public extension ParseInstallation { } /** - Copy the `ParseInstallation` *asynchronously* based on the `installationId`. + Copy the `ParseInstallation` *asynchronously* based on the `objectId`. On success, this saves the `ParseInstallation` to the keychain, so you can retrieve the current installation using *current*. - - parameter installationId: The **id** of the `ParseInstallation` to become. + - parameter objectId: The **id** of the `ParseInstallation` to become. - parameter copyEntireInstallation: When **true**, copies the entire `ParseInstallation`. When **false**, only the `channels` and `deviceToken` are copied; resulting in a new `ParseInstallation` for original `sessionToken`. Defaults to **true**. @@ -137,11 +137,11 @@ public extension ParseInstallation { - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer desires a different policy, it should be inserted in `options`. */ - @discardableResult static func become(_ installationId: String, + @discardableResult static func become(_ objectId: String, copyEntireInstallation: Bool = true, options: API.Options = []) async throws -> Self { try await withCheckedThrowingContinuation { continuation in - Self.become(installationId, + Self.become(objectId, copyEntireInstallation: copyEntireInstallation, options: options, completion: continuation.resume) @@ -349,6 +349,7 @@ public extension ParseInstallation { - warning: The latest **PFInstallation** from the Objective-C SDK should be saved to your Parse Server before calling this method. */ + @available(*, deprecated, message: "This does not work, use become() instead") @discardableResult static func migrateFromObjCKeychain(copyEntireInstallation: Bool = true, options: API.Options = []) async throws -> Self { try await withCheckedThrowingContinuation { continuation in diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift index 262a882c8..4345b2330 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -344,6 +344,7 @@ public extension ParseInstallation { Parse Server before calling this method. This method assumes **PFInstallation.installationId** is saved to the Keychain. If the **installationId** is not saved to the Keychain, this method will not work. */ + @available(*, deprecated, message: "This does not work, use become() instead") static func migrateFromObjCKeychainPublisher(copyEntireInstallation: Bool = true, options: API.Options = []) -> Future { Future { promise in diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 11bbc373e..b82b11603 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -305,11 +305,11 @@ public extension ParseInstallation { } /** - Copy the `ParseInstallation` *asynchronously* based on the `installationId`. + Copy the `ParseInstallation` *asynchronously* based on the `objectId`. On success, this saves the `ParseInstallation` to the keychain, so you can retrieve the current installation using *current*. - - parameter installationId: The **id** of the `ParseInstallation` to become. + - parameter objectId: The **id** of the `ParseInstallation` to become. - parameter copyEntireInstallation: When **true**, copies the entire `ParseInstallation`. When **false**, only the `channels` and `deviceToken` are copied; resulting in a new `ParseInstallation` for original `sessionToken`. Defaults to **true**. @@ -320,12 +320,12 @@ public extension ParseInstallation { - note: The default cache policy for this method is `.reloadIgnoringLocalCacheData`. If a developer desires a different policy, it should be inserted in `options`. */ - static func become(_ installationId: String, + static func become(_ objectId: String, copyEntireInstallation: Bool = true, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { - guard let currentInstallation = Self.current else { + guard var currentInstallation = Self.current else { let error = ParseError(code: .unknownError, message: "Current installation does not exist") callbackQueue.async { @@ -333,15 +333,15 @@ public extension ParseInstallation { } return } - guard currentInstallation.installationId != installationId else { + guard currentInstallation.objectId != objectId else { // If the installationId's are the same, assume successful replacement already occured. callbackQueue.async { completion(.success(currentInstallation)) } return } - let query = Self.query("installationId" == installationId) - query.first(options: options, callbackQueue: callbackQueue) { result in + currentInstallation.objectId = objectId + currentInstallation.fetch(options: options, callbackQueue: callbackQueue) { result in switch result { case .success(var updatedInstallation): if copyEntireInstallation { @@ -1607,6 +1607,7 @@ public extension ParseInstallation { is saved to the Keychain. If the **installationId** is not saved to the Keychain, this method will not work. */ + @available(*, deprecated, message: "This does not work, use become() instead") static func migrateFromObjCKeychain(copyEntireInstallation: Bool = true, options: API.Options = [], callbackQueue: DispatchQueue = .main, diff --git a/Tests/ParseSwiftTests/MigrateObjCSDKCombineTests.swift b/Tests/ParseSwiftTests/MigrateObjCSDKCombineTests.swift index 627893959..24d224529 100644 --- a/Tests/ParseSwiftTests/MigrateObjCSDKCombineTests.swift +++ b/Tests/ParseSwiftTests/MigrateObjCSDKCombineTests.swift @@ -491,15 +491,12 @@ class MigrateObjCSDKCombineTests: XCTestCase { installationOnServer.channels = ["yo"] installationOnServer.deviceToken = "no" - let results = QueryResponse(results: [installationOnServer], count: 1) let encoded: Data! do { - encoded = try ParseCoding.jsonEncoder().encode(results) + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) // Get dates in correct format from ParseDecoding strategy - let encodedInstallation = try installationOnServer.getEncoder().encode(installationOnServer, - skipKeys: .none) installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, - from: encodedInstallation) + from: encoded) } catch { XCTFail("Should encode/decode. Error \(error)") return @@ -511,61 +508,53 @@ class MigrateObjCSDKCombineTests: XCTestCase { let publisher = Installation.migrateFromObjCKeychainPublisher() .sink(receiveCompletion: { result in - // This will throw error because of QueryResponse cannot be used for save. if case let .failure(error) = result { - guard error.message.contains("updatedAt of nil") else { - XCTFail("Should have had proper error") - expectation1.fulfill() - return - } - } else { - XCTFail("Should have thrown error") - expectation1.fulfill() - return - } - guard let currentInstallation = Installation.current else { - XCTFail("Should have current installation") - expectation1.fulfill() - return - } - XCTAssertTrue(installationOnServer.hasSameObjectId(as: currentInstallation)) - XCTAssertTrue(installationOnServer.hasSameInstallationId(as: currentInstallation)) - guard let savedCreatedAt = currentInstallation.createdAt else { - XCTFail("Should unwrap dates") - expectation1.fulfill() - return - } - guard let originalCreatedAt = installationOnServer.createdAt else { - XCTFail("Should unwrap dates") - expectation1.fulfill() - return - } - XCTAssertEqual(savedCreatedAt, originalCreatedAt) - XCTAssertEqual(currentInstallation.channels, installationOnServer.channels) - XCTAssertEqual(currentInstallation.deviceToken, installationOnServer.deviceToken) - - // Should be updated in memory - XCTAssertEqual(Installation.current?.installationId, self.objcInstallationId) - XCTAssertEqual(Installation.current?.customKey, installationOnServer.customKey) - XCTAssertEqual(Installation.current?.channels, installationOnServer.channels) - XCTAssertEqual(Installation.current?.deviceToken, installationOnServer.deviceToken) - - #if !os(Linux) && !os(Android) && !os(Windows) - // Should be updated in Keychain - guard let keychainInstallation: CurrentInstallationContainer - = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { - XCTFail("Should get object from Keychain") - expectation1.fulfill() - return + XCTFail(error.localizedDescription) } - XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, self.objcInstallationId) - XCTAssertEqual(keychainInstallation.currentInstallation?.channels, installationOnServer.channels) - XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) - #endif expectation1.fulfill() - }, receiveValue: { _ in - XCTFail("Should have thrown error") + }, receiveValue: { saved in + guard let currentInstallation = Installation.current else { + XCTFail("Should have current installation") + expectation1.fulfill() + return + } + XCTAssertTrue(installationOnServer.hasSameObjectId(as: saved)) + XCTAssertTrue(installationOnServer.hasSameInstallationId(as: saved)) + XCTAssertTrue(installationOnServer.hasSameObjectId(as: currentInstallation)) + XCTAssertTrue(installationOnServer.hasSameInstallationId(as: currentInstallation)) + guard let savedCreatedAt = saved.createdAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalCreatedAt = installationOnServer.createdAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(saved.channels, installationOnServer.channels) + XCTAssertEqual(saved.deviceToken, installationOnServer.deviceToken) + + // Should be updated in memory + XCTAssertEqual(Installation.current?.installationId, self.objcInstallationId) + XCTAssertEqual(Installation.current?.customKey, installationOnServer.customKey) + XCTAssertEqual(Installation.current?.channels, installationOnServer.channels) + XCTAssertEqual(Installation.current?.deviceToken, installationOnServer.deviceToken) + + #if !os(Linux) && !os(Android) && !os(Windows) + // Should be updated in Keychain + guard let keychainInstallation: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, self.objcInstallationId) + XCTAssertEqual(keychainInstallation.currentInstallation?.channels, installationOnServer.channels) + XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) + #endif expectation1.fulfill() }) publisher.store(in: &subscriptions) @@ -596,15 +585,12 @@ class MigrateObjCSDKCombineTests: XCTestCase { installationOnServer.channels = ["yo"] installationOnServer.deviceToken = "no" - let results = QueryResponse(results: [installationOnServer], count: 1) let encoded: Data! do { - encoded = try ParseCoding.jsonEncoder().encode(results) + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) // Get dates in correct format from ParseDecoding strategy - let encodedInstallation = try installationOnServer.getEncoder().encode(installationOnServer, - skipKeys: .none) installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, - from: encodedInstallation) + from: encoded) } catch { XCTFail("Should encode/decode. Error \(error)") return @@ -615,108 +601,37 @@ class MigrateObjCSDKCombineTests: XCTestCase { let publisher = Installation.migrateFromObjCKeychainPublisher(copyEntireInstallation: false) .sink(receiveCompletion: { result in - // This will throw error because of QueryResponse cannot be used for save. - if case let .failure(error) = result { - guard error.message.contains("updatedAt of nil") else { - XCTFail("Should have had proper error") - expectation1.fulfill() - return - } - } else { - XCTFail("Should have thrown error") - expectation1.fulfill() - return - } - guard let currentInstallation = Installation.current else { - XCTFail("Should have current installation") - return - } - XCTAssertTrue(installationOnServer.hasSameObjectId(as: currentInstallation)) - XCTAssertTrue(installation.hasSameInstallationId(as: currentInstallation)) - guard let savedCreatedAt = currentInstallation.createdAt else { - XCTFail("Should unwrap dates") - return - } - guard let originalCreatedAt = installationOnServer.createdAt else { - XCTFail("Should unwrap dates") - return - } - XCTAssertEqual(savedCreatedAt, originalCreatedAt) - XCTAssertEqual(currentInstallation.channels, installationOnServer.channels) - XCTAssertEqual(currentInstallation.deviceToken, installationOnServer.deviceToken) - - // Should be updated in memory - XCTAssertEqual(Installation.current?.installationId, installation.installationId) - XCTAssertNotEqual(Installation.current?.customKey, installationOnServer.customKey) - XCTAssertEqual(Installation.current?.channels, installationOnServer.channels) - XCTAssertEqual(Installation.current?.deviceToken, installationOnServer.deviceToken) - - // Should be updated in Keychain - guard let keychainInstallation: CurrentInstallationContainer - = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { - XCTFail("Should get object from Keychain") - return - } - XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, installation.installationId) - XCTAssertEqual(keychainInstallation.currentInstallation?.channels, installationOnServer.channels) - XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) - expectation1.fulfill() - - }, receiveValue: { _ in - XCTFail("Should have thrown error") - expectation1.fulfill() - }) - publisher.store(in: &subscriptions) - wait(for: [expectation1], timeout: 20.0) - } - - func testMigrateInstallationAlreadyMigrated() throws { - var subscriptions = Set() - let expectation1 = XCTestExpectation(description: "Migrate Installation") - - try saveCurrentInstallation() - MockURLProtocol.removeAll() - - guard let installation = Installation.current, - let savedObjectId = installation.objectId, - let savedInstallationId = installation.installationId else { - XCTFail("Should unwrap") - return - } - XCTAssertEqual(savedObjectId, self.testInstallationObjectId) - - setupObjcKeychainSDK(installationId: savedInstallationId) - - let publisher = Installation.migrateFromObjCKeychainPublisher() - .sink(receiveCompletion: { result in - if case let .failure(error) = result { XCTFail(error.localizedDescription) } expectation1.fulfill() - }, receiveValue: { fetched in + }, receiveValue: { saved in guard let currentInstallation = Installation.current else { XCTFail("Should have current installation") return } - XCTAssertTrue(fetched.hasSameObjectId(as: currentInstallation)) - XCTAssertTrue(fetched.hasSameInstallationId(as: currentInstallation)) - XCTAssertTrue(fetched.hasSameObjectId(as: installation)) - XCTAssertTrue(fetched.hasSameInstallationId(as: installation)) - guard let fetchedCreatedAt = fetched.createdAt else { - XCTFail("Should unwrap dates") - return + XCTAssertTrue(installationOnServer.hasSameObjectId(as: saved)) + XCTAssertTrue(installation.hasSameInstallationId(as: saved)) + XCTAssertTrue(installationOnServer.hasSameObjectId(as: currentInstallation)) + XCTAssertTrue(installation.hasSameInstallationId(as: currentInstallation)) + guard let savedCreatedAt = saved.createdAt else { + XCTFail("Should unwrap dates") + return } - guard let originalCreatedAt = installation.createdAt else { - XCTFail("Should unwrap dates") - return + guard let originalCreatedAt = installationOnServer.createdAt else { + XCTFail("Should unwrap dates") + return } - XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(saved.channels, installationOnServer.channels) + XCTAssertEqual(saved.deviceToken, installationOnServer.deviceToken) // Should be updated in memory - XCTAssertEqual(Installation.current?.installationId, savedInstallationId) - XCTAssertEqual(Installation.current?.customKey, installation.customKey) + XCTAssertEqual(Installation.current?.installationId, installation.installationId) + XCTAssertNotEqual(Installation.current?.customKey, installationOnServer.customKey) + XCTAssertEqual(Installation.current?.channels, installationOnServer.channels) + XCTAssertEqual(Installation.current?.deviceToken, installationOnServer.deviceToken) // Should be updated in Keychain guard let keychainInstallation: CurrentInstallationContainer @@ -724,7 +639,9 @@ class MigrateObjCSDKCombineTests: XCTestCase { XCTFail("Should get object from Keychain") return } - XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, savedInstallationId) + XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, installation.installationId) + XCTAssertEqual(keychainInstallation.currentInstallation?.channels, installationOnServer.channels) + XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) expectation1.fulfill() }) publisher.store(in: &subscriptions) diff --git a/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift b/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift index d5268620b..ad139ee5a 100644 --- a/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift +++ b/Tests/ParseSwiftTests/MigrateObjCSDKTests.swift @@ -412,15 +412,12 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng installationOnServer.channels = ["yo"] installationOnServer.deviceToken = "no" - let results = QueryResponse(results: [installationOnServer], count: 1) let encoded: Data! do { - encoded = try ParseCoding.jsonEncoder().encode(results) + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) // Get dates in correct format from ParseDecoding strategy - let encodedInstallation = try installationOnServer.getEncoder().encode(installationOnServer, - skipKeys: .none) installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, - from: encodedInstallation) + from: encoded) } catch { XCTFail("Should encode/decode. Error \(error)") return @@ -429,34 +426,26 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } - do { - // This will throw error because of QueryResponse cannot be used for save. - _ = try await Installation.migrateFromObjCKeychain() - XCTFail("Should have thrown error") - } catch { - guard let parseError = error as? ParseError, - parseError.message.contains("updatedAt of nil") else { - XCTFail("Should have had proper error") - return - } - } + let saved = try await Installation.migrateFromObjCKeychain() guard let currentInstallation = Installation.current else { XCTFail("Should have current installation") return } + XCTAssertTrue(installationOnServer.hasSameObjectId(as: saved)) + XCTAssertTrue(installationOnServer.hasSameInstallationId(as: saved)) XCTAssertTrue(installationOnServer.hasSameObjectId(as: currentInstallation)) XCTAssertTrue(installationOnServer.hasSameInstallationId(as: currentInstallation)) guard let savedCreatedAt = currentInstallation.createdAt else { XCTFail("Should unwrap dates") return } - guard let originalCreatedAt = installationOnServer.createdAt else { + guard let originalCreatedAt = saved.createdAt else { XCTFail("Should unwrap dates") return } XCTAssertEqual(savedCreatedAt, originalCreatedAt) - XCTAssertEqual(currentInstallation.channels, installationOnServer.channels) - XCTAssertEqual(currentInstallation.deviceToken, installationOnServer.deviceToken) + XCTAssertEqual(saved.channels, installationOnServer.channels) + XCTAssertEqual(saved.deviceToken, installationOnServer.deviceToken) // Should be updated in memory XCTAssertEqual(Installation.current?.installationId, objcInstallationId) @@ -573,15 +562,12 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng installationOnServer.channels = ["yo"] installationOnServer.deviceToken = "no" - let results = QueryResponse(results: [installationOnServer], count: 1) let encoded: Data! do { - encoded = try ParseCoding.jsonEncoder().encode(results) + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) // Get dates in correct format from ParseDecoding strategy - let encodedInstallation = try installationOnServer.getEncoder().encode(installationOnServer, - skipKeys: .none) installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, - from: encodedInstallation) + from: encoded) } catch { XCTFail("Should encode/decode. Error \(error)") return @@ -590,34 +576,26 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } - do { - // This will throw error because of QueryResponse cannot be used for save. - _ = try await Installation.migrateFromObjCKeychain(copyEntireInstallation: false) - XCTFail("Should have thrown error") - } catch { - guard let parseError = error as? ParseError, - parseError.message.contains("updatedAt of nil") else { - XCTFail("Should have had proper error") - return - } - } + let saved = try await Installation.migrateFromObjCKeychain(copyEntireInstallation: false) guard let currentInstallation = Installation.current else { XCTFail("Should have current installation") return } + XCTAssertTrue(installationOnServer.hasSameObjectId(as: saved)) + XCTAssertTrue(installation.hasSameInstallationId(as: saved)) XCTAssertTrue(installationOnServer.hasSameObjectId(as: currentInstallation)) XCTAssertTrue(installation.hasSameInstallationId(as: currentInstallation)) guard let savedCreatedAt = currentInstallation.createdAt else { - XCTFail("Should unwrap dates") - return + XCTFail("Should unwrap dates") + return } - guard let originalCreatedAt = installationOnServer.createdAt else { - XCTFail("Should unwrap dates") - return + guard let originalCreatedAt = saved.createdAt else { + XCTFail("Should unwrap dates") + return } XCTAssertEqual(savedCreatedAt, originalCreatedAt) - XCTAssertEqual(currentInstallation.channels, installationOnServer.channels) - XCTAssertEqual(currentInstallation.deviceToken, installationOnServer.deviceToken) + XCTAssertEqual(saved.channels, installationOnServer.channels) + XCTAssertEqual(saved.deviceToken, installationOnServer.deviceToken) // Should be updated in memory XCTAssertEqual(Installation.current?.installationId, installation.installationId) @@ -636,53 +614,6 @@ class MigrateObjCSDKTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) } - @MainActor - func testMigrateInstallationAlreadyMigrated() async throws { - try saveCurrentInstallation() - MockURLProtocol.removeAll() - - guard let installation = Installation.current, - let savedObjectId = installation.objectId, - let savedInstallationId = installation.installationId else { - XCTFail("Should unwrap") - return - } - XCTAssertEqual(savedObjectId, self.testInstallationObjectId) - - setupObjcKeychainSDK(installationId: savedInstallationId) - - let fetched = try await Installation.migrateFromObjCKeychain() - guard let currentInstallation = Installation.current else { - XCTFail("Should have current installation") - return - } - XCTAssertTrue(fetched.hasSameObjectId(as: currentInstallation)) - XCTAssertTrue(fetched.hasSameInstallationId(as: currentInstallation)) - XCTAssertTrue(fetched.hasSameObjectId(as: installation)) - XCTAssertTrue(fetched.hasSameInstallationId(as: installation)) - guard let fetchedCreatedAt = fetched.createdAt else { - XCTFail("Should unwrap dates") - return - } - guard let originalCreatedAt = installation.createdAt else { - XCTFail("Should unwrap dates") - return - } - XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) - - // Should be updated in memory - XCTAssertEqual(Installation.current?.installationId, savedInstallationId) - XCTAssertEqual(Installation.current?.customKey, installation.customKey) - - // Should be updated in Keychain - guard let keychainInstallation: CurrentInstallationContainer - = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { - XCTFail("Should get object from Keychain") - return - } - XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, savedInstallationId) - } - @MainActor func testDeleteObjCKeychain() async throws { try saveCurrentInstallation() diff --git a/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift b/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift index f322b5f66..331d33643 100644 --- a/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift @@ -1140,15 +1140,12 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b installationOnServer.channels = ["yo"] installationOnServer.deviceToken = "no" - let results = QueryResponse(results: [installationOnServer], count: 1) let encoded: Data! do { - encoded = try ParseCoding.jsonEncoder().encode(results) + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) // Get dates in correct format from ParseDecoding strategy - let encodedInstallation = try installationOnServer.getEncoder().encode(installationOnServer, - skipKeys: .none) installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, - from: encodedInstallation) + from: encoded) } catch { XCTFail("Should encode/decode. Error \(error)") return @@ -1157,24 +1154,16 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } - do { - // This will throw error because of QueryResponse cannot be used for save. - _ = try await Installation.become("wowsers") - XCTFail("Should have thrown error") - } catch { - guard let parseError = error as? ParseError, - parseError.message.contains("updatedAt of nil") else { - XCTFail("Should have had proper error") - return - } - } + let saved = try await Installation.become("wowsers") guard let currentInstallation = Installation.current else { XCTFail("Should have current installation") return } + XCTAssertTrue(installationOnServer.hasSameObjectId(as: saved)) + XCTAssertTrue(installationOnServer.hasSameInstallationId(as: saved)) XCTAssertTrue(installationOnServer.hasSameObjectId(as: currentInstallation)) XCTAssertTrue(installationOnServer.hasSameInstallationId(as: currentInstallation)) - guard let savedCreatedAt = currentInstallation.createdAt else { + guard let savedCreatedAt = saved.createdAt else { XCTFail("Should unwrap dates") return } @@ -1183,8 +1172,8 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b return } XCTAssertEqual(savedCreatedAt, originalCreatedAt) - XCTAssertEqual(currentInstallation.channels, installationOnServer.channels) - XCTAssertEqual(currentInstallation.deviceToken, installationOnServer.deviceToken) + XCTAssertEqual(saved.channels, installationOnServer.channels) + XCTAssertEqual(saved.deviceToken, installationOnServer.deviceToken) // Should be updated in memory XCTAssertEqual(Installation.current?.installationId, "wowsers") @@ -1204,5 +1193,43 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) #endif } + + @MainActor + func testBecomeSameObjectId() async throws { + try saveCurrentInstallation() + MockURLProtocol.removeAll() + + guard let installation = Installation.current, + let savedObjectId = installation.objectId else { + XCTFail("Should unwrap") + return + } + XCTAssertEqual(savedObjectId, self.testInstallationObjectId) + + let saved = try await Installation.become(testInstallationObjectId) + guard let currentInstallation = Installation.current else { + XCTFail("Should have current installation") + return + } + XCTAssertEqual(saved, currentInstallation) + } + + @MainActor + func testBecomeMissingObjectId() async throws { + try ParseStorage.shared.delete(valueFor: ParseStorage.Keys.currentInstallation) + try KeychainStore.shared.delete(valueFor: ParseStorage.Keys.currentInstallation) + Installation.currentContainer.currentInstallation = nil + + do { + _ = try await Installation.become(testInstallationObjectId) + XCTFail("Should have thrown error") + } catch { + guard let parseError = error as? ParseError else { + XCTFail("Should have casted") + return + } + XCTAssertTrue(parseError.message.contains("does not exist")) + } + } } #endif diff --git a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift index 6e2a63429..271a0b0c8 100644 --- a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift @@ -884,7 +884,7 @@ class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type func testBecome() throws { var subscriptions = Set() - let expectation1 = XCTestExpectation(description: "Migrate Installation") + let expectation1 = XCTestExpectation(description: "Become Installation") try saveCurrentInstallation() MockURLProtocol.removeAll() @@ -904,15 +904,12 @@ class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type installationOnServer.channels = ["yo"] installationOnServer.deviceToken = "no" - let results = QueryResponse(results: [installationOnServer], count: 1) let encoded: Data! do { - encoded = try ParseCoding.jsonEncoder().encode(results) + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) // Get dates in correct format from ParseDecoding strategy - let encodedInstallation = try installationOnServer.getEncoder().encode(installationOnServer, - skipKeys: .none) installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, - from: encodedInstallation) + from: encoded) } catch { XCTFail("Should encode/decode. Error \(error)") return @@ -923,57 +920,73 @@ class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type let publisher = Installation.becomePublisher("wowsers") .sink(receiveCompletion: { result in - // This will throw error because of QueryResponse cannot be used for save. if case let .failure(error) = result { - guard error.message.contains("updatedAt of nil") else { - XCTFail("Should have had proper error") - expectation1.fulfill() - return - } - } else { - XCTFail("Should have thrown error") - expectation1.fulfill() - return - } - guard let currentInstallation = Installation.current else { - XCTFail("Should have current installation") - expectation1.fulfill() - return - } - XCTAssertTrue(installationOnServer.hasSameObjectId(as: currentInstallation)) - XCTAssertTrue(installationOnServer.hasSameInstallationId(as: currentInstallation)) - guard let savedCreatedAt = currentInstallation.createdAt else { - XCTFail("Should unwrap dates") - expectation1.fulfill() - return - } - guard let originalCreatedAt = installationOnServer.createdAt else { - XCTFail("Should unwrap dates") - expectation1.fulfill() - return + XCTFail(error.localizedDescription) } - XCTAssertEqual(savedCreatedAt, originalCreatedAt) - XCTAssertEqual(currentInstallation.channels, installationOnServer.channels) - XCTAssertEqual(currentInstallation.deviceToken, installationOnServer.deviceToken) - - // Should be updated in memory - XCTAssertEqual(Installation.current?.installationId, "wowsers") - XCTAssertEqual(Installation.current?.customKey, installationOnServer.customKey) - XCTAssertEqual(Installation.current?.channels, installationOnServer.channels) - XCTAssertEqual(Installation.current?.deviceToken, installationOnServer.deviceToken) - - #if !os(Linux) && !os(Android) && !os(Windows) - // Should be updated in Keychain - guard let keychainInstallation: CurrentInstallationContainer - = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { - XCTFail("Should get object from Keychain") - expectation1.fulfill() - return + expectation1.fulfill() + + }, receiveValue: { saved in + guard let currentInstallation = Installation.current else { + XCTFail("Should have current installation") + expectation1.fulfill() + return + } + XCTAssertTrue(installationOnServer.hasSameObjectId(as: saved)) + XCTAssertTrue(installationOnServer.hasSameInstallationId(as: saved)) + XCTAssertTrue(installationOnServer.hasSameObjectId(as: currentInstallation)) + XCTAssertTrue(installationOnServer.hasSameInstallationId(as: currentInstallation)) + guard let savedCreatedAt = saved.createdAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalCreatedAt = installationOnServer.createdAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(saved.channels, installationOnServer.channels) + XCTAssertEqual(saved.deviceToken, installationOnServer.deviceToken) + + // Should be updated in memory + XCTAssertEqual(Installation.current?.installationId, "wowsers") + XCTAssertEqual(Installation.current?.customKey, installationOnServer.customKey) + XCTAssertEqual(Installation.current?.channels, installationOnServer.channels) + XCTAssertEqual(Installation.current?.deviceToken, installationOnServer.deviceToken) + + #if !os(Linux) && !os(Android) && !os(Windows) + // Should be updated in Keychain + guard let keychainInstallation: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, "wowsers") + XCTAssertEqual(keychainInstallation.currentInstallation?.channels, installationOnServer.channels) + XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) + #endif + expectation1.fulfill() + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testBecomeMissingObjectId() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Become Installation") + try ParseStorage.shared.delete(valueFor: ParseStorage.Keys.currentInstallation) + try KeychainStore.shared.delete(valueFor: ParseStorage.Keys.currentInstallation) + Installation.currentContainer.currentInstallation = nil + + let publisher = Installation.becomePublisher("wowsers") + .sink(receiveCompletion: { result in + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("does not exist")) + } else { + XCTFail("Should have error") } - XCTAssertEqual(keychainInstallation.currentInstallation?.installationId, "wowsers") - XCTAssertEqual(keychainInstallation.currentInstallation?.channels, installationOnServer.channels) - XCTAssertEqual(keychainInstallation.currentInstallation?.deviceToken, installationOnServer.deviceToken) - #endif expectation1.fulfill() }, receiveValue: { _ in From d09e43a6f54936cdfd5504bf2ca1b7f5da5bf48f Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Sun, 11 Sep 2022 23:45:27 -0400 Subject: [PATCH 09/11] fix linux tests --- Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift | 2 ++ Tests/ParseSwiftTests/ParseInstallationCombineTests.swift | 2 ++ 2 files changed, 4 insertions(+) diff --git a/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift b/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift index 331d33643..87edbb165 100644 --- a/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationAsyncTests.swift @@ -1217,7 +1217,9 @@ class ParseInstallationAsyncTests: XCTestCase { // swiftlint:disable:this type_b @MainActor func testBecomeMissingObjectId() async throws { try ParseStorage.shared.delete(valueFor: ParseStorage.Keys.currentInstallation) + #if !os(Linux) && !os(Android) && !os(Windows) try KeychainStore.shared.delete(valueFor: ParseStorage.Keys.currentInstallation) + #endif Installation.currentContainer.currentInstallation = nil do { diff --git a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift index 271a0b0c8..b2c8ef909 100644 --- a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift @@ -977,7 +977,9 @@ class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type var subscriptions = Set() let expectation1 = XCTestExpectation(description: "Become Installation") try ParseStorage.shared.delete(valueFor: ParseStorage.Keys.currentInstallation) + #if !os(Linux) && !os(Android) && !os(Windows) try KeychainStore.shared.delete(valueFor: ParseStorage.Keys.currentInstallation) + #endif Installation.currentContainer.currentInstallation = nil let publisher = Installation.becomePublisher("wowsers") From eab5043b6ff041fe682d176568268b84be8cc6ef Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Mon, 12 Sep 2022 00:07:38 -0400 Subject: [PATCH 10/11] add test for keychain --- .../ParseSwiftTests/KeychainStoreTests.swift | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Tests/ParseSwiftTests/KeychainStoreTests.swift b/Tests/ParseSwiftTests/KeychainStoreTests.swift index 14122c79f..cf4da7c0f 100644 --- a/Tests/ParseSwiftTests/KeychainStoreTests.swift +++ b/Tests/ParseSwiftTests/KeychainStoreTests.swift @@ -254,5 +254,25 @@ class KeychainStoreTests: XCTestCase { XCTAssertEqual(query[kSecAttrAccessible as String] as? String, kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly as String) } + + func testSetObjectiveC() throws { + // Set keychain the way objc sets keychain + guard let objcParseKeychain = KeychainStore.objectiveC else { + XCTFail("Should have unwrapped") + return + } + let objcInstallationId = "helloWorld" + _ = objcParseKeychain.setObjectiveC(object: objcInstallationId, forKey: "installationId") + + guard let retrievedValue: String = objcParseKeychain.objectObjectiveC(forKey: "installationId") else { + XCTFail("Should have casted") + return + } + XCTAssertEqual(retrievedValue, objcInstallationId) + let newInstallationId: String? = nil + _ = objcParseKeychain.setObjectiveC(object: newInstallationId, forKey: "installationId") + let retrievedValue2: String? = objcParseKeychain.objectObjectiveC(forKey: "installationId") + XCTAssertNil(retrievedValue2) + } } #endif From 2dc94c738a986fe97c4f30c7d7bd45f9acaf19f1 Mon Sep 17 00:00:00 2001 From: Corey Baker Date: Mon, 12 Sep 2022 00:10:58 -0400 Subject: [PATCH 11/11] delete keychain after each test --- Tests/ParseSwiftTests/KeychainStoreTests.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Tests/ParseSwiftTests/KeychainStoreTests.swift b/Tests/ParseSwiftTests/KeychainStoreTests.swift index cf4da7c0f..41d588534 100644 --- a/Tests/ParseSwiftTests/KeychainStoreTests.swift +++ b/Tests/ParseSwiftTests/KeychainStoreTests.swift @@ -31,6 +31,7 @@ class KeychainStoreTests: XCTestCase { _ = testStore.removeAllObjects() #if !os(Linux) && !os(Android) && !os(Windows) try KeychainStore.shared.deleteAll() + try? KeychainStore.objectiveC?.deleteAllObjectiveC() #endif try ParseStorage.shared.deleteAll() }