diff --git a/.codecov.yml b/.codecov.yml index 10e2c3784..709600189 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -9,6 +9,6 @@ coverage: changes: false project: default: - target: 71 + target: 72 comment: require_changes: true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e2dedfed8..010cfa3d9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,6 +107,4 @@ jobs: steps: - uses: actions/checkout@v2 - name: Carthage - run: carthage build --no-skip-current - env: - DEVELOPER_DIR: ${{ env.CI_XCODE_VER }} + run: ./carthage.sh build --no-skip-current diff --git a/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift index 669aeada1..0aa83bae3 100644 --- a/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/2 - Finding Objects.xcplaygroundpage/Contents.swift @@ -52,7 +52,7 @@ results.forEach { (score) in // Query first asynchronously (preferred way) - Performs work on background // queue and returns to designated on designated callbackQueue. // If no callbackQueue is specified it returns to main queue -query.first(callbackQueue: .main) { results in +query.first { results in switch results { case .success(let score): diff --git a/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift index d3a72b8a9..1e31ca843 100644 --- a/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift @@ -40,7 +40,7 @@ User.signup(username: "hello", password: "world") { results in if !currentUser.hasSameObjectId(as: user) { assertionFailure("Error: these two objects should match") } else { - print("Succesfully signed up user \(user)") + print("Successfully signed up user \(user)") } case .failure(let error): diff --git a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift index ae779f88a..42b9eb0f0 100644 --- a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift @@ -33,7 +33,7 @@ User.current?.save { results in switch results { case .success(let updatedUser): - print("Succesufully save myCustomKey to ParseServer: \(updatedUser)") + print("Successfully save myCustomKey to ParseServer: \(updatedUser)") case .failure(let error): assertionFailure("Failed to update user: \(error)") } @@ -42,7 +42,7 @@ User.current?.save { results in //: Logging out - synchronously do { try User.logout() - print("Succesfully logged out") + print("Successfully logged out") } catch let error { assertionFailure("Error logging out: \(error)") } @@ -61,7 +61,7 @@ User.login(username: "hello", password: "world") { results in return } assert(currentUser.hasSameObjectId(as: user)) - print("Succesfully logged in as user: \(user)") + print("Successfully logged in as user: \(user)") case .failure(let error): assertionFailure("Error logging in: \(error)") @@ -71,7 +71,7 @@ User.login(username: "hello", password: "world") { results in //: Logging out - synchronously do { try User.logout() - print("Succesfully logged out") + print("Successfully logged out") } catch let error { assertionFailure("Error logging out: \(error)") } diff --git a/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift index 6a629af67..0f86a2077 100644 --- a/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/6 - Installation.xcplaygroundpage/Contents.swift @@ -44,7 +44,7 @@ DispatchQueue.main.async { switch results { case .success(let updatedInstallation): - print("Succesufully save myCustomInstallationKey to ParseServer: \(updatedInstallation)") + print("Successfully save myCustomInstallationKey to ParseServer: \(updatedInstallation)") case .failure(let error): assertionFailure("Failed to update installation: \(error)") } diff --git a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift index c0073ce85..2a507cfb7 100644 --- a/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/7 - GeoPoint.xcplaygroundpage/Contents.swift @@ -61,7 +61,7 @@ var constraints = [QueryConstraint]() constraints.append(near(key: "location", geoPoint: pointToFind)) let query = GameScore.query(constraints) -query.find(callbackQueue: .main) { results in +query.find { results in switch results { case .success(let scores): @@ -80,7 +80,7 @@ Notice the "var", the query has to be mutable since it's a valueType. */ var querySorted = query querySorted.order([.descending("score")]) -querySorted.find(callbackQueue: .main) { results in +querySorted.find { results in switch results { case .success(let scores): @@ -97,7 +97,7 @@ querySorted.find(callbackQueue: .main) { results in //: If you only want to query for scores > 50, you can add more constraints constraints.append("score" > 9) var query2 = GameScore.query(constraints) -query2.find(callbackQueue: .main) { results in +query2.find { results in switch results { case .success(let scores): @@ -116,7 +116,7 @@ query2.find(callbackQueue: .main) { results in //: If you want to query for scores > 50 and don't have a GeoPoint var query3 = GameScore.query("score" > 50, doesNotExist(key: "location")) -query3.find(callbackQueue: .main) { results in +query3.find { results in switch results { case .success(let scores): @@ -134,7 +134,7 @@ query3.find(callbackQueue: .main) { results in //: If you want to query for scores > 50 and have a GeoPoint var query4 = GameScore.query("score" > 10, exists(key: "location")) -query4.find(callbackQueue: .main) { results in +query4.find { results in switch results { case .success(let scores): @@ -154,7 +154,7 @@ let query5 = GameScore.query("score" == 50) let query6 = GameScore.query("score" == 200) var query7 = GameScore.query(or(queries: [query5, query6])) -query7.find(callbackQueue: .main) { results in +query7.find { results in switch results { case .success(let scores): diff --git a/Sources/ParseSwift/Object Protocols/ParseInstallation.swift b/Sources/ParseSwift/Object Protocols/ParseInstallation.swift index 8eeeee52b..b768fb709 100644 --- a/Sources/ParseSwift/Object Protocols/ParseInstallation.swift +++ b/Sources/ParseSwift/Object Protocols/ParseInstallation.swift @@ -283,3 +283,398 @@ extension ParseInstallation { } } } + +// MARK: Fetchable +extension ParseInstallation { + internal static func updateKeychainIfNeeded(_ results: [Self], deleting: Bool = false) throws { + guard BaseParseUser.current != nil, + let currentInstallation = BaseParseInstallation.current else { + return + } + + var saveInstallation: Self? + let foundCurrentInstallationObjects = results.filter { $0.hasSameObjectId(as: currentInstallation) } + if let foundCurrentInstallation = foundCurrentInstallationObjects.first { + saveInstallation = foundCurrentInstallation + } else { + saveInstallation = results.first + } + + if saveInstallation != nil { + if !deleting { + Self.current = saveInstallation + Self.saveCurrentContainerToKeychain() + } else { + Self.deleteCurrentContainerFromKeychain() + } + } + } + + /** + Fetches the `ParseInstallation` *synchronously* with the current data from the server + and sets an error if one occurs. + + - parameter options: A set of options used to save objects. Defaults to an empty set. + - throws: An Error of `ParseError` type. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + public func fetch(options: API.Options = []) throws -> Self { + let result: Self = try fetchCommand().execute(options: options) + try? Self.updateKeychainIfNeeded([result]) + return result + } + + /** + Fetches the `ParseInstallation` *asynchronously* and executes the given callback block. + + - parameter options: A set of options used to save objects. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default + value of .main. + - parameter completion: The block to execute when completed. + It should have the following argument signature: `(Result)`. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + public func fetch( + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void + ) { + do { + try fetchCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in + if case .success(let foundResult) = result { + try? Self.updateKeychainIfNeeded([foundResult]) + } + completion(result) + } + } catch let error as ParseError { + completion(.failure(error)) + } catch { + completion(.failure(ParseError(code: .unknownError, message: error.localizedDescription))) + } + } +} + +// MARK: Saveable +extension ParseInstallation { + + /** + Saves the `ParseInstallation` *synchronously* and throws an error if there's an issue. + + - parameter options: A set of options used to save objects. Defaults to an empty set. + - throws: A Error of type `ParseError`. + - returns: Returns saved `ParseInstallation`. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + public func save(options: API.Options = []) throws -> Self { + var childObjects: [NSDictionary: PointerType]? + var error: ParseError? + let group = DispatchGroup() + group.enter() + self.ensureDeepSave(options: options) { result in + switch result { + + case .success(let savedChildObjects): + childObjects = savedChildObjects + case .failure(let parseError): + error = parseError + } + group.leave() + } + group.wait() + + if let error = error { + throw error + } + + let result: Self = try saveCommand().execute(options: options, childObjects: childObjects) + try? Self.updateKeychainIfNeeded([result]) + return result + } + + /** + Saves the `ParseInstallation` *asynchronously* and executes the given callback block. + + - parameter options: A set of options used to save objects. 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)`. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + public func save( + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void + ) { + self.ensureDeepSave(options: options) { result in + switch result { + + case .success(let savedChildObjects): + self.saveCommand().executeAsync(options: options, callbackQueue: callbackQueue, + childObjects: savedChildObjects) { result in + if case .success(let foundResults) = result { + try? Self.updateKeychainIfNeeded([foundResults]) + } + completion(result) + } + case .failure(let parseError): + completion(.failure(parseError)) + } + } + } +} + +// MARK: Deletable +extension ParseInstallation { + /** + Deletes the `ParseInstallation` *synchronously* with the current data from the server + and sets an error if one occurs. + + - parameter options: A set of options used to save objects. Defaults to an empty set. + - throws: An Error of `ParseError` type. + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + public func delete(options: API.Options = []) throws { + _ = try deleteCommand().execute(options: options) + try? Self.updateKeychainIfNeeded([self], deleting: true) + } + + /** + Deletes the `ParseInstallation` *asynchronously* and executes the given callback block. + + - parameter options: A set of options used to save objects. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default + value of .main. + - parameter completion: The block to execute when completed. + It should have the following argument signature: `(Result)`. + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + public func delete( + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (ParseError?) -> Void + ) { + do { + try deleteCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in + switch result { + + case .success: + try? Self.updateKeychainIfNeeded([self], deleting: true) + completion(nil) + case .failure(let error): + completion(error) + } + } + } catch let error as ParseError { + completion(error) + } catch { + completion(ParseError(code: .unknownError, message: error.localizedDescription)) + } + } +} + +// MARK: Batch Support +public extension Sequence where Element: ParseInstallation { + + /** + Saves a collection of objects *synchronously* all at once and throws an error if necessary. + + - parameter options: A set of options used to save objects. Defaults to an empty set. + + - returns: Returns a Result enum with the object if a save was successful or a `ParseError` if it failed. + - throws: `ParseError` + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + func saveAll(options: API.Options = []) throws -> [(Result)] { + let commands = map { $0.saveCommand() } + let returnResults = try API.Command + .batch(commands: commands) + .execute(options: options) + try? Self.Element.updateKeychainIfNeeded(compactMap {$0}) + return returnResults + } + + /** + Saves a collection of objects all at once *asynchronously* and executes the completion block when done. + + - parameter options: A set of options used to save objects. 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<[(Result)], ParseError>)`. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + func saveAll( + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result<[(Result)], ParseError>) -> Void + ) { + let commands = map { $0.saveCommand() } + API.Command + .batch(commands: commands) + .executeAsync(options: options, callbackQueue: callbackQueue) { results in + switch results { + + case .success(let saved): + try? Self.Element.updateKeychainIfNeeded(compactMap {$0}) + completion(.success(saved)) + case .failure(let error): + completion(.failure(error)) + } + } + } + + /** + Fetches a collection of objects *synchronously* all at once and throws an error if necessary. + + - parameter options: A set of options used to fetch objects. Defaults to an empty set. + + - returns: Returns a Result enum with the object if a fetch was successful or a `ParseError` if it failed. + - throws: `ParseError` + - important: If an object fetched has the same objectId as current, it will automatically update the current. + - warning: The order in which objects are returned are not guarenteed. You shouldn't expect results in + any particular order. + */ + func fetchAll(options: API.Options = []) throws -> [(Result)] { + + if (allSatisfy { $0.className == Self.Element.className}) { + let uniqueObjectIds = Set(compactMap { $0.objectId }) + let query = Self.Element.query(containedIn(key: "objectId", array: [uniqueObjectIds])) + let fetchedObjects = try query.find(options: options) + var fetchedObjectsToReturn = [(Result)]() + + uniqueObjectIds.forEach { + let uniqueObjectId = $0 + if let fetchedObject = fetchedObjects.first(where: {$0.objectId == uniqueObjectId}) { + fetchedObjectsToReturn.append(.success(fetchedObject)) + } else { + fetchedObjectsToReturn.append(.failure(ParseError(code: .objectNotFound, + // swiftlint:disable:next line_length + message: "objectId \"\(uniqueObjectId)\" was not found in className \"\(Self.Element.className)\""))) + } + } + try? Self.Element.updateKeychainIfNeeded(fetchedObjects) + return fetchedObjectsToReturn + } else { + throw ParseError(code: .unknownError, message: "all items to fetch must be of the same class") + } + } + + /** + Fetches a collection of objects all at once *asynchronously* and executes the completion block when done. + + - parameter options: A set of options used to fetch objects. 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<[(Result)], ParseError>)`. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + - warning: The order in which objects are returned are not guarenteed. You shouldn't expect results in + any particular order. + */ + func fetchAll( + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result<[(Result)], ParseError>) -> Void + ) { + if (allSatisfy { $0.className == Self.Element.className}) { + let uniqueObjectIds = Set(compactMap { $0.objectId }) + let query = Self.Element.query(containedIn(key: "objectId", array: [uniqueObjectIds])) + query.find(options: options, callbackQueue: callbackQueue) { result in + switch result { + + case .success(let fetchedObjects): + var fetchedObjectsToReturn = [(Result)]() + + uniqueObjectIds.forEach { + let uniqueObjectId = $0 + if let fetchedObject = fetchedObjects.first(where: {$0.objectId == uniqueObjectId}) { + fetchedObjectsToReturn.append(.success(fetchedObject)) + } else { + fetchedObjectsToReturn.append(.failure(ParseError(code: .objectNotFound, + // swiftlint:disable:next line_length + message: "objectId \"\(uniqueObjectId)\" was not found in className \"\(Self.Element.className)\""))) + } + } + try? Self.Element.updateKeychainIfNeeded(fetchedObjects) + completion(.success(fetchedObjectsToReturn)) + case .failure(let error): + completion(.failure(error)) + } + } + } else { + completion(.failure(ParseError(code: .unknownError, + message: "all items to fetch must be of the same class"))) + } + } + + /** + Deletes a collection of objects *synchronously* all at once and throws an error if necessary. + + - parameter options: A set of options used to delete objects. Defaults to an empty set. + + - returns: Returns a Result enum with `true` if the delete successful or a `ParseError` if it failed. + 1. A `ParseError.Code.aggregateError`. This object's "errors" property is an + array of other Parse.Error objects. Each error object in this array + has an "object" property that references the object that could not be + deleted (for instance, because that object could not be found). + 2. A non-aggregate Parse.Error. This indicates a serious error that + caused the delete operation to be aborted partway through (for + instance, a connection failure in the middle of the delete). + - throws: `ParseError` + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + func deleteAll(options: API.Options = []) throws -> [(Result)] { + let commands = try map { try $0.deleteCommand() } + let returnResults = try API.Command + .batch(commands: commands) + .execute(options: options) + + try? Self.Element.updateKeychainIfNeeded(compactMap {$0}) + return returnResults + } + + /** + Deletes a collection of objects all at once *asynchronously* and executes the completion block when done. + + - parameter options: A set of options used to delete objects. 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<[(Result)], ParseError>)`. + Each element in the array is a Result enum with `true` if the delete successful or a `ParseError` if it failed. + 1. A `ParseError.Code.aggregateError`. This object's "errors" property is an + array of other Parse.Error objects. Each error object in this array + has an "object" property that references the object that could not be + deleted (for instance, because that object could not be found). + 2. A non-aggregate Parse.Error. This indicates a serious error that + caused the delete operation to be aborted partway through (for + instance, a connection failure in the middle of the delete). + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + func deleteAll( + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result<[(Result)], ParseError>) -> Void + ) { + do { + let commands = try map({ try $0.deleteCommand() }) + API.Command + .batch(commands: commands) + .executeAsync(options: options, + callbackQueue: callbackQueue) { results in + switch results { + + case .success(let deleted): + try? Self.Element.updateKeychainIfNeeded(compactMap {$0}) + completion(.success(deleted)) + case .failure(let error): + completion(.failure(error)) + } + } + } catch { + guard let parseError = error as? ParseError else { + completion(.failure(ParseError(code: .unknownError, + message: error.localizedDescription))) + return + } + completion(.failure(parseError)) + } + } +} // swiftlint:disable:this file_length diff --git a/Sources/ParseSwift/Object Protocols/ParseObject.swift b/Sources/ParseSwift/Object Protocols/ParseObject.swift index a7de228a2..46c5e161a 100644 --- a/Sources/ParseSwift/Object Protocols/ParseObject.swift +++ b/Sources/ParseSwift/Object Protocols/ParseObject.swift @@ -183,8 +183,8 @@ public extension Sequence where Element: ParseObject { func deleteAll(options: API.Options = []) throws -> [(Result)] { let commands = try map { try $0.deleteCommand() } return try API.Command - .batch(commands: commands) - .execute(options: options) + .batch(commands: commands) + .execute(options: options) } /** @@ -211,11 +211,14 @@ public extension Sequence where Element: ParseObject { do { let commands = try map({ try $0.deleteCommand() }) API.Command - .batch(commands: commands) - .executeAsync(options: options, callbackQueue: callbackQueue, completion: completion) + .batch(commands: commands) + .executeAsync(options: options, + callbackQueue: callbackQueue, + completion: completion) } catch { guard let parseError = error as? ParseError else { - completion(.failure(ParseError(code: .unknownError, message: error.localizedDescription))) + completion(.failure(ParseError(code: .unknownError, + message: error.localizedDescription))) return } completion(.failure(parseError)) @@ -256,52 +259,6 @@ extension ParseObject { // MARK: Fetchable extension ParseObject { - internal static func updateKeychainIfNeeded(_ results: [Self], deleting: Bool = false) throws { - guard let currentUser = BaseParseUser.current else { - return - } - - var foundCurrentUserObjects = results.filter { $0.hasSameObjectId(as: currentUser) } - foundCurrentUserObjects = try foundCurrentUserObjects.sorted(by: { - if $0.updatedAt == nil || $1.updatedAt == nil { - throw ParseError(code: .unknownError, - message: "Objects from the server should always have an 'updatedAt'") - } - return $0.updatedAt!.compare($1.updatedAt!) == .orderedDescending - }) - if let foundCurrentUser = foundCurrentUserObjects.first { - if !deleting { - let encoded = try ParseCoding.parseEncoder(skipKeys: false).encode(foundCurrentUser) - let updatedCurrentUser = try ParseCoding.jsonDecoder().decode(BaseParseUser.self, from: encoded) - BaseParseUser.current = updatedCurrentUser - BaseParseUser.saveCurrentContainerToKeychain() - } else { - BaseParseUser.deleteCurrentContainerFromKeychain() - } - } else if results.first?.className == BaseParseInstallation.className { - guard let currentInstallation = BaseParseInstallation.current else { - return - } - var saveInstallation: Self? - let foundCurrentInstallationObjects = results.filter { $0.hasSameObjectId(as: currentInstallation) } - if let foundCurrentInstallation = foundCurrentInstallationObjects.first { - saveInstallation = foundCurrentInstallation - } else { - saveInstallation = results.first - } - if saveInstallation != nil { - if !deleting { - let encoded = try ParseCoding.parseEncoder(skipKeys: false).encode(saveInstallation!) - let updatedCurrentInstallation = - try ParseCoding.jsonDecoder().decode(BaseParseInstallation.self, from: encoded) - BaseParseInstallation.current = updatedCurrentInstallation - BaseParseInstallation.saveCurrentContainerToKeychain() - } else { - BaseParseInstallation.deleteCurrentContainerFromKeychain() - } - } - } - } /** Fetches the `ParseObject` *synchronously* with the current data from the server and sets an error if one occurs. @@ -310,9 +267,7 @@ extension ParseObject { - throws: An Error of `ParseError` type. */ public func fetch(options: API.Options = []) throws -> Self { - let result: Self = try fetchCommand().execute(options: options) - try? Self.updateKeychainIfNeeded([result]) - return result + try fetchCommand().execute(options: options) } /** @@ -330,12 +285,7 @@ extension ParseObject { completion: @escaping (Result) -> Void ) { do { - try fetchCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in - if case .success(let foundResult) = result { - try? Self.updateKeychainIfNeeded([foundResult]) - } - completion(result) - } + try fetchCommand().executeAsync(options: options, callbackQueue: callbackQueue, completion: completion) } catch let error as ParseError { completion(.failure(error)) } catch { @@ -344,7 +294,7 @@ extension ParseObject { } internal func fetchCommand() throws -> API.Command { - return try API.Command.fetchCommand(self) + try API.Command.fetchCommand(self) } } @@ -392,10 +342,10 @@ extension ParseObject { case .success(let savedChildObjects): childObjects = savedChildObjects - group.leave() case .failure(let parseError): error = parseError } + group.leave() } group.wait() @@ -403,9 +353,7 @@ extension ParseObject { throw error } - let result: Self = try saveCommand().execute(options: options, childObjects: childObjects) - try? Self.updateKeychainIfNeeded([result]) - return result + return try saveCommand().execute(options: options, childObjects: childObjects) } /** @@ -426,12 +374,7 @@ extension ParseObject { case .success(let savedChildObjects): self.saveCommand().executeAsync(options: options, callbackQueue: callbackQueue, - childObjects: savedChildObjects) { result in - if case .success(let foundResults) = result { - try? Self.updateKeychainIfNeeded([foundResults]) - } - completion(result) - } + childObjects: savedChildObjects, completion: completion) case .failure(let parseError): completion(.failure(parseError)) } @@ -500,11 +443,11 @@ extension ParseObject { // MARK: Savable Encodable Version internal extension Encodable { func save(options: API.Options = []) throws -> PointerType { - return try saveCommand().execute(options: options) + try saveCommand().execute(options: options) } func saveCommand() throws -> API.Command { - return try API.Command.saveCommand(self) + try API.Command.saveCommand(self) } func saveAll(options: API.Options = [], @@ -526,7 +469,7 @@ extension ParseObject { */ public func delete(options: API.Options = []) throws { _ = try deleteCommand().execute(options: options) - try? Self.updateKeychainIfNeeded([self], deleting: true) + return } /** @@ -548,7 +491,6 @@ extension ParseObject { switch result { case .success: - try? Self.updateKeychainIfNeeded([self], deleting: true) completion(nil) case .failure(let error): completion(error) diff --git a/Sources/ParseSwift/Object Protocols/ParseUser.swift b/Sources/ParseSwift/Object Protocols/ParseUser.swift index 46ea8cfa6..9fc916478 100644 --- a/Sources/ParseSwift/Object Protocols/ParseUser.swift +++ b/Sources/ParseSwift/Object Protocols/ParseUser.swift @@ -105,7 +105,7 @@ extension ParseUser { */ public static func login(username: String, password: String) throws -> Self { - return try loginCommand(username: username, password: password).execute(options: []) + try loginCommand(username: username, password: password).execute(options: []) } /** @@ -202,7 +202,7 @@ extension ParseUser { */ public static func signup(username: String, password: String) throws -> Self { - return try signupCommand(username: username, password: password).execute(options: []) + try signupCommand(username: username, password: password).execute(options: []) } /** @@ -215,7 +215,7 @@ extension ParseUser { - returns: Returns whether the sign up was successful. */ public func signup() throws -> Self { - return try signupCommand().execute(options: []) + try signupCommand().execute(options: []) } /** @@ -299,3 +299,395 @@ private struct SignupBody: Codable { let username: String let password: String } + +// MARK: Fetchable +extension ParseUser { + internal static func updateKeychainIfNeeded(_ results: [Self], deleting: Bool = false) throws { + guard let currentUser = BaseParseUser.current else { + return + } + + var foundCurrentUserObjects = results.filter { $0.hasSameObjectId(as: currentUser) } + foundCurrentUserObjects = try foundCurrentUserObjects.sorted(by: { + if $0.updatedAt == nil || $1.updatedAt == nil { + throw ParseError(code: .unknownError, + message: "Objects from the server should always have an 'updatedAt'") + } + return $0.updatedAt!.compare($1.updatedAt!) == .orderedDescending + }) + if let foundCurrentUser = foundCurrentUserObjects.first { + if !deleting { + Self.current = foundCurrentUser + Self.saveCurrentContainerToKeychain() + } else { + Self.deleteCurrentContainerFromKeychain() + } + } + } + + /** + Fetches the `ParseUser` *synchronously* with the current data from the server and sets an error if one occurs. + + - parameter options: A set of options used to save objects. Defaults to an empty set. + - throws: An Error of `ParseError` type. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + public func fetch(options: API.Options = []) throws -> Self { + let result: Self = try fetchCommand().execute(options: options) + try? Self.updateKeychainIfNeeded([result]) + return result + } + + /** + Fetches the `ParseUser` *asynchronously* and executes the given callback block. + + - parameter options: A set of options used to save objects. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default + value of .main. + - parameter completion: The block to execute when completed. + It should have the following argument signature: `(Result)`. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + public func fetch( + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void + ) { + do { + try fetchCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in + if case .success(let foundResult) = result { + try? Self.updateKeychainIfNeeded([foundResult]) + } + completion(result) + } + } catch let error as ParseError { + completion(.failure(error)) + } catch { + completion(.failure(ParseError(code: .unknownError, message: error.localizedDescription))) + } + } +} + +// MARK: Saveable +extension ParseUser { + + /** + Saves the `ParseUser` *synchronously* and throws an error if there's an issue. + + - parameter options: A set of options used to save objects. Defaults to an empty set. + - throws: A Error of type `ParseError`. + - returns: Returns saved `ParseUser`. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + public func save(options: API.Options = []) throws -> Self { + var childObjects: [NSDictionary: PointerType]? + var error: ParseError? + let group = DispatchGroup() + group.enter() + self.ensureDeepSave(options: options) { result in + switch result { + + case .success(let savedChildObjects): + childObjects = savedChildObjects + case .failure(let parseError): + error = parseError + } + group.leave() + } + group.wait() + + if let error = error { + throw error + } + + let result: Self = try saveCommand().execute(options: options, childObjects: childObjects) + try? Self.updateKeychainIfNeeded([result]) + return result + } + + /** + Saves the `ParseUser` *asynchronously* and executes the given callback block. + + - parameter options: A set of options used to save objects. 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)`. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + public func save( + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void + ) { + self.ensureDeepSave(options: options) { result in + switch result { + + case .success(let savedChildObjects): + self.saveCommand().executeAsync(options: options, callbackQueue: callbackQueue, + childObjects: savedChildObjects) { result in + if case .success(let foundResults) = result { + try? Self.updateKeychainIfNeeded([foundResults]) + } + completion(result) + } + case .failure(let parseError): + completion(.failure(parseError)) + } + } + } +} + +// MARK: Deletable +extension ParseUser { + /** + Deletes the `ParseUser` *synchronously* with the current data from the server and sets an error if one occurs. + + - parameter options: A set of options used to save objects. Defaults to an empty set. + - throws: An Error of `ParseError` type. + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + public func delete(options: API.Options = []) throws { + _ = try deleteCommand().execute(options: options) + try? Self.updateKeychainIfNeeded([self], deleting: true) + } + + /** + Deletes the `ParseUser` *asynchronously* and executes the given callback block. + + - parameter options: A set of options used to save objects. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default + value of .main. + - parameter completion: The block to execute when completed. + It should have the following argument signature: `(Result)`. + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + public func delete( + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (ParseError?) -> Void + ) { + do { + try deleteCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in + switch result { + + case .success: + try? Self.updateKeychainIfNeeded([self], deleting: true) + completion(nil) + case .failure(let error): + completion(error) + } + } + } catch let error as ParseError { + completion(error) + } catch { + completion(ParseError(code: .unknownError, message: error.localizedDescription)) + } + } +} + +// MARK: Batch Support +public extension Sequence where Element: ParseUser { + + /** + Saves a collection of objects *synchronously* all at once and throws an error if necessary. + + - parameter options: A set of options used to save objects. Defaults to an empty set. + + - returns: Returns a Result enum with the object if a save was successful or a `ParseError` if it failed. + - throws: `ParseError` + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + func saveAll(options: API.Options = []) throws -> [(Result)] { + let commands = map { $0.saveCommand() } + let returnResults = try API.Command + .batch(commands: commands) + .execute(options: options) + try? Self.Element.updateKeychainIfNeeded(compactMap {$0}) + return returnResults + } + + /** + Saves a collection of objects all at once *asynchronously* and executes the completion block when done. + + - parameter options: A set of options used to save objects. 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<[(Result)], ParseError>)`. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + func saveAll( + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result<[(Result)], ParseError>) -> Void + ) { + let commands = map { $0.saveCommand() } + API.Command + .batch(commands: commands) + .executeAsync(options: options, callbackQueue: callbackQueue) { results in + switch results { + + case .success(let saved): + try? Self.Element.updateKeychainIfNeeded(compactMap {$0}) + completion(.success(saved)) + case .failure(let error): + completion(.failure(error)) + } + } + } + + /** + Fetches a collection of objects *synchronously* all at once and throws an error if necessary. + + - parameter options: A set of options used to fetch objects. Defaults to an empty set. + + - returns: Returns a Result enum with the object if a fetch was successful or a `ParseError` if it failed. + - throws: `ParseError` + - important: If an object fetched has the same objectId as current, it will automatically update the current. + - warning: The order in which objects are returned are not guarenteed. You shouldn't expect results in + any particular order. + */ + func fetchAll(options: API.Options = []) throws -> [(Result)] { + + if (allSatisfy { $0.className == Self.Element.className}) { + let uniqueObjectIds = Set(compactMap { $0.objectId }) + let query = Self.Element.query(containedIn(key: "objectId", array: [uniqueObjectIds])) + let fetchedObjects = try query.find(options: options) + var fetchedObjectsToReturn = [(Result)]() + + uniqueObjectIds.forEach { + let uniqueObjectId = $0 + if let fetchedObject = fetchedObjects.first(where: {$0.objectId == uniqueObjectId}) { + fetchedObjectsToReturn.append(.success(fetchedObject)) + } else { + fetchedObjectsToReturn.append(.failure(ParseError(code: .objectNotFound, + // swiftlint:disable:next line_length + message: "objectId \"\(uniqueObjectId)\" was not found in className \"\(Self.Element.className)\""))) + } + } + try? Self.Element.updateKeychainIfNeeded(fetchedObjects) + return fetchedObjectsToReturn + } else { + throw ParseError(code: .unknownError, message: "all items to fetch must be of the same class") + } + } + + /** + Fetches a collection of objects all at once *asynchronously* and executes the completion block when done. + + - parameter options: A set of options used to fetch objects. 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<[(Result)], ParseError>)`. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + - warning: The order in which objects are returned are not guarenteed. You shouldn't expect results in + any particular order. + */ + func fetchAll( + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result<[(Result)], ParseError>) -> Void + ) { + if (allSatisfy { $0.className == Self.Element.className}) { + let uniqueObjectIds = Set(compactMap { $0.objectId }) + let query = Self.Element.query(containedIn(key: "objectId", array: [uniqueObjectIds])) + query.find(options: options, callbackQueue: callbackQueue) { result in + switch result { + + case .success(let fetchedObjects): + var fetchedObjectsToReturn = [(Result)]() + + uniqueObjectIds.forEach { + let uniqueObjectId = $0 + if let fetchedObject = fetchedObjects.first(where: {$0.objectId == uniqueObjectId}) { + fetchedObjectsToReturn.append(.success(fetchedObject)) + } else { + fetchedObjectsToReturn.append(.failure(ParseError(code: .objectNotFound, + // swiftlint:disable:next line_length + message: "objectId \"\(uniqueObjectId)\" was not found in className \"\(Self.Element.className)\""))) + } + } + try? Self.Element.updateKeychainIfNeeded(fetchedObjects) + completion(.success(fetchedObjectsToReturn)) + case .failure(let error): + completion(.failure(error)) + } + } + } else { + completion(.failure(ParseError(code: .unknownError, + message: "all items to fetch must be of the same class"))) + } + } + + /** + Deletes a collection of objects *synchronously* all at once and throws an error if necessary. + + - parameter options: A set of options used to delete objects. Defaults to an empty set. + + - returns: Returns a Result enum with `true` if the delete successful or a `ParseError` if it failed. + 1. A `ParseError.Code.aggregateError`. This object's "errors" property is an + array of other Parse.Error objects. Each error object in this array + has an "object" property that references the object that could not be + deleted (for instance, because that object could not be found). + 2. A non-aggregate Parse.Error. This indicates a serious error that + caused the delete operation to be aborted partway through (for + instance, a connection failure in the middle of the delete). + - throws: `ParseError` + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + func deleteAll(options: API.Options = []) throws -> [(Result)] { + let commands = try map { try $0.deleteCommand() } + let returnResults = try API.Command + .batch(commands: commands) + .execute(options: options) + + try? Self.Element.updateKeychainIfNeeded(compactMap {$0}) + return returnResults + } + + /** + Deletes a collection of objects all at once *asynchronously* and executes the completion block when done. + + - parameter options: A set of options used to delete objects. 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<[(Result)], ParseError>)`. + Each element in the array is a Result enum with `true` if the delete successful or a `ParseError` if it failed. + 1. A `ParseError.Code.aggregateError`. This object's "errors" property is an + array of other Parse.Error objects. Each error object in this array + has an "object" property that references the object that could not be + deleted (for instance, because that object could not be found). + 2. A non-aggregate Parse.Error. This indicates a serious error that + caused the delete operation to be aborted partway through (for + instance, a connection failure in the middle of the delete). + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + func deleteAll( + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result<[(Result)], ParseError>) -> Void + ) { + do { + let commands = try map({ try $0.deleteCommand() }) + API.Command + .batch(commands: commands) + .executeAsync(options: options, + callbackQueue: callbackQueue) { results in + switch results { + + case .success(let deleted): + try? Self.Element.updateKeychainIfNeeded(compactMap {$0}) + completion(.success(deleted)) + case .failure(let error): + completion(.failure(error)) + } + } + } catch { + guard let parseError = error as? ParseError else { + completion(.failure(ParseError(code: .unknownError, + message: error.localizedDescription))) + return + } + completion(.failure(parseError)) + } + } +} // swiftlint:disable:this file_length diff --git a/Sources/ParseSwift/Object Protocols/Protocols/Deletable.swift b/Sources/ParseSwift/Object Protocols/Protocols/Deletable.swift index 0d4689c0b..b81a6f1b8 100644 --- a/Sources/ParseSwift/Object Protocols/Protocols/Deletable.swift +++ b/Sources/ParseSwift/Object Protocols/Protocols/Deletable.swift @@ -15,6 +15,6 @@ public protocol Deletable: Codable { extension Deletable { public func delete() throws -> DeletingType { - return try delete(options: []) + try delete(options: []) } } diff --git a/Sources/ParseSwift/Object Protocols/Protocols/Fetchable.swift b/Sources/ParseSwift/Object Protocols/Protocols/Fetchable.swift index 745e73093..4fe31fe87 100644 --- a/Sources/ParseSwift/Object Protocols/Protocols/Fetchable.swift +++ b/Sources/ParseSwift/Object Protocols/Protocols/Fetchable.swift @@ -15,6 +15,6 @@ public protocol Fetchable: Codable { extension Fetchable { public func fetch() throws -> FetchingType { - return try fetch(options: []) + try fetch(options: []) } } diff --git a/Sources/ParseSwift/Object Protocols/Protocols/Queryable.swift b/Sources/ParseSwift/Object Protocols/Protocols/Queryable.swift index ff0b8981b..e04cf9f9c 100644 --- a/Sources/ParseSwift/Object Protocols/Protocols/Queryable.swift +++ b/Sources/ParseSwift/Object Protocols/Protocols/Queryable.swift @@ -30,7 +30,7 @@ extension Queryable { - returns: Returns an array of `ParseObject`s that were found. */ func find() throws -> [ResultType] { - return try find(options: []) + try find(options: []) } /** @@ -43,7 +43,7 @@ extension Queryable { - returns: Returns a `ParseObject`, or `nil` if none was found. */ func first() throws -> ResultType? { - return try first(options: []) + try first(options: []) } /** @@ -54,7 +54,7 @@ extension Queryable { - returns: Returns the number of `ParseObject`s that match the query, or `-1` if there is an error. */ func count() throws -> Int { - return try count(options: []) + try count(options: []) } /** diff --git a/Sources/ParseSwift/Object Protocols/Protocols/Saveable.swift b/Sources/ParseSwift/Object Protocols/Protocols/Saveable.swift index ed80538f0..c3a36efe5 100644 --- a/Sources/ParseSwift/Object Protocols/Protocols/Saveable.swift +++ b/Sources/ParseSwift/Object Protocols/Protocols/Saveable.swift @@ -15,6 +15,6 @@ public protocol Saveable: Codable { extension Saveable { public func save() throws -> SavingType { - return try save(options: []) + try save(options: []) } } diff --git a/Sources/ParseSwift/Parse Types/Query.swift b/Sources/ParseSwift/Parse Types/Query.swift index 99e3b9786..6fab4fbcb 100644 --- a/Sources/ParseSwift/Parse Types/Query.swift +++ b/Sources/ParseSwift/Parse Types/Query.swift @@ -713,9 +713,7 @@ extension Query: Queryable { - returns: Returns an array of `ParseObject`s that were found. */ public func find(options: API.Options = []) throws -> [ResultType] { - let foundResults = try findCommand().execute(options: options) - try? ResultType.updateKeychainIfNeeded(foundResults) - return foundResults + try findCommand().execute(options: options) } /** @@ -740,14 +738,9 @@ extension Query: Queryable { - parameter completion: The block to execute. It should have the following argument signature: `(Result<[ResultType], ParseError>)` */ - public func find(options: API.Options = [], callbackQueue: DispatchQueue, + public func find(options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result<[ResultType], ParseError>) -> Void) { - findCommand().executeAsync(options: options, callbackQueue: callbackQueue) { results in - if case .success(let foundResults) = results { - try? ResultType.updateKeychainIfNeeded(foundResults) - } - completion(results) - } + findCommand().executeAsync(options: options, callbackQueue: callbackQueue, completion: completion) } /** @@ -760,7 +753,8 @@ extension Query: Queryable { - parameter completion: The block to execute. It should have the following argument signature: `(Result<[AnyResultType], ParseError>)` */ - public func find(explain: Bool, hint: String? = nil, options: API.Options = [], callbackQueue: DispatchQueue, + public func find(explain: Bool, hint: String? = nil, options: API.Options = [], + callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { findCommand(explain: explain, hint: hint).executeAsync(options: options, callbackQueue: callbackQueue, completion: completion) @@ -776,13 +770,7 @@ extension Query: Queryable { - returns: Returns a `ParseObject`, or `nil` if none was found. */ public func first(options: API.Options = []) throws -> ResultType? { - let result = try firstCommand().execute(options: options) - if let foundResult = result { - try? ResultType.updateKeychainIfNeeded([foundResult]) - } else { - throw ParseError(code: .objectNotFound, message: "Object not found on the server.") - } - return result + try firstCommand().execute(options: options) } /** @@ -809,7 +797,7 @@ extension Query: Queryable { - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. */ - public func first(options: API.Options = [], callbackQueue: DispatchQueue, + public func first(options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { firstCommand().executeAsync(options: options, callbackQueue: callbackQueue) { result in @@ -819,7 +807,6 @@ extension Query: Queryable { completion(.failure(ParseError(code: .objectNotFound, message: "Object not found on the server."))) return } - try? ResultType.updateKeychainIfNeeded([first]) completion(.success(first)) case .failure(let error): completion(.failure(error)) @@ -838,7 +825,8 @@ extension Query: Queryable { - parameter completion: The block to execute. It should have the following argument signature: `(Result)`. */ - public func first(explain: Bool, hint: String? = nil, options: API.Options = [], callbackQueue: DispatchQueue, + public func first(explain: Bool, hint: String? = nil, options: API.Options = [], + callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { firstCommand(explain: explain, hint: hint).executeAsync(options: options, callbackQueue: callbackQueue, completion: completion) @@ -878,7 +866,7 @@ extension Query: Queryable { - parameter completion: The block to execute. It should have the following argument signature: `(Result)` */ - public func count(options: API.Options = [], callbackQueue: DispatchQueue, + public func count(options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { countCommand().executeAsync(options: options, callbackQueue: callbackQueue, completion: completion) } @@ -892,7 +880,8 @@ extension Query: Queryable { - parameter completion: The block to execute. It should have the following argument signature: `(Result)` */ - public func count(explain: Bool, hint: String? = nil, options: API.Options = [], callbackQueue: DispatchQueue, + public func count(explain: Bool, hint: String? = nil, options: API.Options = [], + callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { countCommand(explain: explain, hint: hint).executeAsync(options: options, callbackQueue: callbackQueue, completion: completion) diff --git a/Tests/ParseSwiftTests/ParseInstallationTests.swift b/Tests/ParseSwiftTests/ParseInstallationTests.swift index e04a46f9f..30dca716d 100644 --- a/Tests/ParseSwiftTests/ParseInstallationTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationTests.swift @@ -475,6 +475,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l var installationOnServer = installation installationOnServer.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + installationOnServer.customKey = "newValue" let encoded: Data! do { @@ -499,7 +500,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l expectation1.fulfill() return } - guard let originalCreatedAt = installation.createdAt, + guard let originalCreatedAt = installationOnServer.createdAt, let originalUpdatedAt = installation.updatedAt, let serverUpdatedAt = installationOnServer.updatedAt else { XCTFail("Should unwrap dates") @@ -509,6 +510,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) XCTAssertGreaterThan(fetchedUpdatedAt, originalUpdatedAt) XCTAssertEqual(fetchedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(Installation.current?.customKey, installationOnServer.customKey) //Should be updated in memory guard let updatedCurrentDate = Installation.current?.updatedAt else { @@ -551,6 +553,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l var installationOnServer = installation installationOnServer.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + installationOnServer.customKey = "newValue" let encoded: Data! do { @@ -577,7 +580,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l expectation1.fulfill() return } - guard let originalCreatedAt = installation.createdAt, + guard let originalCreatedAt = installationOnServer.createdAt, let originalUpdatedAt = installation.updatedAt, let serverUpdatedAt = installationOnServer.updatedAt else { XCTFail("Should unwrap dates") @@ -587,6 +590,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) XCTAssertGreaterThan(fetchedUpdatedAt, originalUpdatedAt) XCTAssertEqual(fetchedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(Installation.current?.customKey, installationOnServer.customKey) //Should be updated in memory guard let updatedCurrentDate = Installation.current?.updatedAt else { @@ -613,5 +617,514 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } wait(for: [expectation1], timeout: 10.0) } + + func testDelete() { + testUpdate() + let expectation1 = XCTestExpectation(description: "Delete installation1") + DispatchQueue.main.async { + guard let installation = Installation.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + do { + try installation.delete(options: []) + } catch { + XCTFail(error.localizedDescription) + } + + do { + try installation.delete(options: [.useMasterKey]) + } catch { + XCTFail(error.localizedDescription) + } + + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 10.0) + } + + func testDeleteAsyncMainQueue() { + testUpdate() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Delete installation1") + DispatchQueue.main.async { + guard let installation = Installation.current else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + + var installationOnServer = installation + installationOnServer.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + + let encoded: Data! + do { + encoded = try installationOnServer.getEncoder(skipKeys: false).encode(installationOnServer) + //Get dates in correct format from ParseDecoding strategy + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + installation.delete { error in + XCTAssertNil(error) + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 10.0) + } + + // swiftlint:disable:next function_body_length + func testFetchAll() { + testUpdate() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Fetch installation1") + + DispatchQueue.main.async { + guard var installation = Installation.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + installation.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + installation.customKey = "newValue" + let installationOnServer = FindResult(results: [installation], count: 1) + + let encoded: Data! + do { + encoded = try installation.getEncoder(skipKeys: false).encode(installationOnServer) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try installation.getEncoder(skipKeys: false).encode(installation) + installation = try installation.getDecoder().decode(Installation.self, from: encoded1) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + let fetched = try [installation].fetchAll() + fetched.forEach { + switch $0 { + case .success(let fetched): + XCTAssert(fetched.hasSameObjectId(as: installation)) + guard let fetchedCreatedAt = fetched.createdAt, + let fetchedUpdatedAt = fetched.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalCreatedAt = installation.createdAt, + let originalUpdatedAt = installation.updatedAt, + let serverUpdatedAt = installation.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) + XCTAssertEqual(fetchedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(fetchedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(Installation.current?.customKey, installation.customKey) + + //Should be updated in memory + guard let updatedCurrentDate = Installation.current?.updatedAt else { + XCTFail("Should unwrap current date") + expectation1.fulfill() + return + } + XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) + + //Shold be updated in Keychain + guard let keychainInstallation: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation), + let keychainUpdatedCurrentDate = keychainInstallation.currentInstallation?.updatedAt else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + } catch { + XCTFail(error.localizedDescription) + } + + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 10.0) + } + + // swiftlint:disable:next function_body_length + func testFetchAllAsyncMainQueue() { + testUpdate() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Fetch installation1") + DispatchQueue.main.async { + guard var installation = Installation.current else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + + installation.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + installation.customKey = "newValue" + let installationOnServer = FindResult(results: [installation], count: 1) + + let encoded: Data! + do { + encoded = try installation.getEncoder(skipKeys: false).encode(installationOnServer) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try installation.getEncoder(skipKeys: false).encode(installation) + installation = try installation.getDecoder().decode(Installation.self, from: encoded1) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + [installation].fetchAll { results in + switch results { + + case .success(let fetched): + fetched.forEach { + switch $0 { + case .success(let fetched): + XCTAssert(fetched.hasSameObjectId(as: installation)) + guard let fetchedCreatedAt = fetched.createdAt, + let fetchedUpdatedAt = fetched.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalCreatedAt = installation.createdAt, + let originalUpdatedAt = installation.updatedAt, + let serverUpdatedAt = installation.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) + XCTAssertEqual(fetchedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(fetchedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(Installation.current?.customKey, installation.customKey) + + //Should be updated in memory + guard let updatedCurrentDate = Installation.current?.updatedAt else { + XCTFail("Should unwrap current date") + expectation1.fulfill() + return + } + XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) + + //Shold be updated in Keychain + guard let keychainInstallation: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation), + let keychainUpdatedCurrentDate = keychainInstallation + .currentInstallation?.updatedAt else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 10.0) + } + + // swiftlint:disable:next function_body_length + func testSaveAll() { + testUpdate() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Fetch installation1") + + DispatchQueue.main.async { + guard var installation = Installation.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + installation.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + installation.customKey = "newValue" + let installationOnServer = [BatchResponseItem(success: installation, error: nil)] + + let encoded: Data! + do { + encoded = try installation.getEncoder(skipKeys: false).encode(installationOnServer) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try installation.getEncoder(skipKeys: false).encode(installation) + installation = try installation.getDecoder().decode(Installation.self, from: encoded1) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + let saved = try [installation].saveAll() + saved.forEach { + switch $0 { + case .success(let saved): + XCTAssert(saved.hasSameObjectId(as: installation)) + guard let savedCreatedAt = saved.createdAt, + let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalCreatedAt = installation.createdAt, + let originalUpdatedAt = installation.updatedAt, + let serverUpdatedAt = installation.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(savedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(Installation.current?.customKey, installation.customKey) + + //Should be updated in memory + guard let updatedCurrentDate = Installation.current?.updatedAt else { + XCTFail("Should unwrap current date") + expectation1.fulfill() + return + } + XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) + + //Shold be updated in Keychain + guard let keychainInstallation: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation), + let keychainUpdatedCurrentDate = keychainInstallation.currentInstallation?.updatedAt else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + } catch { + XCTFail(error.localizedDescription) + } + + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 10.0) + } + + // swiftlint:disable:next function_body_length + func testSaveAllAsyncMainQueue() { + testUpdate() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Fetch installation1") + DispatchQueue.main.async { + guard var installation = Installation.current else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + + installation.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + installation.customKey = "newValue" + let installationOnServer = [BatchResponseItem(success: installation, error: nil)] + + let encoded: Data! + do { + encoded = try installation.getEncoder(skipKeys: false).encode(installationOnServer) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try installation.getEncoder(skipKeys: false).encode(installation) + installation = try installation.getDecoder().decode(Installation.self, from: encoded1) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + [installation].saveAll { results in + switch results { + + case .success(let saved): + saved.forEach { + switch $0 { + case .success(let saved): + XCTAssert(saved.hasSameObjectId(as: installation)) + guard let savedCreatedAt = saved.createdAt, + let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalCreatedAt = installation.createdAt, + let originalUpdatedAt = installation.updatedAt, + let serverUpdatedAt = installation.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(savedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(Installation.current?.customKey, installation.customKey) + + //Should be updated in memory + guard let updatedCurrentDate = Installation.current?.updatedAt else { + XCTFail("Should unwrap current date") + expectation1.fulfill() + return + } + XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) + + //Shold be updated in Keychain + guard let keychainInstallation: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation), + let keychainUpdatedCurrentDate = keychainInstallation + .currentInstallation?.updatedAt else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 10.0) + } + + func testDeleteAll() { + testUpdate() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Delete installation1") + + DispatchQueue.main.async { + guard let installation = Installation.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + let installationOnServer = [BatchResponseItem(success: true, error: nil)] + + let encoded: Data! + do { + encoded = try installation.getEncoder(skipKeys: false).encode(installationOnServer) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + let deleted = try [installation].deleteAll() + deleted.forEach { + switch $0 { + case .success: + return + case .failure(let error): + XCTFail("Should have deleted: \(error.localizedDescription)") + } + } + } catch { + XCTFail(error.localizedDescription) + } + + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 10.0) + } + + func testDeleteAllAsyncMainQueue() { + testUpdate() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Delete installation1") + DispatchQueue.main.async { + guard let installation = Installation.current else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + + let installationOnServer = [BatchResponseItem(success: true, error: nil)] + + let encoded: Data! + do { + encoded = try installation.getEncoder(skipKeys: false).encode(installationOnServer) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + [installation].deleteAll { results in + switch results { + + case .success(let deleted): + deleted.forEach { + switch $0 { + case .success: + return + case .failure(let error): + XCTFail("Should have deleted: \(error.localizedDescription)") + } + } + case .failure(let error): + XCTFail("Should have deleted: \(error.localizedDescription)") + } + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 10.0) + } } // swiftlint:disable:this file_length diff --git a/Tests/ParseSwiftTests/ParseObjectTests.swift b/Tests/ParseSwiftTests/ParseObjectTests.swift index 9eab3f341..9c1125c1c 100644 --- a/Tests/ParseSwiftTests/ParseObjectTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectTests.swift @@ -853,7 +853,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length func deleteAsync(score: GameScore, scoreOnServer: GameScore, callbackQueue: DispatchQueue) { - let expectation1 = XCTestExpectation(description: "Fetch object1") + let expectation1 = XCTestExpectation(description: "Delete object1") score.delete(options: [], callbackQueue: callbackQueue) { error in guard let error = error else { @@ -864,7 +864,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length expectation1.fulfill() } - let expectation2 = XCTestExpectation(description: "Fetch object2") + let expectation2 = XCTestExpectation(description: "Delete object2") score.delete(options: [.useMasterKey], callbackQueue: callbackQueue) { error in guard let error = error else { diff --git a/Tests/ParseSwiftTests/ParseQueryTests.swift b/Tests/ParseSwiftTests/ParseQueryTests.swift index 9d17c9685..28da79ef7 100755 --- a/Tests/ParseSwiftTests/ParseQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryTests.swift @@ -349,7 +349,7 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length let query = GameScore.query() do { - guard try query.first(options: []) != nil else { + guard try query.first(options: []) == nil else { XCTFail("Should have thrown error") return } @@ -934,10 +934,6 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length do { let encoded = try ParseCoding.parseEncoder().encode(queryWhere) - guard let jsonString = String(data: encoded, encoding: .utf8) else { - XCTFail("Should have encoded") - return - } let decodedDictionary = try JSONDecoder().decode([String: AnyCodable].self, from: encoded) XCTAssertEqual(expected.keys, decodedDictionary.keys) diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index 77d1b2600..8082521ec 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -159,7 +159,6 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } func testFetchAndUpdateCurrentUser() { // swiftlint:disable:this function_body_length - XCTAssertNil(User.current?.objectId) testUserLogin() MockURLProtocol.removeAll() XCTAssertNotNil(User.current?.objectId) @@ -172,6 +171,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length var userOnServer = user userOnServer.createdAt = User.current?.createdAt userOnServer.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) + userOnServer.customKey = "newValue" let encoded: Data! do { @@ -202,9 +202,11 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) XCTAssertGreaterThan(fetchedUpdatedAt, originalUpdatedAt) XCTAssertNil(fetched.ACL) + XCTAssertEqual(fetched.customKey, userOnServer.customKey) //Should be updated in memory XCTAssertEqual(User.current?.updatedAt, fetchedUpdatedAt) + XCTAssertEqual(User.current?.customKey, userOnServer.customKey) //Shold be updated in Keychain guard let keychainUser: CurrentUserContainer @@ -233,6 +235,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length var userOnServer = user userOnServer.createdAt = User.current?.createdAt userOnServer.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) + userOnServer.customKey = "newValue" let encoded: Data! do { @@ -266,6 +269,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) XCTAssertGreaterThan(fetchedUpdatedAt, originalUpdatedAt) XCTAssertNil(fetched.ACL) + XCTAssertEqual(User.current?.customKey, userOnServer.customKey) //Should be updated in memory XCTAssertEqual(User.current?.updatedAt, fetchedUpdatedAt) @@ -969,5 +973,512 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } XCTAssertNil(keychainUser.currentUser?.customKey) } + + func testDelete() { + testUserLogin() + let expectation1 = XCTestExpectation(description: "Delete installation1") + DispatchQueue.main.async { + guard let user = User.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + do { + try user.delete(options: []) + } catch { + XCTFail(error.localizedDescription) + } + + do { + try user.delete(options: [.useMasterKey]) + } catch { + XCTFail(error.localizedDescription) + } + + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 10.0) + } + + func testDeleteAsyncMainQueue() { + testUserLogin() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Delete installation1") + DispatchQueue.main.async { + guard let user = User.current else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + + var userOnServer = user + userOnServer.updatedAt = user.updatedAt?.addingTimeInterval(+300) + + let encoded: Data! + do { + encoded = try userOnServer.getEncoder(skipKeys: false).encode(userOnServer) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + user.delete { error in + XCTAssertNil(error) + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 10.0) + } + + // swiftlint:disable:next function_body_length + func testFetchAll() { + testUserLogin() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Fetch user1") + + DispatchQueue.main.async { + guard var user = User.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + user.updatedAt = user.updatedAt?.addingTimeInterval(+300) + user.customKey = "newValue" + let userOnServer = FindResult(results: [user], count: 1) + + let encoded: Data! + do { + encoded = try user.getEncoder(skipKeys: false).encode(userOnServer) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try user.getEncoder(skipKeys: false).encode(user) + user = try user.getDecoder().decode(User.self, from: encoded1) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + let fetched = try [user].fetchAll() + fetched.forEach { + switch $0 { + case .success(let fetched): + XCTAssert(fetched.hasSameObjectId(as: user)) + guard let fetchedCreatedAt = fetched.createdAt, + let fetchedUpdatedAt = fetched.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalCreatedAt = user.createdAt, + let originalUpdatedAt = user.updatedAt, + let serverUpdatedAt = user.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) + XCTAssertEqual(fetchedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(fetchedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(User.current?.customKey, user.customKey) + + //Should be updated in memory + guard let updatedCurrentDate = User.current?.updatedAt else { + XCTFail("Should unwrap current date") + expectation1.fulfill() + return + } + XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) + + //Shold be updated in Keychain + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser), + let keychainUpdatedCurrentDate = keychainUser.currentUser?.updatedAt else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + } catch { + XCTFail(error.localizedDescription) + } + + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 10.0) + } + + // swiftlint:disable:next function_body_length + func testFetchAllAsyncMainQueue() { + testUserLogin() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Fetch user1") + DispatchQueue.main.async { + guard var user = User.current else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + + user.updatedAt = user.updatedAt?.addingTimeInterval(+300) + user.customKey = "newValue" + let userOnServer = FindResult(results: [user], count: 1) + + let encoded: Data! + do { + encoded = try user.getEncoder(skipKeys: false).encode(userOnServer) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try user.getEncoder(skipKeys: false).encode(user) + user = try user.getDecoder().decode(User.self, from: encoded1) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + [user].fetchAll { results in + switch results { + + case .success(let fetched): + fetched.forEach { + switch $0 { + case .success(let fetched): + XCTAssert(fetched.hasSameObjectId(as: user)) + guard let fetchedCreatedAt = fetched.createdAt, + let fetchedUpdatedAt = fetched.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalCreatedAt = user.createdAt, + let originalUpdatedAt = user.updatedAt, + let serverUpdatedAt = user.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) + XCTAssertEqual(fetchedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(fetchedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(User.current?.customKey, user.customKey) + + //Should be updated in memory + guard let updatedCurrentDate = User.current?.updatedAt else { + XCTFail("Should unwrap current date") + expectation1.fulfill() + return + } + XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) + + //Shold be updated in Keychain + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser), + let keychainUpdatedCurrentDate = keychainUser.currentUser?.updatedAt else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 10.0) + } + + // swiftlint:disable:next function_body_length + func testSaveAll() { + testUserLogin() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Fetch user1") + + DispatchQueue.main.async { + guard var user = User.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + user.updatedAt = user.updatedAt?.addingTimeInterval(+300) + user.customKey = "newValue" + let userOnServer = [BatchResponseItem(success: user, error: nil)] + + let encoded: Data! + do { + encoded = try user.getEncoder(skipKeys: false).encode(userOnServer) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try user.getEncoder(skipKeys: false).encode(user) + user = try user.getDecoder().decode(User.self, from: encoded1) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + let saved = try [user].saveAll() + saved.forEach { + switch $0 { + case .success(let saved): + XCTAssert(saved.hasSameObjectId(as: user)) + guard let savedCreatedAt = saved.createdAt, + let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalCreatedAt = user.createdAt, + let originalUpdatedAt = user.updatedAt, + let serverUpdatedAt = user.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(savedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(User.current?.customKey, user.customKey) + + //Should be updated in memory + guard let updatedCurrentDate = User.current?.updatedAt else { + XCTFail("Should unwrap current date") + expectation1.fulfill() + return + } + XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) + + //Shold be updated in Keychain + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser), + let keychainUpdatedCurrentDate = keychainUser.currentUser?.updatedAt else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + } catch { + XCTFail(error.localizedDescription) + } + + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 10.0) + } + + // swiftlint:disable:next function_body_length + func testSaveAllAsyncMainQueue() { + testUserLogin() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Fetch user1") + DispatchQueue.main.async { + guard var user = User.current else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + + user.updatedAt = user.updatedAt?.addingTimeInterval(+300) + user.customKey = "newValue" + let userOnServer = [BatchResponseItem(success: user, error: nil)] + + let encoded: Data! + do { + encoded = try user.getEncoder(skipKeys: false).encode(userOnServer) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try user.getEncoder(skipKeys: false).encode(user) + user = try user.getDecoder().decode(User.self, from: encoded1) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + [user].saveAll { results in + switch results { + + case .success(let saved): + saved.forEach { + switch $0 { + case .success(let saved): + XCTAssert(saved.hasSameObjectId(as: user)) + guard let savedCreatedAt = saved.createdAt, + let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalCreatedAt = user.createdAt, + let originalUpdatedAt = user.updatedAt, + let serverUpdatedAt = user.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertEqual(savedUpdatedAt, serverUpdatedAt) + XCTAssertEqual(User.current?.customKey, user.customKey) + + //Should be updated in memory + guard let updatedCurrentDate = User.current?.updatedAt else { + XCTFail("Should unwrap current date") + expectation1.fulfill() + return + } + XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) + + //Shold be updated in Keychain + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser), + let keychainUpdatedCurrentDate = keychainUser.currentUser?.updatedAt else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 10.0) + } + + func testDeleteAll() { + testUserLogin() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Delete user1") + + DispatchQueue.main.async { + guard let user = User.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + let userOnServer = [BatchResponseItem(success: true, error: nil)] + + let encoded: Data! + do { + encoded = try user.getEncoder(skipKeys: false).encode(userOnServer) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + let deleted = try [user].deleteAll() + deleted.forEach { + switch $0 { + case .success: + return + case .failure(let error): + XCTFail("Should have deleted: \(error.localizedDescription)") + } + } + } catch { + XCTFail(error.localizedDescription) + } + + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 10.0) + } + + func testDeleteAllAsyncMainQueue() { + testUserLogin() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Delete user1") + DispatchQueue.main.async { + guard let user = User.current else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + + let userOnServer = [BatchResponseItem(success: true, error: nil)] + + let encoded: Data! + do { + encoded = try user.getEncoder(skipKeys: false).encode(userOnServer) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + [user].deleteAll { results in + switch results { + + case .success(let deleted): + deleted.forEach { + switch $0 { + case .success: + return + case .failure(let error): + XCTFail("Should have deleted: \(error.localizedDescription)") + } + } + case .failure(let error): + XCTFail("Should have deleted: \(error.localizedDescription)") + } + expectation1.fulfill() + } + } + wait(for: [expectation1], timeout: 10.0) + } } // swiftlint:disable:this file_length diff --git a/carthage.sh b/carthage.sh new file mode 100755 index 000000000..7a062998e --- /dev/null +++ b/carthage.sh @@ -0,0 +1,19 @@ +# carthage.sh +# Usage example: ./carthage.sh build --platform iOS + +set -euo pipefail + +xcconfig=$(mktemp /tmp/static.xcconfig.XXXXXX) +trap 'rm -f "$xcconfig"' INT TERM HUP EXIT + +# For Xcode 12 make sure EXCLUDED_ARCHS is set to arm architectures otherwise +# the build will fail on lipo due to duplicate architectures. + +CURRENT_XCODE_VERSION=$(xcodebuild -version | grep "Build version" | cut -d' ' -f3) +echo "EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_$CURRENT_XCODE_VERSION = arm64 arm64e armv7 armv7s armv6 armv8" >> $xcconfig + +echo 'EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200 = $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_simulator__NATIVE_ARCH_64_BIT_x86_64__XCODE_1200__BUILD_$(XCODE_PRODUCT_BUILD_VERSION))' >> $xcconfig +echo 'EXCLUDED_ARCHS = $(inherited) $(EXCLUDED_ARCHS__EFFECTIVE_PLATFORM_SUFFIX_$(EFFECTIVE_PLATFORM_SUFFIX)__NATIVE_ARCH_64_BIT_$(NATIVE_ARCH_64_BIT)__XCODE_$(XCODE_VERSION_MAJOR))' >> $xcconfig + +export XCODE_XCCONFIG_FILE="$xcconfig" +carthage "$@"