diff --git a/.codecov.yml b/.codecov.yml index 5148b8498..9d8475330 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -9,6 +9,6 @@ coverage: changes: false project: default: - target: 76 + target: 80 comment: require_changes: true diff --git a/CHANGELOG.md b/CHANGELOG.md index d151930dc..343d37d2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,20 @@ # Parse-Swift Changelog ### main -[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.2.1...main) +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.2.2...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ + +### 1.2.2 +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.2.1...1.2.2) + __New features__ +- Allow custom objectIds ([#100](https://github.com/parse-community/Parse-Swift/pull/100)), thanks to [Corey Baker](https://github.com/cbaker6). - Add ParseTwitter and ParseFacebook authentication ([#97](https://github.com/parse-community/Parse-Swift/pull/97)), thanks to [Abdulaziz Alhomaidhi](https://github.com/abs8090). +- Add build support for Android ([#90](https://github.com/parse-community/Parse-Swift/pull/90)), thanks to [jt9897253](https://github.com/jt9897253). + +__Fixes__ +- There was another bug after a user first logs in anonymously and then becomes a real user. The authData sent to the server wasn't stripped, keep the user anonymous instead of making them a real user ([#100](https://github.com/parse-community/Parse-Swift/pull/100)), thanks to [Corey Baker](https://github.com/cbaker6). ### 1.2.1 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.2.0...1.2.1) diff --git a/ParseSwift.podspec b/ParseSwift.podspec index 28f5974bb..455ccb4ef 100644 --- a/ParseSwift.podspec +++ b/ParseSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "ParseSwift" - s.version = "1.2.1" + s.version = "1.2.2" s.summary = "Parse Pure Swift SDK" s.homepage = "https://github.com/parse-community/Parse-Swift" s.authors = { diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 4f8527e9c..96ee5180d 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -197,6 +197,9 @@ 70647E9D259E3A9A004C1004 /* ParseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E9B259E3A9A004C1004 /* ParseType.swift */; }; 70647E9E259E3A9A004C1004 /* ParseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E9B259E3A9A004C1004 /* ParseType.swift */; }; 70647E9F259E3A9A004C1004 /* ParseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E9B259E3A9A004C1004 /* ParseType.swift */; }; + 70732C5A2606CCAD000CAB81 /* ParseObjectCustomObjectId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectId.swift */; }; + 70732C5B2606CCAD000CAB81 /* ParseObjectCustomObjectId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectId.swift */; }; + 70732C5C2606CCAD000CAB81 /* ParseObjectCustomObjectId.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectId.swift */; }; 707A3BF125B0A4F0000D215C /* ParseAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3BF025B0A4F0000D215C /* ParseAuthentication.swift */; }; 707A3BF225B0A4F0000D215C /* ParseAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3BF025B0A4F0000D215C /* ParseAuthentication.swift */; }; 707A3BF325B0A4F0000D215C /* ParseAuthentication.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3BF025B0A4F0000D215C /* ParseAuthentication.swift */; }; @@ -599,6 +602,7 @@ 705D950725BE4C08003EF6F8 /* SubscriptionCallback.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SubscriptionCallback.swift; sourceTree = ""; }; 70647E8D259E3375004C1004 /* LocallyIdentifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocallyIdentifiable.swift; sourceTree = ""; }; 70647E9B259E3A9A004C1004 /* ParseType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseType.swift; sourceTree = ""; }; + 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectId.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseObjectCustomObjectId.swift; sourceTree = ""; }; 707A3BF025B0A4F0000D215C /* ParseAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAuthentication.swift; sourceTree = ""; }; 707A3C1025B0A8E8000D215C /* ParseAnonymous.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAnonymous.swift; sourceTree = ""; }; 707A3C1F25B14BCF000D215C /* ParseApple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseApple.swift; sourceTree = ""; }; @@ -830,6 +834,7 @@ 7003963A25A288100052CB31 /* ParseLiveQueryTests.swift */, 70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */, 7044C1DE25C5C70D0011F6E7 /* ParseObjectCombine.swift */, + 70732C592606CCAD000CAB81 /* ParseObjectCustomObjectId.swift */, 911DB13524C4FC100027F3C7 /* ParseObjectTests.swift */, 7044C1EB25C5CC930011F6E7 /* ParseOperationCombineTests.swift */, 70C5508425B4A68700B5DBC2 /* ParseOperationTests.swift */, @@ -1660,6 +1665,7 @@ 89899D772603CF66002E2043 /* ParseFacebookTests.swift in Sources */, 70386A4625D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */, 911DB12E24C4837E0027F3C7 /* APICommandTests.swift in Sources */, + 70732C5A2606CCAD000CAB81 /* ParseObjectCustomObjectId.swift in Sources */, 911DB12C24C3F7720027F3C7 /* MockURLResponse.swift in Sources */, 7044C24325C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, 7044C1DF25C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, @@ -1810,6 +1816,7 @@ 89899D822603CF67002E2043 /* ParseFacebookTests.swift in Sources */, 70386A4825D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */, 709B984C2556ECAA00507778 /* APICommandTests.swift in Sources */, + 70732C5C2606CCAD000CAB81 /* ParseObjectCustomObjectId.swift in Sources */, 709B984D2556ECAA00507778 /* AnyDecodableTests.swift in Sources */, 7044C24525C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, 7044C1E125C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, @@ -1863,6 +1870,7 @@ 89899D812603CF67002E2043 /* ParseFacebookTests.swift in Sources */, 70386A4725D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */, 70F2E2B5254F283000B2EA5C /* ParseEncoderTests.swift in Sources */, + 70732C5B2606CCAD000CAB81 /* ParseObjectCustomObjectId.swift in Sources */, 70F2E2C2254F283000B2EA5C /* APICommandTests.swift in Sources */, 7044C24425C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, 7044C1E025C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, @@ -2313,7 +2321,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.2.2; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SKIP_INSTALL = YES; @@ -2337,7 +2345,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; IPHONEOS_DEPLOYMENT_TARGET = 12.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.2.2; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SKIP_INSTALL = YES; @@ -2403,7 +2411,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.2.2; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SDKROOT = macosx; @@ -2429,7 +2437,7 @@ INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/../Frameworks @loader_path/Frameworks"; MACOSX_DEPLOYMENT_TARGET = 10.13; - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.2.2; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SDKROOT = macosx; @@ -2576,7 +2584,7 @@ INFOPLIST_FILE = "ParseSwift-watchOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.2.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-watchOS"; @@ -2605,7 +2613,7 @@ INFOPLIST_FILE = "ParseSwift-watchOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.2.2; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-watchOS"; PRODUCT_NAME = ParseSwift; @@ -2632,7 +2640,7 @@ INFOPLIST_FILE = "ParseSwift-tvOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.2.2; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-tvOS"; @@ -2660,7 +2668,7 @@ INFOPLIST_FILE = "ParseSwift-tvOS/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; - MARKETING_VERSION = 1.2.1; + MARKETING_VERSION = 1.2.2; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-tvOS"; PRODUCT_NAME = ParseSwift; diff --git a/Scripts/jazzy.sh b/Scripts/jazzy.sh index d2c4bb938..3427498ca 100755 --- a/Scripts/jazzy.sh +++ b/Scripts/jazzy.sh @@ -5,7 +5,7 @@ bundle exec jazzy \ --author_url http://parseplatform.org \ --github_url https://github.com/parse-community/Parse-Swift \ --root-url http://parseplatform.org/Parse-Swift/api/ \ - --module-version 1.2.1 \ + --module-version 1.2.2 \ --theme fullwidth \ --skip-undocumented \ --output ./docs/api \ diff --git a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift index 06cef283a..2e24a19c4 100644 --- a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift +++ b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift @@ -217,8 +217,12 @@ public extension ParseUser { static func login(_ type: String, authData: [String: String], options: API.Options) throws -> Self { - let body = SignupLoginBody(authData: [type: authData]) - return try signupCommand(body: body).execute(options: options) + if Self.current != nil { + return try Self.link(type, authData: authData, options: options) + } else { + let body = SignupLoginBody(authData: [type: authData]) + return try signupCommand(body: body).execute(options: options) + } } /** @@ -238,22 +242,28 @@ public extension ParseUser { options: API.Options, callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { - - let body = SignupLoginBody(authData: [type: authData]) - do { - try signupCommand(body: body) - .executeAsync(options: options) { result in - callbackQueue.async { - completion(result) + if Self.current != nil { + Self.link(type, authData: authData, + options: options, + callbackQueue: callbackQueue, + completion: completion) + } else { + let body = SignupLoginBody(authData: [type: authData]) + do { + try signupCommand(body: body) + .executeAsync(options: options) { result in + callbackQueue.async { + completion(result) + } + } + } catch { + callbackQueue.async { + if let parseError = error as? ParseError { + completion(.failure(parseError)) + } else { + let parseError = ParseError(code: .unknownError, message: error.localizedDescription) + completion(.failure(parseError)) } - } - } catch { - callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - let parseError = ParseError(code: .unknownError, message: error.localizedDescription) - completion(.failure(parseError)) } } } @@ -384,29 +394,47 @@ public extension ParseUser { } } + internal func linkCommand() -> API.NonParseBodyCommand { + Self.current?.anonymous.strip() + return API.NonParseBodyCommand(method: .PUT, + path: endpoint, + body: Self.current) { (data) -> Self in + let user = try ParseCoding.jsonDecoder().decode(UpdateSessionTokenResponse.self, from: data) + Self.current?.updatedAt = user.updatedAt + guard let current = Self.current else { + throw ParseError(code: .unknownError, message: "Should have a current user.") + } + Self.currentUserContainer = .init(currentUser: current, + sessionToken: user.sessionToken) + Self.saveCurrentContainerToKeychain() + return current + } + } + internal func linkCommand(body: SignupLoginBody) -> API.NonParseBodyCommand { + var body = body + Self.current?.anonymous.strip() + if var currentAuthData = Self.current?.authData { + if let bodyAuthData = body.authData { + bodyAuthData.forEach { (key, value) in + currentAuthData[key] = value + } + } + body.authData = currentAuthData + } return API.NonParseBodyCommand(method: .PUT, path: endpoint, body: body) { (data) -> Self in - let user = try ParseCoding.jsonDecoder().decode(Self.self, from: data) - if let authData = body.authData { - Self.current?.anonymous.strip() - if Self.current?.authData == nil { - Self.current?.authData = authData - } else { - authData.forEach { (key, value) in - Self.current?.authData?[key] = value - } - } - if let updatedAt = user.updatedAt { - Self.current?.updatedAt = updatedAt - } - } - Self.saveCurrentContainerToKeychain() + let user = try ParseCoding.jsonDecoder().decode(UpdateSessionTokenResponse.self, from: data) + Self.current?.updatedAt = user.updatedAt + Self.current?.authData = body.authData guard let current = Self.current else { throw ParseError(code: .unknownError, message: "Should have a current user.") } + Self.currentUserContainer = .init(currentUser: current, + sessionToken: user.sessionToken) + Self.saveCurrentContainerToKeychain() return current } } diff --git a/Sources/ParseSwift/Coding/ParseEncoder.swift b/Sources/ParseSwift/Coding/ParseEncoder.swift index 71e2eea94..f241b4200 100644 --- a/Sources/ParseSwift/Coding/ParseEncoder.swift +++ b/Sources/ParseSwift/Coding/ParseEncoder.swift @@ -56,6 +56,7 @@ public struct ParseEncoder { case object case cloud case none + case customObjectId case custom(Set) func keys() -> Set { @@ -63,6 +64,8 @@ public struct ParseEncoder { case .object: return Set(["createdAt", "updatedAt", "objectId", "className"]) + case .customObjectId: + return Set(["createdAt", "updatedAt", "className"]) case .cloud: return Set(["functionJobName"]) case .none: @@ -99,7 +102,13 @@ public struct ParseEncoder { internal func encode(_ value: T, objectsSavedBeforeThisOne: [String: PointerType]?, filesSavedBeforeThisOne: [UUID: ParseFile]?) throws -> (encoded: Data, unique: Set, unsavedChildren: [Encodable]) { - let encoder = _ParseEncoder(codingPath: [], dictionary: NSMutableDictionary(), skippingKeys: SkippedKeys.object.keys()) + let keysToSkip: Set! + if !ParseConfiguration.allowCustomObjectId { + keysToSkip = SkippedKeys.object.keys() + } else { + keysToSkip = SkippedKeys.customObjectId.keys() + } + let encoder = _ParseEncoder(codingPath: [], dictionary: NSMutableDictionary(), skippingKeys: keysToSkip) if let dateEncodingStrategy = dateEncodingStrategy { encoder.dateEncodingStrategy = dateEncodingStrategy } @@ -110,7 +119,13 @@ public struct ParseEncoder { internal func encode(_ value: ParseType, collectChildren: Bool, objectsSavedBeforeThisOne: [String: PointerType]?, filesSavedBeforeThisOne: [UUID: ParseFile]?) throws -> (encoded: Data, unique: Set, unsavedChildren: [Encodable]) { - let encoder = _ParseEncoder(codingPath: [], dictionary: NSMutableDictionary(), skippingKeys: SkippedKeys.object.keys()) + let keysToSkip: Set! + if !ParseConfiguration.allowCustomObjectId { + keysToSkip = SkippedKeys.object.keys() + } else { + keysToSkip = SkippedKeys.customObjectId.keys() + } + let encoder = _ParseEncoder(codingPath: [], dictionary: NSMutableDictionary(), skippingKeys: keysToSkip) if let dateEncodingStrategy = dateEncodingStrategy { encoder.dateEncodingStrategy = dateEncodingStrategy } diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 28a061d8e..a7bf2ac0d 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -476,10 +476,17 @@ extension ParseUser { - returns: Returns whether the sign up was successful. */ public static func signup(username: String, - password: String, options: API.Options = []) throws -> Self { - try signupCommand(body: SignupLoginBody(username: username, - password: password)) - .execute(options: options) + password: String, + options: API.Options = []) throws -> Self { + let body = SignupLoginBody(username: username, + password: password) + if let current = Self.current { + return try current.linkCommand(body: body) + .execute(options: options) + } else { + return try signupCommand(body: body) + .execute(options: options) + } } /** @@ -492,8 +499,13 @@ extension ParseUser { - returns: Returns whether the sign up was successful. */ public func signup(options: API.Options = []) throws -> Self { - try signupCommand().execute(options: options, - callbackQueue: .main) + if let current = Self.current { + return try current.linkCommand() + .execute(options: options) + } else { + return try signupCommand().execute(options: options, + callbackQueue: .main) + } } /** @@ -509,21 +521,30 @@ extension ParseUser { */ public func signup(options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { - do { - try signupCommand() - .executeAsync(options: options, - callbackQueue: callbackQueue) { result in - callbackQueue.async { - completion(result) + if let current = Self.current { + current.linkCommand() + .executeAsync(options: options) { result in + callbackQueue.async { + completion(result) + } } - } - } catch { - callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - let parseError = ParseError(code: .unknownError, message: error.localizedDescription) - completion(.failure(parseError)) + } else { + do { + try signupCommand() + .executeAsync(options: options, + callbackQueue: callbackQueue) { result in + callbackQueue.async { + completion(result) + } + } + } catch { + callbackQueue.async { + if let parseError = error as? ParseError { + completion(.failure(parseError)) + } else { + let parseError = ParseError(code: .unknownError, message: error.localizedDescription) + completion(.failure(parseError)) + } } } } @@ -548,79 +569,52 @@ extension ParseUser { options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { + let body = SignupLoginBody(username: username, password: password) - do { - try signupCommand(body: body) + if let current = Self.current { + current.linkCommand(body: body) .executeAsync(options: options) { result in - callbackQueue.async { - completion(result) + callbackQueue.async { + completion(result) + } } - } - } catch { - callbackQueue.async { - if let parseError = error as? ParseError { - completion(.failure(parseError)) - } else { - let parseError = ParseError(code: .unknownError, message: error.localizedDescription) - completion(.failure(parseError)) + } else { + do { + try signupCommand(body: body) + .executeAsync(options: options) { result in + callbackQueue.async { + completion(result) + } + } + } catch { + callbackQueue.async { + if let parseError = error as? ParseError { + completion(.failure(parseError)) + } else { + let parseError = ParseError(code: .unknownError, message: error.localizedDescription) + completion(.failure(parseError)) + } } } } } internal static func signupCommand(body: SignupLoginBody) throws -> API.NonParseBodyCommand { + API.NonParseBodyCommand(method: .POST, + path: .users, body: body) { (data) -> Self in - var method = API.Method.POST - var path = API.Endpoint.users - if let current = Self.current { - if current.anonymous.isLinked { - Self.current!.anonymous.strip() - method = .PUT - path = current.endpoint - } else { - throw ParseError(code: .usernameTaken, - message: "Cannot sign up a user that has already signed up.") - } - } - - return API.NonParseBodyCommand(method: method, path: path, body: body) { (data) -> Self in - var user: Self! - var sessionToken: String! - - if method == .POST { - sessionToken = try ParseCoding.jsonDecoder().decode(LoginSignupResponse.self, from: data).sessionToken - user = try ParseCoding.jsonDecoder().decode(Self.self, from: data) + let sessionToken = try ParseCoding.jsonDecoder() + .decode(LoginSignupResponse.self, from: data).sessionToken + var user = try ParseCoding.jsonDecoder().decode(Self.self, from: data) - if user.username == nil { - if let username = body.username { - user.username = username - } - } - if user.authData == nil { - if let authData = body.authData { - user.authData = authData - } + if user.username == nil { + if let username = body.username { + user.username = username } - } else { - if let currentUser = Self.current { - let response = try ParseCoding.jsonDecoder().decode(UpdateSessionTokenResponse.self, from: data) - user = currentUser - if user.authData == nil { - user.authData = body.authData - } else { - if user.authData != body.authData { - if let authData = body.authData { - for (key, value) in authData { - user.authData![key] = value - } - } - } - } - user.updatedAt = response.updatedAt - sessionToken = response.sessionToken - } else { - throw ParseError(code: .usernameTaken, - message: "Cannot link user when current user is not logged in.") + } + if user.authData == nil { + if let authData = body.authData { + user.authData = authData } } Self.currentUserContainer = .init(currentUser: user, @@ -631,38 +625,15 @@ extension ParseUser { } internal func signupCommand() throws -> API.Command { - var method = API.Method.POST - if let currentUser = Self.current { - if currentUser.anonymous.isLinked { - Self.current!.anonymous.strip() - method = .PUT - } else { - throw ParseError(code: .usernameTaken, - message: "Cannot sign up a user that has already signed up.") - } - } - return API.Command(method: method, - path: endpoint, - body: self) { (data) -> Self in - var user: Self! - var sessionToken: String! - - if method == .POST { - sessionToken = try ParseCoding.jsonDecoder().decode(LoginSignupResponse.self, from: data).sessionToken - user = try ParseCoding.jsonDecoder().decode(Self.self, from: data) - user.username = self.username - } else { - if let currentUser = Self.current { - let response = try ParseCoding.jsonDecoder().decode(UpdateSessionTokenResponse.self, from: data) - user = currentUser - user.updatedAt = response.updatedAt - sessionToken = response.sessionToken - } else { - throw ParseError(code: .usernameTaken, - message: "Cannot link user when current user is not logged in.") - } - } + API.Command(method: .POST, + path: endpoint, + body: self) { (data) -> Self in + + let sessionToken = try ParseCoding.jsonDecoder() + .decode(LoginSignupResponse.self, from: data).sessionToken + var user = try ParseCoding.jsonDecoder().decode(Self.self, from: data) + user.username = self.username Self.currentUserContainer = .init( currentUser: user, sessionToken: sessionToken diff --git a/Sources/ParseSwift/Parse.swift b/Sources/ParseSwift/Parse.swift index 9c483f293..09c65e064 100644 --- a/Sources/ParseSwift/Parse.swift +++ b/Sources/ParseSwift/Parse.swift @@ -13,6 +13,7 @@ internal struct ParseConfiguration { static var liveQuerysServerURL: URL? static var mountPath: String! static var sessionDelegate: ParseURLSessionDelegate! + static var allowCustomObjectId = false static var isTestingSDK = false //Enable this only for certain tests like ParseFile } @@ -29,6 +30,8 @@ public struct ParseSwift { - parameter masterKey: The master key of your Parse application. - parameter serverURL: The server URL to connect to Parse Server. - parameter liveQueryServerURL: The server URL to connect to Parse Server. + - parameter allowCustomObjectId: Allows objectIds to be created on the client + side for each object. Must be enabled on the server to work. - parameter keyValueStore: A key/value store that conforms to the `ParseKeyValueStore` protocol. Defaults to `nil` in which one will be created an memory, but never persisted. For Linux, this this is the only store available since there is no Keychain. Linux users should replace this store with an @@ -45,6 +48,7 @@ public struct ParseSwift { masterKey: String? = nil, serverURL: URL, liveQueryServerURL: URL? = nil, + allowCustomObjectId: Bool = false, keyValueStore: ParseKeyValueStore? = nil, authentication: ((URLAuthenticationChallenge, (URLSession.AuthChallengeDisposition, @@ -55,6 +59,7 @@ public struct ParseSwift { ParseConfiguration.masterKey = masterKey ParseConfiguration.serverURL = serverURL ParseConfiguration.liveQuerysServerURL = liveQueryServerURL + ParseConfiguration.allowCustomObjectId = allowCustomObjectId ParseConfiguration.mountPath = "/" + serverURL.pathComponents .filter { $0 != "/" } .joined(separator: "/") @@ -71,6 +76,7 @@ public struct ParseSwift { masterKey: String? = nil, serverURL: URL, liveQueryServerURL: URL? = nil, + allowCustomObjectId: Bool = false, primitiveObjectStore: ParseKeyValueStore? = nil, testing: Bool = false, authentication: ((URLAuthenticationChallenge, @@ -83,6 +89,7 @@ public struct ParseSwift { masterKey: masterKey, serverURL: serverURL, liveQueryServerURL: liveQueryServerURL, + allowCustomObjectId: allowCustomObjectId, authentication: authentication) } } diff --git a/Sources/ParseSwift/ParseConstants.swift b/Sources/ParseSwift/ParseConstants.swift index 2adf55b2b..e402bed63 100644 --- a/Sources/ParseSwift/ParseConstants.swift +++ b/Sources/ParseSwift/ParseConstants.swift @@ -9,7 +9,7 @@ import Foundation enum ParseConstants { - static let parseVersion = "1.2.1" + static let parseVersion = "1.2.2" static let hashingKey = "parseSwift" static let fileManagementDirectory = "parse/" static let fileManagementPrivateDocumentsDirectory = "Private Documents/" diff --git a/Sources/ParseSwift/Protocols/Objectable.swift b/Sources/ParseSwift/Protocols/Objectable.swift index 5d5991e2c..62e138cff 100644 --- a/Sources/ParseSwift/Protocols/Objectable.swift +++ b/Sources/ParseSwift/Protocols/Objectable.swift @@ -75,7 +75,11 @@ extension Objectable { } var isSaved: Bool { - return objectId != nil + if !ParseConfiguration.allowCustomObjectId { + return objectId != nil + } else { + return createdAt != nil + } } func toPointer() throws -> PointerType { diff --git a/Tests/ParseSwiftTests/ParseAnonymousTests.swift b/Tests/ParseSwiftTests/ParseAnonymousTests.swift index 824a035d2..146df9758 100644 --- a/Tests/ParseSwiftTests/ParseAnonymousTests.swift +++ b/Tests/ParseSwiftTests/ParseAnonymousTests.swift @@ -276,47 +276,41 @@ class ParseAnonymousTests: XCTestCase { wait(for: [expectation1], timeout: 20.0) } - func testReplaceAnonymousWithUser() throws { - let expectedAuth = ["id": "yolo"] - var newUser = User() - newUser.authData = [newUser.anonymous.__type: expectedAuth] - newUser.username = "hello" - newUser.password = "world" - XCTAssertTrue(newUser.anonymous.isLinked(with: newUser)) - - //: Convert the anonymous user to a real new user. - var serverResponse = LoginSignupResponse() - serverResponse.username = "hello" - serverResponse.password = "world" - serverResponse.objectId = "yarr" - serverResponse.sessionToken = "myToken" - serverResponse.authData = [serverResponse.anonymous.__type: nil] - serverResponse.createdAt = Date() - serverResponse.updatedAt = serverResponse.createdAt?.addingTimeInterval(+300) + func testReplaceAnonymousUser() throws { + try testLogin() + guard let user = User.current, + let updatedAt = user.updatedAt else { + XCTFail("Shold have unwrapped") + return + } + XCTAssertTrue(user.anonymous.isLinked) - var userOnServer: User! + var response = UpdateSessionTokenResponse(updatedAt: updatedAt.addingTimeInterval(+300), + sessionToken: "blast") let encoded: Data! do { - encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + encoded = try ParseCoding.jsonEncoder().encode(response) //Get dates in correct format from ParseDecoding strategy - userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + response = try ParseCoding.jsonDecoder().decode(UpdateSessionTokenResponse.self, from: encoded) } catch { XCTFail("Should encode/decode. Error \(error)") return } + MockURLProtocol.removeAll() MockURLProtocol.mockRequests { _ in return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } let expectation1 = XCTestExpectation(description: "Login") - newUser.anonymous.login { result in + User.current?.username = "hello" + User.current?.password = "world" + User.current?.signup { result in switch result { case .success(let user): XCTAssertEqual(user, User.current) - XCTAssertEqual(user, userOnServer) XCTAssertEqual(user.username, "hello") XCTAssertEqual(user.password, "world") XCTAssertFalse(user.anonymous.isLinked) @@ -328,7 +322,7 @@ class ParseAnonymousTests: XCTestCase { wait(for: [expectation1], timeout: 20.0) } - func testReplaceAnonymousUser() throws { + func testReplaceAnonymousUserBody() throws { try testLogin() guard let user = User.current, let updatedAt = user.updatedAt else { @@ -356,7 +350,8 @@ class ParseAnonymousTests: XCTestCase { let expectation1 = XCTestExpectation(description: "Login") - User.current?.signup { result in + User.signup(username: "hello", + password: "world") { result in switch result { case .success(let user): @@ -372,6 +367,78 @@ class ParseAnonymousTests: XCTestCase { wait(for: [expectation1], timeout: 20.0) } + func testReplaceAnonymousUserSync() throws { + try testLogin() + guard let user = User.current, + let updatedAt = user.updatedAt else { + XCTFail("Shold have unwrapped") + return + } + XCTAssertTrue(user.anonymous.isLinked) + + var response = UpdateSessionTokenResponse(updatedAt: updatedAt.addingTimeInterval(+300), + sessionToken: "blast") + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + //Get dates in correct format from ParseDecoding strategy + response = try ParseCoding.jsonDecoder().decode(UpdateSessionTokenResponse.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.removeAll() + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + User.current?.username = "hello" + User.current?.password = "world" + guard let signedInUser = try User.current?.signup() else { + XCTFail("Shouuld have unwrapped") + return + } + XCTAssertEqual(signedInUser, User.current) + XCTAssertEqual(signedInUser.username, "hello") + XCTAssertEqual(signedInUser.password, "world") + XCTAssertFalse(signedInUser.anonymous.isLinked) + } + + func testReplaceAnonymousUserBodySync() throws { + try testLogin() + guard let user = User.current, + let updatedAt = user.updatedAt else { + XCTFail("Shold have unwrapped") + return + } + XCTAssertTrue(user.anonymous.isLinked) + + var response = UpdateSessionTokenResponse(updatedAt: updatedAt.addingTimeInterval(+300), + sessionToken: "blast") + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + //Get dates in correct format from ParseDecoding strategy + response = try ParseCoding.jsonDecoder().decode(UpdateSessionTokenResponse.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.removeAll() + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let signedInUser = try User.signup(username: "hello", + password: "world") + XCTAssertEqual(signedInUser, User.current) + XCTAssertEqual(signedInUser.username, "hello") + XCTAssertEqual(signedInUser.password, "world") + XCTAssertFalse(signedInUser.anonymous.isLinked) + } + func testReplaceAnonymousWithBecome() throws { // swiftlint:disable:this function_body_length XCTAssertNil(User.current?.objectId) try testLogin() diff --git a/Tests/ParseSwiftTests/ParseAuthenticationTests.swift b/Tests/ParseSwiftTests/ParseAuthenticationTests.swift index 144271f3a..0ac278668 100644 --- a/Tests/ParseSwiftTests/ParseAuthenticationTests.swift +++ b/Tests/ParseSwiftTests/ParseAuthenticationTests.swift @@ -30,6 +30,36 @@ class ParseAuthenticationTests: XCTestCase { var authData: [String: [String: String]?]? } + struct LoginSignupResponse: ParseUser { + + var objectId: String? + var createdAt: Date? + var sessionToken: String + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + + // Your custom keys + var customKey: String? + + init() { + let date = Date() + self.createdAt = date + self.updatedAt = date + self.objectId = "yarr" + self.ACL = nil + self.customKey = "blah" + self.sessionToken = "myToken" + self.username = "hello10" + self.email = "hello@parse.com" + } + } + struct TestAuth: ParseAuthentication { static var __type: String { // swiftlint:disable:this identifier_name "test" @@ -94,21 +124,64 @@ class ParseAuthenticationTests: XCTestCase { try ParseStorage.shared.deleteAll() } - func testLinkCommand() { - var user = User() - let objectId = "yarr" - user.objectId = objectId + func loginNormally() throws -> User { + let loginResponse = LoginSignupResponse() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + return try User.login(username: "parse", password: "user") + } + + func testLinkCommand() throws { + let user = User() let body = SignupLoginBody(authData: ["test": ["id": "yolo"]]) + let command = user.linkCommand(body: body) + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/users") + XCTAssertEqual(command.method, API.Method.PUT) + XCTAssertNotNil(command.body) + XCTAssertEqual(command.body?.authData, body.authData) + } + + func testLinkCommandNoBody() throws { + var user = User() + user.username = "hello" + user.password = "world" + let command = user.linkCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/users") + XCTAssertEqual(command.method, API.Method.PUT) + XCTAssertNil(command.body) + XCTAssertNil(command.body?.authData) + } + func testLinkCommandLoggedIn() throws { + let user = try loginNormally() + let body = SignupLoginBody(authData: ["test": ["id": "yolo"]]) let command = user.linkCommand(body: body) XCTAssertNotNil(command) - XCTAssertEqual(command.path.urlComponent, "/users/\(objectId)") + XCTAssertEqual(command.path.urlComponent, "/users/\("yarr")") XCTAssertEqual(command.method, API.Method.PUT) XCTAssertNotNil(command.body) XCTAssertEqual(command.body?.authData, body.authData) } + func testLinkCommandNoBodyLoggedIn() throws { + let user = try loginNormally() + let command = user.linkCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/users/\("yarr")") + XCTAssertEqual(command.method, API.Method.PUT) + XCTAssertNotNil(command.body) + XCTAssertNil(command.body?.authData) + } + func testIsLinkedWithString() throws { let expectedAuth = ["id": "yolo"] diff --git a/Tests/ParseSwiftTests/ParseInstallationTests.swift b/Tests/ParseSwiftTests/ParseInstallationTests.swift index a1da0e00a..f5d6521f0 100644 --- a/Tests/ParseSwiftTests/ParseInstallationTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationTests.swift @@ -923,7 +923,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l wait(for: [expectation1], timeout: 20.0) } - func testSaveCommand() { + func testSaveCommand() throws { let installation = Installation() let command = installation.saveCommand() XCTAssertNotNil(command) @@ -933,7 +933,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l XCTAssertNotNil(command.body) } - func testUpdateCommand() { + func testUpdateCommand() throws { var installation = Installation() let objectId = "yarr" installation.objectId = objectId @@ -1067,7 +1067,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) - #if !os(Linux) + #if !os(Linux) && !os(Android) //Should be updated in Keychain guard let keychainInstallation: CurrentInstallationContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation), @@ -1213,7 +1213,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l return } XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) - #if !os(Linux) + #if !os(Linux) && !os(Android) //Should be updated in Keychain guard let keychainInstallation: CurrentInstallationContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation), diff --git a/Tests/ParseSwiftTests/ParseObjectCustomObjectId.swift b/Tests/ParseSwiftTests/ParseObjectCustomObjectId.swift new file mode 100644 index 000000000..8f1c893c3 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseObjectCustomObjectId.swift @@ -0,0 +1,1377 @@ +// +// ParseObjectCustomObjectId.swift +// ParseSwift +// +// Created by Corey Baker on 3/20/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation +import XCTest +@testable import ParseSwift + +class ParseObjectCustomObjectId: XCTestCase { // swiftlint:disable:this type_body_length + struct Level: ParseObject { + var objectId: String? + + var createdAt: Date? + + var updatedAt: Date? + + var ACL: ParseACL? + + var name = "First" + } + + struct GameScore: ParseObject { + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: Your own properties + var score: Int? + var player: String? + var level: Level? + var levels: [Level]? + + //custom initializers + init (objectId: String?) { + self.objectId = objectId + } + init(score: Int) { + self.score = score + self.player = "Jen" + } + init(score: Int, name: String) { + self.score = score + self.player = name + } + } + + struct Game: ParseObject { + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + //: Your own properties + var score: GameScore + var scores = [GameScore]() + var name = "Hello" + var profilePicture: ParseFile? + + //: a custom initializer + init(score: GameScore) { + self.score = score + } + } + + struct User: ParseUser { + + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + + // Your custom keys + var customKey: String? + } + + struct Installation: ParseInstallation { + var installationId: String? + var deviceType: String? + var deviceToken: String? + var badge: Int? + var timeZone: String? + var channels: [String]? + var appName: String? + var appIdentifier: String? + var appVersion: String? + var parseVersion: String? + var localeIdentifier: String? + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + var customKey: String? + } + + override func setUpWithError() throws { + try super.setUpWithError() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + allowCustomObjectId: true, + testing: true) + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + MockURLProtocol.removeAll() + #if !os(Linux) && !os(Android) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + + guard let fileManager = ParseFileManager(), + let defaultDirectoryPath = fileManager.defaultDataDirectoryPath else { + throw ParseError(code: .unknownError, message: "Should have initialized file manage") + } + + let directory2 = defaultDirectoryPath + .appendingPathComponent(ParseConstants.fileDownloadsDirectory, isDirectory: true) + let expectation2 = XCTestExpectation(description: "Delete files2") + fileManager.removeDirectoryContents(directory2) { _ in + expectation2.fulfill() + } + wait(for: [expectation2], timeout: 20.0) + } + + #if !os(Linux) && !os(Android) + func testSaveCommand() throws { + let objectId = "yarr" + var score = GameScore(score: 10) + score.objectId = objectId + let className = score.className + + let command = score.saveCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/classes/\(className)/\(objectId)") + XCTAssertEqual(command.method, API.Method.POST) + XCTAssertNil(command.params) + XCTAssertNotNil(command.data) + + guard let body = command.body else { + XCTFail("Should be able to unwrap") + return + } + + let expected = "{\"score\":10,\"player\":\"Jen\",\"objectId\":\"yarr\"}" + let encoded = try ParseCoding.parseEncoder() + .encode(body, collectChildren: false, + objectsSavedBeforeThisOne: nil, + filesSavedBeforeThisOne: nil).encoded + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + + func testUpdateCommand() throws { + var score = GameScore(score: 10) + let className = score.className + let objectId = "yarr" + score.objectId = objectId + score.createdAt = Date() + score.updatedAt = score.createdAt + + let command = score.saveCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/classes/\(className)/\(objectId)") + XCTAssertEqual(command.method, API.Method.PUT) + XCTAssertNil(command.params) + XCTAssertNotNil(command.data) + + guard let body = command.body else { + XCTFail("Should be able to unwrap") + return + } + + let expected = "{\"score\":10,\"player\":\"Jen\",\"objectId\":\"yarr\"}" + let encoded = try ParseCoding.parseEncoder() + .encode(body, collectChildren: false, + objectsSavedBeforeThisOne: nil, + filesSavedBeforeThisOne: nil).encoded + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + + func testSaveAllCommand() throws { + var score = GameScore(score: 10) + score.objectId = "yarr" + var score2 = GameScore(score: 20) + score2.objectId = "yolo" + + let objects = [score, score2] + let commands = objects.map { $0.saveCommand() } + let body = BatchCommand(requests: commands, transaction: false) + // swiftlint:disable:next line_length + let expected = "{\"requests\":[{\"path\":\"\\/classes\\/GameScore\\/yarr\",\"method\":\"POST\",\"body\":{\"score\":10,\"player\":\"Jen\",\"objectId\":\"yarr\"}},{\"path\":\"\\/classes\\/GameScore\\/yolo\",\"method\":\"POST\",\"body\":{\"score\":20,\"player\":\"Jen\",\"objectId\":\"yolo\"}}],\"transaction\":false}" + let encoded = try ParseCoding.parseEncoder() + .encode(body, collectChildren: false, + objectsSavedBeforeThisOne: nil, + filesSavedBeforeThisOne: nil).encoded + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + + func testUpdateAllCommand() throws { + var score = GameScore(score: 10) + score.objectId = "yarr" + score.createdAt = Date() + var score2 = GameScore(score: 20) + score2.objectId = "yolo" + score2.createdAt = Date() + + let objects = [score, score2] + let commands = objects.map { $0.saveCommand() } + let body = BatchCommand(requests: commands, transaction: false) + // swiftlint:disable:next line_length + let expected = "{\"requests\":[{\"path\":\"\\/classes\\/GameScore\\/yarr\",\"method\":\"PUT\",\"body\":{\"score\":10,\"player\":\"Jen\",\"objectId\":\"yarr\"}},{\"path\":\"\\/classes\\/GameScore\\/yolo\",\"method\":\"PUT\",\"body\":{\"score\":20,\"player\":\"Jen\",\"objectId\":\"yolo\"}}],\"transaction\":false}" + let encoded = try ParseCoding.parseEncoder() + .encode(body, collectChildren: false, + objectsSavedBeforeThisOne: nil, + filesSavedBeforeThisOne: nil).encoded + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + + func testUserSaveCommand() throws { + let objectId = "yarr" + var user = User() + user.objectId = objectId + + let command = user.saveCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/users/\(objectId)") + XCTAssertEqual(command.method, API.Method.POST) + XCTAssertNil(command.params) + XCTAssertNotNil(command.data) + + guard let body = command.body else { + XCTFail("Should be able to unwrap") + return + } + + let expected = "{\"objectId\":\"yarr\"}" + let encoded = try ParseCoding.parseEncoder() + .encode(body, collectChildren: false, + objectsSavedBeforeThisOne: nil, + filesSavedBeforeThisOne: nil).encoded + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + + func testUserUpdateCommand() throws { + let objectId = "yarr" + var user = User() + user.objectId = objectId + user.createdAt = Date() + + let command = user.saveCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/users/\(objectId)") + XCTAssertEqual(command.method, API.Method.PUT) + XCTAssertNil(command.params) + XCTAssertNotNil(command.data) + + guard let body = command.body else { + XCTFail("Should be able to unwrap") + return + } + + let expected = "{\"objectId\":\"yarr\"}" + let encoded = try ParseCoding.parseEncoder() + .encode(body, collectChildren: false, + objectsSavedBeforeThisOne: nil, + filesSavedBeforeThisOne: nil).encoded + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + + func testUserSaveAllCommand() throws { + var user = User() + user.objectId = "yarr" + var user2 = User() + user2.objectId = "yolo" + + let objects = [user, user2] + let commands = objects.map { $0.saveCommand() } + let body = BatchCommand(requests: commands, transaction: false) + // swiftlint:disable:next line_length + let expected = "{\"requests\":[{\"path\":\"\\/users\\/yarr\",\"method\":\"POST\",\"body\":{\"objectId\":\"yarr\"}},{\"path\":\"\\/users\\/yolo\",\"method\":\"POST\",\"body\":{\"objectId\":\"yolo\"}}],\"transaction\":false}" + let encoded = try ParseCoding.parseEncoder() + .encode(body, collectChildren: false, + objectsSavedBeforeThisOne: nil, + filesSavedBeforeThisOne: nil).encoded + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + + func testUserUpdateAllCommand() throws { + var user = User() + user.objectId = "yarr" + user.createdAt = Date() + var user2 = User() + user2.objectId = "yolo" + user2.createdAt = Date() + + let objects = [user, user2] + let commands = objects.map { $0.saveCommand() } + let body = BatchCommand(requests: commands, transaction: false) + // swiftlint:disable:next line_length + let expected = "{\"requests\":[{\"path\":\"\\/users\\/yarr\",\"method\":\"PUT\",\"body\":{\"objectId\":\"yarr\"}},{\"path\":\"\\/users\\/yolo\",\"method\":\"PUT\",\"body\":{\"objectId\":\"yolo\"}}],\"transaction\":false}" + let encoded = try ParseCoding.parseEncoder() + .encode(body, collectChildren: false, + objectsSavedBeforeThisOne: nil, + filesSavedBeforeThisOne: nil).encoded + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + + func testInstallationSaveCommand() throws { + let objectId = "yarr" + var installation = Installation() + installation.objectId = objectId + + let command = installation.saveCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/installations/\(objectId)") + XCTAssertEqual(command.method, API.Method.POST) + XCTAssertNil(command.params) + XCTAssertNotNil(command.data) + + guard let body = command.body else { + XCTFail("Should be able to unwrap") + return + } + + let expected = "{\"objectId\":\"yarr\"}" + let encoded = try ParseCoding.parseEncoder() + .encode(body, collectChildren: false, + objectsSavedBeforeThisOne: nil, + filesSavedBeforeThisOne: nil).encoded + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + + func testInstallationUpdateCommand() throws { + let objectId = "yarr" + var installation = Installation() + installation.objectId = objectId + installation.createdAt = Date() + + let command = installation.saveCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/installations/\(objectId)") + XCTAssertEqual(command.method, API.Method.PUT) + XCTAssertNil(command.params) + XCTAssertNotNil(command.data) + + guard let body = command.body else { + XCTFail("Should be able to unwrap") + return + } + + let expected = "{\"objectId\":\"yarr\"}" + let encoded = try ParseCoding.parseEncoder() + .encode(body, collectChildren: false, + objectsSavedBeforeThisOne: nil, + filesSavedBeforeThisOne: nil).encoded + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + + func testInstallationSaveAllCommand() throws { + var installation = Installation() + installation.objectId = "yarr" + var installation2 = Installation() + installation2.objectId = "yolo" + + let objects = [installation, installation2] + let commands = objects.map { $0.saveCommand() } + let body = BatchCommand(requests: commands, transaction: false) + // swiftlint:disable:next line_length + let expected = "{\"requests\":[{\"path\":\"\\/installations\\/yarr\",\"method\":\"POST\",\"body\":{\"objectId\":\"yarr\"}},{\"path\":\"\\/installations\\/yolo\",\"method\":\"POST\",\"body\":{\"objectId\":\"yolo\"}}],\"transaction\":false}" + let encoded = try ParseCoding.parseEncoder() + .encode(body, collectChildren: false, + objectsSavedBeforeThisOne: nil, + filesSavedBeforeThisOne: nil).encoded + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + + func testInstallationUpdateAllCommand() throws { + var installation = Installation() + installation.objectId = "yarr" + installation.createdAt = Date() + var installation2 = Installation() + installation2.objectId = "yolo" + installation2.createdAt = Date() + + let objects = [installation, installation2] + let commands = objects.map { $0.saveCommand() } + let body = BatchCommand(requests: commands, transaction: false) + // swiftlint:disable:next line_length + let expected = "{\"requests\":[{\"path\":\"\\/installations\\/yarr\",\"method\":\"PUT\",\"body\":{\"objectId\":\"yarr\"}},{\"path\":\"\\/installations\\/yolo\",\"method\":\"PUT\",\"body\":{\"objectId\":\"yolo\"}}],\"transaction\":false}" + let encoded = try ParseCoding.parseEncoder() + .encode(body, collectChildren: false, + objectsSavedBeforeThisOne: nil, + filesSavedBeforeThisOne: nil).encoded + let decoded = try XCTUnwrap(String(data: encoded, encoding: .utf8)) + XCTAssertEqual(decoded, expected) + } + #endif + + func testSave() { // swiftlint:disable:this function_body_length + var score = GameScore(score: 10) + score.objectId = "yarr" + + var scoreOnServer = score + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) + //Get dates in correct format from ParseDecoding strategy + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + let saved = try score.save() + XCTAssert(saved.hasSameObjectId(as: scoreOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testSaveNoObjectId() throws { + let score = GameScore(score: 10) + XCTAssertThrowsError(try score.save()) + } + + func testUpdate() { + var score = GameScore(score: 10) + score.objectId = "yarr" + score.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + score.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + score.ACL = nil + + var scoreOnServer = score + scoreOnServer.updatedAt = Date() + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) + //Get dates in correct format from ParseDecoding strategy + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + let saved = try score.save() + XCTAssertTrue(saved.hasSameObjectId(as: scoreOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testUpdateNoObjectId() throws { + var score = GameScore(score: 10) + score.createdAt = Date() + XCTAssertThrowsError(try score.save()) + } + + // swiftlint:disable:next function_body_length + func saveAsync(score: GameScore, scoreOnServer: GameScore, callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Save object1") + + score.save(options: [], callbackQueue: callbackQueue) { result in + + switch result { + + case .success(let saved): + XCTAssert(saved.hasSameObjectId(as: scoreOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + + let expectation2 = XCTestExpectation(description: "Save object2") + score.save(options: [.useMasterKey], callbackQueue: callbackQueue) { result in + + switch result { + + case .success(let saved): + XCTAssert(saved.hasSameObjectId(as: scoreOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation2.fulfill() + } + wait(for: [expectation1, expectation2], timeout: 20.0) + } + + func testSaveAsyncMainQueue() { + var score = GameScore(score: 10) + score.objectId = "yarr" + + var scoreOnServer = score + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) + //Get dates in correct format from ParseDecoding strategy + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) + } catch { + XCTFail("Should have encoded/decoded: Error: \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + self.saveAsync(score: score, scoreOnServer: scoreOnServer, callbackQueue: .main) + } + + func updateAsync(score: GameScore, scoreOnServer: GameScore, callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Update object1") + + score.save(options: [], callbackQueue: callbackQueue) { result in + + switch result { + + case .success(let saved): + guard let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalUpdatedAt = score.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(saved.ACL) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + + let expectation2 = XCTestExpectation(description: "Update object2") + score.save(options: [.useMasterKey], callbackQueue: callbackQueue) { result in + + switch result { + + case .success(let saved): + XCTAssertTrue(saved.hasSameObjectId(as: scoreOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation2.fulfill() + } + wait(for: [expectation1, expectation2], timeout: 20.0) + } + + func testUpdateAsyncMainQueue() { + var score = GameScore(score: 10) + score.objectId = "yarr" + score.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + score.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + score.ACL = nil + + var scoreOnServer = score + scoreOnServer.updatedAt = Date() + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) + //Get dates in correct format from ParseDecoding strategy + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded) + } catch { + XCTFail("Should have encoded/decoded: Error: \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + self.updateAsync(score: score, scoreOnServer: scoreOnServer, callbackQueue: .main) + } + + func testSaveAll() { // swiftlint:disable:this function_body_length cyclomatic_complexity + var score = GameScore(score: 10) + score.objectId = "yarr" + var score2 = GameScore(score: 20) + score2.objectId = "yolo" + + var scoreOnServer = score + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + var scoreOnServer2 = score2 + scoreOnServer2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + scoreOnServer2.updatedAt = scoreOnServer2.createdAt + scoreOnServer2.ACL = nil + + let response = [BatchResponseItem(success: scoreOnServer, error: nil), + BatchResponseItem(success: scoreOnServer2, error: nil)] + let encoded: Data! + do { + encoded = try scoreOnServer.getJSONEncoder().encode(response) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) + scoreOnServer2 = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded2) + + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + + let saved = try [score, score2].saveAll() + + XCTAssertEqual(saved.count, 2) + switch saved[0] { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: scoreOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + switch saved[1] { + + case .success(let second): + XCTAssert(second.hasSameObjectId(as: scoreOnServer2)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + } + + func testSaveAllNoObjectId() throws { + let score = GameScore(score: 10) + let score2 = GameScore(score: 20) + XCTAssertThrowsError(try [score, score2].saveAll()) + } + + func testUpdateAll() { // swiftlint:disable:this function_body_length cyclomatic_complexity + var score = GameScore(score: 10) + score.objectId = "yarr" + score.createdAt = Date() + var score2 = GameScore(score: 20) + score2.objectId = "yolo" + score2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + + var scoreOnServer = score + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + var scoreOnServer2 = score2 + scoreOnServer2.updatedAt = scoreOnServer2.createdAt + scoreOnServer2.ACL = nil + + let response = [BatchResponseItem(success: scoreOnServer, error: nil), + BatchResponseItem(success: scoreOnServer2, error: nil)] + let encoded: Data! + do { + encoded = try scoreOnServer.getJSONEncoder().encode(response) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(scoreOnServer) + scoreOnServer = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded1) + let encoded2 = try ParseCoding.jsonEncoder().encode(scoreOnServer2) + scoreOnServer2 = try scoreOnServer.getDecoder().decode(GameScore.self, from: encoded2) + + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + + let saved = try [score, score2].saveAll() + + XCTAssertEqual(saved.count, 2) + switch saved[0] { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: scoreOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + switch saved[1] { + + case .success(let second): + XCTAssert(second.hasSameObjectId(as: scoreOnServer2)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + } + + func testUpdateAllNoObjectId() throws { + var score = GameScore(score: 10) + score.createdAt = Date() + var score2 = GameScore(score: 20) + score2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + XCTAssertThrowsError(try [score, score2].saveAll()) + } + + func testUserSave() { // swiftlint:disable:this function_body_length + var user = User() + user.objectId = "yarr" + user.ACL = nil + + var userOnServer = user + userOnServer.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + userOnServer.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().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)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + let saved = try user.save() + XCTAssert(saved.hasSameObjectId(as: userOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testUserSaveNoObjectId() throws { + let score = GameScore(score: 10) + XCTAssertThrowsError(try score.save()) + } + + func testUserUpdate() { + var user = User() + user.objectId = "yarr" + user.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + user.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + user.ACL = nil + + var userOnServer = user + userOnServer.updatedAt = Date() + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().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)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + let saved = try user.save() + XCTAssertTrue(saved.hasSameObjectId(as: userOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testUserUpdateNoObjectId() throws { + var user = User() + user.createdAt = Date() + XCTAssertThrowsError(try user.save()) + } + + // swiftlint:disable:next function_body_length + func saveUserAsync(user: User, userOnServer: User, callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Update object1") + + user.save(options: [], callbackQueue: callbackQueue) { result in + + switch result { + + case .success(let saved): + XCTAssertTrue(saved.hasSameObjectId(as: userOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testUserSaveAsyncMainQueue() { + var user = User() + user.objectId = "yarr" + user.ACL = nil + + var userOnServer = user + userOnServer.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + userOnServer.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(userOnServer) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should have encoded/decoded: Error: \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + self.saveUserAsync(user: user, userOnServer: userOnServer, callbackQueue: .main) + } + + func updateUserAsync(user: User, userOnServer: User, callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Update object1") + + user.save(options: [], callbackQueue: callbackQueue) { result in + + switch result { + + case .success(let saved): + XCTAssertTrue(saved.hasSameObjectId(as: userOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testUserUpdateAsyncMainQueue() { + var user = User() + user.objectId = "yarr" + user.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + user.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + user.ACL = nil + + var userOnServer = user + userOnServer.updatedAt = Date() + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(userOnServer) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should have encoded/decoded: Error: \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + self.updateUserAsync(user: user, userOnServer: userOnServer, callbackQueue: .main) + } + + func testUserSaveAll() { // swiftlint:disable:this function_body_length cyclomatic_complexity + var user = User() + user.objectId = "yarr" + + var user2 = User() + user2.objectId = "yolo" + + var userOnServer = user + userOnServer.createdAt = Date() + userOnServer.updatedAt = userOnServer.createdAt + userOnServer.ACL = nil + + var userOnServer2 = user2 + userOnServer2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + userOnServer2.updatedAt = userOnServer2.createdAt + userOnServer2.ACL = nil + + let response = [BatchResponseItem(success: userOnServer, error: nil), + BatchResponseItem(success: userOnServer2, error: nil)] + let encoded: Data! + do { + encoded = try userOnServer.getJSONEncoder().encode(response) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(userOnServer) + userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded1) + let encoded2 = try ParseCoding.jsonEncoder().encode(userOnServer2) + userOnServer2 = try userOnServer.getDecoder().decode(User.self, from: encoded2) + + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + + let saved = try [user, user2].saveAll() + + XCTAssertEqual(saved.count, 2) + switch saved[0] { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: userOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + switch saved[1] { + + case .success(let second): + XCTAssert(second.hasSameObjectId(as: userOnServer2)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + } + + func testUserSaveAllNoObjectId() throws { + let user = User() + let user2 = User() + XCTAssertThrowsError(try [user, user2].saveAll()) + } + + func testUserUpdateAll() { // swiftlint:disable:this function_body_length cyclomatic_complexity + var user = User() + user.objectId = "yarr" + user.createdAt = Date() + var user2 = User() + user2.objectId = "yolo" + user2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + + var userOnServer = user + userOnServer.updatedAt = userOnServer.createdAt + userOnServer.ACL = nil + + var userOnServer2 = user2 + userOnServer2.updatedAt = userOnServer2.createdAt + userOnServer2.ACL = nil + + let response = [BatchResponseItem(success: userOnServer, error: nil), + BatchResponseItem(success: userOnServer2, error: nil)] + let encoded: Data! + do { + encoded = try userOnServer.getJSONEncoder().encode(response) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(userOnServer) + userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded1) + let encoded2 = try ParseCoding.jsonEncoder().encode(userOnServer2) + userOnServer2 = try userOnServer.getDecoder().decode(User.self, from: encoded2) + + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + + let saved = try [user, user2].saveAll() + + XCTAssertEqual(saved.count, 2) + switch saved[0] { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: userOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + switch saved[1] { + + case .success(let second): + XCTAssert(second.hasSameObjectId(as: userOnServer2)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + } + + func testUserUpdateAllNoObjectId() throws { + var user = User() + user.createdAt = Date() + var user2 = User() + user2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + XCTAssertThrowsError(try [user, user2].saveAll()) + } + + func testInstallationSave() { // swiftlint:disable:this function_body_length + var installation = Installation() + installation.objectId = "yarr" + installation.ACL = nil + + var installationOnServer = installation + installationOnServer.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + installationOnServer.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().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)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + let saved = try installation.save() + XCTAssert(saved.hasSameObjectId(as: installationOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testInstallationSaveNoObjectId() throws { + let score = GameScore(score: 10) + XCTAssertThrowsError(try score.save()) + } + + func testInstallationUpdate() { + var installation = Installation() + installation.objectId = "yarr" + installation.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + installation.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + installation.ACL = nil + + var installationOnServer = installation + installationOnServer.updatedAt = Date() + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().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)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + do { + let saved = try installation.save() + XCTAssertTrue(saved.hasSameObjectId(as: installationOnServer)) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testInstallationUpdateNoObjectId() throws { + var installation = Installation() + installation.createdAt = Date() + XCTAssertThrowsError(try installation.save()) + } + + // swiftlint:disable:next function_body_length + func saveInstallationAsync(installation: Installation, + installationOnServer: Installation, + callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Update object1") + + installation.save(options: [], callbackQueue: callbackQueue) { result in + + switch result { + + case .success(let saved): + XCTAssertTrue(saved.hasSameObjectId(as: installationOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + + let expectation2 = XCTestExpectation(description: "Update object2") + installation.save(options: [.useMasterKey], callbackQueue: callbackQueue) { result in + + switch result { + + case .success(let saved): + XCTAssertTrue(saved.hasSameObjectId(as: installationOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation2.fulfill() + } + wait(for: [expectation1, expectation2], timeout: 20.0) + } + + func testInstallationSaveAsyncMainQueue() { + var installation = Installation() + installation.objectId = "yarr" + installation.ACL = nil + + var installationOnServer = installation + installationOnServer.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + installationOnServer.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) + //Get dates in correct format from ParseDecoding strategy + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + } catch { + XCTFail("Should have encoded/decoded: Error: \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + self.saveInstallationAsync(installation: installation, + installationOnServer: installationOnServer, + callbackQueue: .main) + } + + func updateInstallationAsync(installation: Installation, + installationOnServer: Installation, + callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Update object1") + + installation.save(options: [], callbackQueue: callbackQueue) { result in + + switch result { + + case .success(let saved): + XCTAssertTrue(saved.hasSameObjectId(as: installationOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testInstallationUpdateAsyncMainQueue() { + var installation = Installation() + installation.objectId = "yarr" + installation.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + installation.updatedAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + installation.ACL = nil + + var installationOnServer = installation + installationOnServer.updatedAt = Date() + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) + //Get dates in correct format from ParseDecoding strategy + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + } catch { + XCTFail("Should have encoded/decoded: Error: \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + self.updateInstallationAsync(installation: installation, + installationOnServer: installationOnServer, + callbackQueue: .main) + } + + func testInstallationSaveAll() { // swiftlint:disable:this function_body_length cyclomatic_complexity + var installation = Installation() + installation.objectId = "yarr" + + var installation2 = Installation() + installation2.objectId = "yolo" + + var installationOnServer = installation + installationOnServer.createdAt = Date() + installationOnServer.updatedAt = installationOnServer.createdAt + installationOnServer.ACL = nil + + var installationOnServer2 = installation2 + installationOnServer2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + installationOnServer2.updatedAt = installationOnServer2.createdAt + installationOnServer2.ACL = nil + + let response = [BatchResponseItem(success: installationOnServer, error: nil), + BatchResponseItem(success: installationOnServer2, error: nil)] + let encoded: Data! + do { + encoded = try installationOnServer.getJSONEncoder().encode(response) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(installationOnServer) + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded1) + let encoded2 = try ParseCoding.jsonEncoder().encode(installationOnServer2) + installationOnServer2 = try installationOnServer.getDecoder().decode(Installation.self, from: encoded2) + + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + + let saved = try [installation, installation2].saveAll() + + XCTAssertEqual(saved.count, 2) + switch saved[0] { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: installationOnServer)) + guard let savedCreatedAt = first.createdAt, + let savedUpdatedAt = first.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = installationOnServer.createdAt, + let originalUpdatedAt = installationOnServer.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(first.ACL) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + switch saved[1] { + + case .success(let second): + XCTAssert(second.hasSameObjectId(as: installationOnServer2)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + } + + func testInstallationSaveAllNoObjectId() throws { + let installation = Installation() + let installation2 = Installation() + XCTAssertThrowsError(try [installation, installation2].saveAll()) + } + + func testInstallationUpdateAll() { // swiftlint:disable:this function_body_length cyclomatic_complexity + var installation = Installation() + installation.objectId = "yarr" + installation.createdAt = Date() + var installation2 = Installation() + installation2.objectId = "yolo" + installation2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + + var installationOnServer = installation + installationOnServer.updatedAt = installationOnServer.createdAt + installationOnServer.ACL = nil + + var installationOnServer2 = installation2 + installationOnServer2.updatedAt = installationOnServer2.createdAt + installationOnServer2.ACL = nil + + let response = [BatchResponseItem(success: installationOnServer, error: nil), + BatchResponseItem(success: installationOnServer2, error: nil)] + let encoded: Data! + do { + encoded = try installationOnServer.getJSONEncoder().encode(response) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().encode(installationOnServer) + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded1) + let encoded2 = try ParseCoding.jsonEncoder().encode(installationOnServer2) + installationOnServer2 = try installationOnServer.getDecoder().decode(Installation.self, from: encoded2) + + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + + let saved = try [installation, installation2].saveAll() + + XCTAssertEqual(saved.count, 2) + switch saved[0] { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: installationOnServer)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + switch saved[1] { + + case .success(let second): + XCTAssert(second.hasSameObjectId(as: installationOnServer2)) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + } catch { + XCTFail(error.localizedDescription) + } + } + + func testInstallationUpdateAllNoObjectId() throws { + var installation = Installation() + installation.createdAt = Date() + var installation2 = Installation() + installation2.createdAt = Calendar.current.date(byAdding: .init(day: -1), to: Date()) + XCTAssertThrowsError(try [installation, installation2].saveAll()) + } +} diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index 2477fa340..f7dd06ce0 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -429,7 +429,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } #endif - func testSaveCommand() { + func testSaveCommand() throws { let user = User() let command = user.saveCommand() @@ -441,7 +441,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNotNil(command.data) } - func testUpdateCommand() { + func testUpdateCommand() throws { var user = User() let objectId = "yarr" user.objectId = objectId @@ -807,6 +807,52 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } } + func testUserSignUpNoBody() { + let loginResponse = LoginSignupResponse() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + do { + var user = User() + user.username = loginUserName + user.password = loginPassword + let signedUp = try user.signup() + XCTAssertNotNil(signedUp) + XCTAssertNotNil(signedUp.createdAt) + XCTAssertNotNil(signedUp.updatedAt) + XCTAssertNotNil(signedUp.email) + XCTAssertNotNil(signedUp.username) + XCTAssertNil(signedUp.password) + XCTAssertNotNil(signedUp.objectId) + XCTAssertNotNil(signedUp.sessionToken) + XCTAssertNotNil(signedUp.customKey) + XCTAssertNil(signedUp.ACL) + + guard let userFromKeychain = BaseParseUser.current else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + + XCTAssertNotNil(userFromKeychain.createdAt) + XCTAssertNotNil(userFromKeychain.updatedAt) + XCTAssertNotNil(userFromKeychain.email) + XCTAssertNotNil(userFromKeychain.username) + XCTAssertNil(userFromKeychain.password) + XCTAssertNotNil(userFromKeychain.objectId) + XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.ACL) + + } catch { + XCTFail(error.localizedDescription) + } + } + func signUpAsync(loginResponse: LoginSignupResponse, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Signup user1") @@ -862,6 +908,63 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length self.signUpAsync(loginResponse: loginResponse, callbackQueue: .main) } + func signUpAsyncNoBody(loginResponse: LoginSignupResponse, callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Signup user1") + var user = User() + user.username = loginUserName + user.password = loginPassword + user.signup(callbackQueue: callbackQueue) { result in + switch result { + + case .success(let signedUp): + XCTAssertNotNil(signedUp.createdAt) + XCTAssertNotNil(signedUp.updatedAt) + XCTAssertNotNil(signedUp.email) + XCTAssertNotNil(signedUp.username) + XCTAssertNil(signedUp.password) + XCTAssertNotNil(signedUp.objectId) + XCTAssertNotNil(signedUp.sessionToken) + XCTAssertNotNil(signedUp.customKey) + XCTAssertNil(signedUp.ACL) + + guard let userFromKeychain = BaseParseUser.current else { + XCTFail("Couldn't get CurrentUser from Keychain") + expectation1.fulfill() + return + } + + XCTAssertNotNil(userFromKeychain.createdAt) + XCTAssertNotNil(userFromKeychain.updatedAt) + XCTAssertNotNil(userFromKeychain.email) + XCTAssertNotNil(userFromKeychain.username) + XCTAssertNil(userFromKeychain.password) + XCTAssertNotNil(userFromKeychain.objectId) + XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.ACL) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testSignUpAsyncMainQueueNoBody() { + let loginResponse = LoginSignupResponse() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + self.signUpAsyncNoBody(loginResponse: loginResponse, callbackQueue: .main) + } + func testLoginCommand() { let command = User.loginCommand(username: "test", password: "user") XCTAssertNotNil(command) @@ -1677,7 +1780,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) - #if !os(Linux) + #if !os(Linux) && !os(Android) //Should be updated in Keychain guard let keychainUser: CurrentUserContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser), @@ -1826,7 +1929,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) - #if !os(Linux) + #if !os(Linux) && !os(Android) //Should be updated in Keychain guard let keychainUser: CurrentUserContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser),