diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c777f1e43..d78b8077e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -109,9 +109,9 @@ jobs: uses: actions/cache@v2 with: path: vendor/bundle - key: ${{ runner.os }}-gem-v2-${{ hashFiles('**/Gemfile.lock') }} + key: ${{ runner.os }}-gem-v3-${{ hashFiles('**/Gemfile.lock') }} restore-keys: | - ${{ runner.os }}-gem-v2 + ${{ runner.os }}-gem-v3 - name: Install Bundle run: | bundle config path vendor/bundle diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b75b7c192..b4ab29e6b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,9 +26,9 @@ jobs: uses: actions/cache@v2 with: path: vendor/bundle - key: ${{ runner.os }}-gem-v2-${{ hashFiles('**/Gemfile.lock') }} + key: ${{ runner.os }}-gem-v3-${{ hashFiles('**/Gemfile.lock') }} restore-keys: | - ${{ runner.os }}-gem-v2 + ${{ runner.os }}-gem-v3 - name: Install Bundle run: | bundle config path vendor/bundle diff --git a/CHANGELOG.md b/CHANGELOG.md index 89c3ba161..c1854a690 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,18 @@ # Parse-Swift Changelog ### main -[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.2...main) +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.3...main) * _Contributing to this repo? Add info about your change here to be included in next release_ +### 1.1.3 +[Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.2...1.1.3) + +__New features__ +- SwiftUI ready! ([#73](https://github.com/parse-community/Parse-Swift/pull/73)), thanks to [Corey Baker](https://github.com/cbaker6). + +__Fixes__ +- Fixes some issues with `ParseUser.logout` ([#73](https://github.com/parse-community/Parse-Swift/pull/73)), thanks to [Corey Baker](https://github.com/cbaker6). + ### 1.1.2 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.1...1.1.2) diff --git a/Package.swift b/Package.swift index 1db075989..2d267e373 100644 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,7 @@ import PackageDescription let package = Package( name: "ParseSwift", - platforms: [.iOS(.v11), .macOS(.v10_13), .tvOS(.v11), .watchOS(.v4)], + platforms: [.iOS(.v12), .macOS(.v10_13), .tvOS(.v12), .watchOS(.v5)], products: [ .library( name: "ParseSwift", diff --git a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift index b9688ba34..98581d432 100644 --- a/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/1 - Your first Object.xcplaygroundpage/Contents.swift @@ -238,12 +238,13 @@ do { [scoreToFetch, score2ToFetch].deleteAll { result in switch result { case .success(let deletedScores): - deletedScores.forEach { error in - guard let error = error else { - print("Successfully deleted scores") - return + deletedScores.forEach { result in + switch result { + case .success: + print("Successfully deleted score") + case .failure(let error): + print("Error deleting: \(error)") } - print("Error deleting: \(error)") } case .failure(let error): assertionFailure("Error deleting: \(error)") diff --git a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift index 284427846..f282f6d7d 100644 --- a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift @@ -101,9 +101,17 @@ User.current?.signup { result in } } +//: Logging out - synchronously. +do { + try User.logout() + print("Successfully logged out") +} catch let error { + print("Error logging out: \(error)") +} + //: Password Reset Request - synchronously. do { - try User.verificationEmailRequest(email: "hello@parse.org") + try User.verificationEmail(email: "hello@parse.org") print("Successfully requested verification email be sent") } catch let error { print("Error requesting verification email be sent: \(error)") diff --git a/ParseSwift.playground/contents.xcplayground b/ParseSwift.playground/contents.xcplayground index 992108bbe..ab9557970 100644 --- a/ParseSwift.playground/contents.xcplayground +++ b/ParseSwift.playground/contents.xcplayground @@ -11,9 +11,9 @@ - - - + + + - + \ No newline at end of file diff --git a/ParseSwift.podspec b/ParseSwift.podspec index f81957ee8..fa204bd7b 100644 --- a/ParseSwift.podspec +++ b/ParseSwift.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "ParseSwift" - s.version = "1.1.2" + s.version = "1.1.3" s.summary = "Parse Pure Swift SDK" s.homepage = "https://github.com/parse-community/Parse-Swift" s.authors = { @@ -10,10 +10,10 @@ Pod::Spec.new do |s| :git => "#{s.homepage}.git", :tag => "#{s.version}", } - s.ios.deployment_target = "11.0" + s.ios.deployment_target = "12.0" s.osx.deployment_target = "10.13" - s.tvos.deployment_target = "11.0" - s.watchos.deployment_target = "4.0" + s.tvos.deployment_target = "12.0" + s.watchos.deployment_target = "5.0" s.swift_versions = ['5.1', '5.2', '5.3'] s.source_files = "Sources/ParseSwift/**/*.swift" s.license = { diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 502d489d1..0d0bcdfd6 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -75,11 +75,77 @@ 70110D592506CE890091CC1D /* BaseParseInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D562506CE890091CC1D /* BaseParseInstallation.swift */; }; 70110D5A2506CE890091CC1D /* BaseParseInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D562506CE890091CC1D /* BaseParseInstallation.swift */; }; 70110D5C2506ED0E0091CC1D /* ParseInstallationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */; }; + 7016ED3225C3BA2000038648 /* ParseUser+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3125C3BA2000038648 /* ParseUser+combine.swift */; }; + 7016ED3325C3BA2000038648 /* ParseUser+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3125C3BA2000038648 /* ParseUser+combine.swift */; }; + 7016ED3425C3BA2000038648 /* ParseUser+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3125C3BA2000038648 /* ParseUser+combine.swift */; }; + 7016ED3525C3BA2000038648 /* ParseUser+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3125C3BA2000038648 /* ParseUser+combine.swift */; }; + 7016ED4025C4A25A00038648 /* ParseUserCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3F25C4A25A00038648 /* ParseUserCombineTests.swift */; }; + 7016ED4125C4A25A00038648 /* ParseUserCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3F25C4A25A00038648 /* ParseUserCombineTests.swift */; }; + 7016ED4225C4A25A00038648 /* ParseUserCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED3F25C4A25A00038648 /* ParseUserCombineTests.swift */; }; + 7016ED5625C4C32B00038648 /* ParseInstallation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED5525C4C32B00038648 /* ParseInstallation+combine.swift */; }; + 7016ED5725C4C32B00038648 /* ParseInstallation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED5525C4C32B00038648 /* ParseInstallation+combine.swift */; }; + 7016ED5825C4C32B00038648 /* ParseInstallation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED5525C4C32B00038648 /* ParseInstallation+combine.swift */; }; + 7016ED5925C4C32B00038648 /* ParseInstallation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED5525C4C32B00038648 /* ParseInstallation+combine.swift */; }; + 7016ED6425C4C46B00038648 /* ParseObject+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED6325C4C46B00038648 /* ParseObject+combine.swift */; }; + 7016ED6525C4C46B00038648 /* ParseObject+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED6325C4C46B00038648 /* ParseObject+combine.swift */; }; + 7016ED6625C4C46B00038648 /* ParseObject+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED6325C4C46B00038648 /* ParseObject+combine.swift */; }; + 7016ED6725C4C46B00038648 /* ParseObject+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7016ED6325C4C46B00038648 /* ParseObject+combine.swift */; }; 7033ECB325584A83009770F3 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7033ECB225584A83009770F3 /* AppDelegate.swift */; }; 7033ECB525584A83009770F3 /* ViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7033ECB425584A83009770F3 /* ViewController.swift */; }; 7033ECB825584A83009770F3 /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7033ECB625584A83009770F3 /* Main.storyboard */; }; 7033ECBA25584A85009770F3 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 7033ECB925584A85009770F3 /* Assets.xcassets */; }; 7033ECBD25584A85009770F3 /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 7033ECBB25584A85009770F3 /* LaunchScreen.storyboard */; }; + 7044C17525C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C17425C4ECFF0011F6E7 /* ParseCloud+combine.swift */; }; + 7044C17625C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C17425C4ECFF0011F6E7 /* ParseCloud+combine.swift */; }; + 7044C17725C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C17425C4ECFF0011F6E7 /* ParseCloud+combine.swift */; }; + 7044C17825C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C17425C4ECFF0011F6E7 /* ParseCloud+combine.swift */; }; + 7044C18325C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */; }; + 7044C18425C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */; }; + 7044C18525C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */; }; + 7044C18625C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */; }; + 7044C19125C4F5B60011F6E7 /* ParseFile+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19025C4F5B60011F6E7 /* ParseFile+combine.swift */; }; + 7044C19225C4F5B60011F6E7 /* ParseFile+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19025C4F5B60011F6E7 /* ParseFile+combine.swift */; }; + 7044C19325C4F5B60011F6E7 /* ParseFile+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19025C4F5B60011F6E7 /* ParseFile+combine.swift */; }; + 7044C19425C4F5B60011F6E7 /* ParseFile+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19025C4F5B60011F6E7 /* ParseFile+combine.swift */; }; + 7044C19F25C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */; }; + 7044C1A025C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */; }; + 7044C1A125C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */; }; + 7044C1A225C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */; }; + 7044C1AD25C4FC080011F6E7 /* Query+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */; }; + 7044C1AE25C4FC080011F6E7 /* Query+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */; }; + 7044C1AF25C4FC080011F6E7 /* Query+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */; }; + 7044C1B025C4FC080011F6E7 /* Query+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */; }; + 7044C1BB25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1BA25C52E410011F6E7 /* ParseInstallationCombineTests.swift */; }; + 7044C1BC25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1BA25C52E410011F6E7 /* ParseInstallationCombineTests.swift */; }; + 7044C1BD25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1BA25C52E410011F6E7 /* ParseInstallationCombineTests.swift */; }; + 7044C1C825C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1C725C5B2B10011F6E7 /* ParseAuthentication+combine.swift */; }; + 7044C1C925C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1C725C5B2B10011F6E7 /* ParseAuthentication+combine.swift */; }; + 7044C1CA25C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1C725C5B2B10011F6E7 /* ParseAuthentication+combine.swift */; }; + 7044C1CB25C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1C725C5B2B10011F6E7 /* ParseAuthentication+combine.swift */; }; + 7044C1DF25C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1DE25C5C70D0011F6E7 /* ParseObjectCombine.swift */; }; + 7044C1E025C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1DE25C5C70D0011F6E7 /* ParseObjectCombine.swift */; }; + 7044C1E125C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1DE25C5C70D0011F6E7 /* ParseObjectCombine.swift */; }; + 7044C1EC25C5CC930011F6E7 /* ParseOperationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1EB25C5CC930011F6E7 /* ParseOperationCombineTests.swift */; }; + 7044C1ED25C5CC930011F6E7 /* ParseOperationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1EB25C5CC930011F6E7 /* ParseOperationCombineTests.swift */; }; + 7044C1EE25C5CC930011F6E7 /* ParseOperationCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1EB25C5CC930011F6E7 /* ParseOperationCombineTests.swift */; }; + 7044C1F925C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1F825C5CFAB0011F6E7 /* ParseFileCombineTests.swift */; }; + 7044C1FA25C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1F825C5CFAB0011F6E7 /* ParseFileCombineTests.swift */; }; + 7044C1FB25C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C1F825C5CFAB0011F6E7 /* ParseFileCombineTests.swift */; }; + 7044C20625C5D6780011F6E7 /* ParseQueryCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C20525C5D6780011F6E7 /* ParseQueryCombineTests.swift */; }; + 7044C20725C5D6780011F6E7 /* ParseQueryCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C20525C5D6780011F6E7 /* ParseQueryCombineTests.swift */; }; + 7044C20825C5D6780011F6E7 /* ParseQueryCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C20525C5D6780011F6E7 /* ParseQueryCombineTests.swift */; }; + 7044C21325C5DE490011F6E7 /* ParseCloudCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C21225C5DE490011F6E7 /* ParseCloudCombineTests.swift */; }; + 7044C21425C5DE490011F6E7 /* ParseCloudCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C21225C5DE490011F6E7 /* ParseCloudCombineTests.swift */; }; + 7044C21525C5DE490011F6E7 /* ParseCloudCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C21225C5DE490011F6E7 /* ParseCloudCombineTests.swift */; }; + 7044C22025C5E0160011F6E7 /* ParseConfigCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C21F25C5E0160011F6E7 /* ParseConfigCombineTests.swift */; }; + 7044C22125C5E0160011F6E7 /* ParseConfigCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C21F25C5E0160011F6E7 /* ParseConfigCombineTests.swift */; }; + 7044C22225C5E0160011F6E7 /* ParseConfigCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C21F25C5E0160011F6E7 /* ParseConfigCombineTests.swift */; }; + 7044C22D25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C22C25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift */; }; + 7044C22E25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C22C25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift */; }; + 7044C22F25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C22C25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift */; }; + 7044C24325C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C24225C5EA360011F6E7 /* ParseAppleCombineTests.swift */; }; + 7044C24425C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C24225C5EA360011F6E7 /* ParseAppleCombineTests.swift */; }; + 7044C24525C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7044C24225C5EA360011F6E7 /* ParseAppleCombineTests.swift */; }; 70510AAC259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */; }; 70510AAD259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */; }; 70510AAE259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */; }; @@ -459,6 +525,10 @@ 70110D51250680140091CC1D /* ParseConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseConstants.swift; sourceTree = ""; }; 70110D562506CE890091CC1D /* BaseParseInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BaseParseInstallation.swift; sourceTree = ""; }; 70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseInstallationTests.swift; sourceTree = ""; }; + 7016ED3125C3BA2000038648 /* ParseUser+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseUser+combine.swift"; sourceTree = ""; }; + 7016ED3F25C4A25A00038648 /* ParseUserCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseUserCombineTests.swift; sourceTree = ""; }; + 7016ED5525C4C32B00038648 /* ParseInstallation+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseInstallation+combine.swift"; sourceTree = ""; }; + 7016ED6325C4C46B00038648 /* ParseObject+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseObject+combine.swift"; sourceTree = ""; }; 7033ECB025584A83009770F3 /* TestHostTV.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = TestHostTV.app; sourceTree = BUILT_PRODUCTS_DIR; }; 7033ECB225584A83009770F3 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 7033ECB425584A83009770F3 /* ViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ViewController.swift; sourceTree = ""; }; @@ -466,6 +536,21 @@ 7033ECB925584A85009770F3 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 7033ECBC25584A85009770F3 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 7033ECBE25584A85009770F3 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 7044C17425C4ECFF0011F6E7 /* ParseCloud+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseCloud+combine.swift"; sourceTree = ""; }; + 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseConfig+combine.swift"; sourceTree = ""; }; + 7044C19025C4F5B60011F6E7 /* ParseFile+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseFile+combine.swift"; sourceTree = ""; }; + 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseOperation+combine.swift"; sourceTree = ""; }; + 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Query+combine.swift"; sourceTree = ""; }; + 7044C1BA25C52E410011F6E7 /* ParseInstallationCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseInstallationCombineTests.swift; sourceTree = ""; }; + 7044C1C725C5B2B10011F6E7 /* ParseAuthentication+combine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ParseAuthentication+combine.swift"; sourceTree = ""; }; + 7044C1DE25C5C70D0011F6E7 /* ParseObjectCombine.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseObjectCombine.swift; sourceTree = ""; }; + 7044C1EB25C5CC930011F6E7 /* ParseOperationCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseOperationCombineTests.swift; sourceTree = ""; }; + 7044C1F825C5CFAB0011F6E7 /* ParseFileCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFileCombineTests.swift; sourceTree = ""; }; + 7044C20525C5D6780011F6E7 /* ParseQueryCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseQueryCombineTests.swift; sourceTree = ""; }; + 7044C21225C5DE490011F6E7 /* ParseCloudCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseCloudCombineTests.swift; sourceTree = ""; }; + 7044C21F25C5E0160011F6E7 /* ParseConfigCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseConfigCombineTests.swift; sourceTree = ""; }; + 7044C22C25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAnonymousCombineTests.swift; sourceTree = ""; }; + 7044C24225C5EA360011F6E7 /* ParseAppleCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAppleCombineTests.swift; sourceTree = ""; }; 70510AAB259EE25E00FEA700 /* LiveQuerySocket.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LiveQuerySocket.swift; sourceTree = ""; }; 70572670259033A700F0ADD5 /* ParseFileManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFileManager.swift; sourceTree = ""; }; 705726DF2592C2A800F0ADD5 /* ParseHash.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseHash.swift; sourceTree = ""; }; @@ -678,25 +763,35 @@ 705726ED2592C91C00F0ADD5 /* HashTests.swift */, 4AA8076E1F794C1C008CD551 /* KeychainStoreTests.swift */, 9194657724F16E330070296B /* ParseACLTests.swift */, + 7044C22C25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift */, 70A2D86A25B3ADB6001BEB7D /* ParseAnonymousTests.swift */, + 7044C24225C5EA360011F6E7 /* ParseAppleCombineTests.swift */, 70C5502125B3D8F700B5DBC2 /* ParseAppleTests.swift */, 70A2D81E25B36A7D001BEB7D /* ParseAuthenticationTests.swift */, + 7044C21225C5DE490011F6E7 /* ParseCloudCombineTests.swift */, 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */, + 7044C21F25C5E0160011F6E7 /* ParseConfigCombineTests.swift */, 70D1BE0625BB2BF400A42E7C /* ParseConfigTests.swift */, F971F4F524DE381A006CB79B /* ParseEncoderTests.swift */, + 7044C1F825C5CFAB0011F6E7 /* ParseFileCombineTests.swift */, 705A99F8259807F900B3547F /* ParseFileManagerTests.swift */, 705727882593FF8000F0ADD5 /* ParseFileTests.swift */, 70BC0B32251903D1001556DB /* ParseGeoPointTests.swift */, + 7044C1BA25C52E410011F6E7 /* ParseInstallationCombineTests.swift */, 70110D5B2506ED0E0091CC1D /* ParseInstallationTests.swift */, 7003963A25A288100052CB31 /* ParseLiveQueryTests.swift */, 70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */, + 7044C1DE25C5C70D0011F6E7 /* ParseObjectCombine.swift */, 911DB13524C4FC100027F3C7 /* ParseObjectTests.swift */, + 7044C1EB25C5CC930011F6E7 /* ParseOperationCombineTests.swift */, 70C5508425B4A68700B5DBC2 /* ParseOperationTests.swift */, 70CE1D882545BF730018D572 /* ParsePointerTests.swift */, + 7044C20525C5D6780011F6E7 /* ParseQueryCombineTests.swift */, 70C7DC1F24D20F180050419B /* ParseQueryTests.swift */, 70D1BD8625B8C37200A42E7C /* ParseRelationTests.swift */, 7004C22D25B69077005E0AD9 /* ParseRoleTests.swift */, 70C5504525B40D5200B5DBC2 /* ParseSessionTests.swift */, + 7016ED3F25C4A25A00038648 /* ParseUserCombineTests.swift */, 70C7DC1D24D20E530050419B /* ParseUserTests.swift */, 7FFF552A2217E729007C3B4E /* AnyCodableTests */, 911DB12A24C3F7260027F3C7 /* NetworkMocking */, @@ -850,6 +945,7 @@ isa = PBXGroup; children = ( 707A3BF025B0A4F0000D215C /* ParseAuthentication.swift */, + 7044C1C725C5B2B10011F6E7 /* ParseAuthentication+combine.swift */, ); path = Protocols; sourceTree = ""; @@ -949,13 +1045,17 @@ children = ( F97B45C024D9C6F200F4A88B /* ParseACL.swift */, 916786E1259B7DDA00BB5B4E /* ParseCloud.swift */, + 7044C17425C4ECFF0011F6E7 /* ParseCloud+combine.swift */, 70D1BDB925BB17A600A42E7C /* ParseConfig.swift */, + 7044C18225C4EFC10011F6E7 /* ParseConfig+combine.swift */, F97B45BF24D9C6F200F4A88B /* ParseError.swift */, F97B45C124D9C6F200F4A88B /* ParseFile.swift */, + 7044C19025C4F5B60011F6E7 /* ParseFile+combine.swift */, F97B45BC24D9C6F200F4A88B /* ParseGeoPoint.swift */, 7004C21F25B63C7A005E0AD9 /* ParseRelation.swift */, F97B45BE24D9C6F200F4A88B /* Pointer.swift */, F97B45BB24D9C6F200F4A88B /* Query.swift */, + 7044C1AC25C4FC080011F6E7 /* Query+combine.swift */, ); path = Types; sourceTree = ""; @@ -964,10 +1064,13 @@ isa = PBXGroup; children = ( 70BDA2B2250536FF00FC2237 /* ParseInstallation.swift */, + 7016ED5525C4C32B00038648 /* ParseInstallation+combine.swift */, F97B45C624D9C6F200F4A88B /* ParseObject.swift */, + 7016ED6325C4C46B00038648 /* ParseObject+combine.swift */, 70C5507625B49D3A00B5DBC2 /* ParseRole.swift */, 70C5503725B406B800B5DBC2 /* ParseSession.swift */, F97B45C424D9C6F200F4A88B /* ParseUser.swift */, + 7016ED3125C3BA2000038648 /* ParseUser+combine.swift */, ); path = Objects; sourceTree = ""; @@ -1006,6 +1109,7 @@ F97B464124D9C78B00F4A88B /* Delete.swift */, F97B464524D9C78B00F4A88B /* Increment.swift */, F97B464024D9C78B00F4A88B /* ParseOperation.swift */, + 7044C19E25C4FA870011F6E7 /* ParseOperation+combine.swift */, F97B464424D9C78B00F4A88B /* Remove.swift */, 70C5509F25B4A9F600B5DBC2 /* RemoveRelation.swift */, ); @@ -1413,6 +1517,7 @@ F97B461624D9C6F200F4A88B /* Queryable.swift in Sources */, F97B45DA24D9C6F200F4A88B /* Extensions.swift in Sources */, 70C5503825B406B800B5DBC2 /* ParseSession.swift in Sources */, + 7044C1C825C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */, 707A3BF125B0A4F0000D215C /* ParseAuthentication.swift in Sources */, 70D1BE7325BB43EB00A42E7C /* BaseConfig.swift in Sources */, F97B465F24D9C7B500F4A88B /* KeychainStore.swift in Sources */, @@ -1425,7 +1530,10 @@ 700395F225A171320052CB31 /* LiveQueryable.swift in Sources */, F97B45F224D9C6F200F4A88B /* Pointer.swift in Sources */, 70510AAC259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */, + 7044C19125C4F5B60011F6E7 /* ParseFile+combine.swift in Sources */, + 7044C19F25C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */, F97B461E24D9C6F200F4A88B /* ParseStorage.swift in Sources */, + 7044C1AD25C4FC080011F6E7 /* Query+combine.swift in Sources */, F97B45D224D9C6F200F4A88B /* AnyDecodable.swift in Sources */, 70C550A025B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */, F97B463B24D9C74400F4A88B /* API+Commands.swift in Sources */, @@ -1437,6 +1545,7 @@ 700396F825A394AE0052CB31 /* ParseLiveQueryDelegate.swift in Sources */, F97B465A24D9C78C00F4A88B /* Increment.swift in Sources */, 7003960925A184EF0052CB31 /* ParseLiveQuery.swift in Sources */, + 7044C17525C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */, F97B45E224D9C6F200F4A88B /* AnyEncodable.swift in Sources */, 700396EA25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */, 70572671259033A700F0ADD5 /* ParseFileManager.swift in Sources */, @@ -1455,6 +1564,7 @@ F97B45EA24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */, F97B460224D9C6F200F4A88B /* NoBody.swift in Sources */, 700395BA25A1470F0052CB31 /* Subscription.swift in Sources */, + 7016ED5625C4C32B00038648 /* ParseInstallation+combine.swift in Sources */, 7003972A25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */, 700395D125A147BE0052CB31 /* ParseSubscription.swift in Sources */, F97B45F624D9C6F200F4A88B /* ParseError.swift in Sources */, @@ -1469,6 +1579,7 @@ F97B45CE24D9C6F200F4A88B /* ParseCoding.swift in Sources */, F97B465624D9C78C00F4A88B /* Remove.swift in Sources */, F97B45FA24D9C6F200F4A88B /* ParseACL.swift in Sources */, + 7016ED6425C4C46B00038648 /* ParseObject+combine.swift in Sources */, 70BDA2B3250536FF00FC2237 /* ParseInstallation.swift in Sources */, F97B462724D9C72700F4A88B /* API.swift in Sources */, 70647E9C259E3A9A004C1004 /* ParseType.swift in Sources */, @@ -1476,6 +1587,8 @@ 70110D572506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45DE24D9C6F200F4A88B /* AnyCodable.swift in Sources */, 70C5507725B49D3A00B5DBC2 /* ParseRole.swift in Sources */, + 7044C18325C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */, + 7016ED3225C3BA2000038648 /* ParseUser+combine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1487,27 +1600,37 @@ 70CE1D892545BF730018D572 /* ParsePointerTests.swift in Sources */, 911DB12E24C4837E0027F3C7 /* APICommandTests.swift in Sources */, 911DB12C24C3F7720027F3C7 /* MockURLResponse.swift in Sources */, + 7044C24325C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, + 7044C1DF25C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, 70C5504625B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, 70110D5C2506ED0E0091CC1D /* ParseInstallationTests.swift in Sources */, + 7016ED4025C4A25A00038648 /* ParseUserCombineTests.swift in Sources */, 705727B12593FF8800F0ADD5 /* ParseFileTests.swift in Sources */, 70BC0B33251903D1001556DB /* ParseGeoPointTests.swift in Sources */, 7003957625A0EE770052CB31 /* BatchUtilsTests.swift in Sources */, 705A99F9259807F900B3547F /* ParseFileManagerTests.swift in Sources */, + 7044C20625C5D6780011F6E7 /* ParseQueryCombineTests.swift in Sources */, 70C5508525B4A68700B5DBC2 /* ParseOperationTests.swift in Sources */, 7004C24D25B69207005E0AD9 /* ParseRoleTests.swift in Sources */, 91678706259BC5D400BB5B4E /* ParseCloudTests.swift in Sources */, 70D1BD8725B8C37200A42E7C /* ParseRelationTests.swift in Sources */, 7003963B25A288100052CB31 /* ParseLiveQueryTests.swift in Sources */, 7FFF552E2217E72A007C3B4E /* AnyEncodableTests.swift in Sources */, + 7044C22025C5E0160011F6E7 /* ParseConfigCombineTests.swift in Sources */, 7FFF55302217E72A007C3B4E /* AnyDecodableTests.swift in Sources */, 70C7DC2224D20F190050419B /* ParseObjectBatchTests.swift in Sources */, + 7044C1BB25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */, 7FFF552F2217E72A007C3B4E /* AnyCodableTests.swift in Sources */, 4AA807701F794C31008CD551 /* KeychainStoreTests.swift in Sources */, + 7044C1F925C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */, 70C5502225B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, F971F4F624DE381A006CB79B /* ParseEncoderTests.swift in Sources */, 70C7DC2124D20F190050419B /* ParseQueryTests.swift in Sources */, + 7044C22D25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift in Sources */, 9194657824F16E330070296B /* ParseACLTests.swift in Sources */, + 7044C21325C5DE490011F6E7 /* ParseCloudCombineTests.swift in Sources */, 70A2D86B25B3ADB6001BEB7D /* ParseAnonymousTests.swift in Sources */, + 7044C1EC25C5CC930011F6E7 /* ParseOperationCombineTests.swift in Sources */, 70C7DC1E24D20E530050419B /* ParseUserTests.swift in Sources */, 70A2D81F25B36A7D001BEB7D /* ParseAuthenticationTests.swift in Sources */, 70D1BE4B25BB312700A42E7C /* ParseConfigTests.swift in Sources */, @@ -1525,6 +1648,7 @@ F97B461724D9C6F200F4A88B /* Queryable.swift in Sources */, F97B45DB24D9C6F200F4A88B /* Extensions.swift in Sources */, 70C5503925B406B800B5DBC2 /* ParseSession.swift in Sources */, + 7044C1C925C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */, 707A3BF225B0A4F0000D215C /* ParseAuthentication.swift in Sources */, 70D1BE7425BB43EB00A42E7C /* BaseConfig.swift in Sources */, F97B466024D9C7B500F4A88B /* KeychainStore.swift in Sources */, @@ -1537,7 +1661,10 @@ 700395F325A171320052CB31 /* LiveQueryable.swift in Sources */, F97B45F324D9C6F200F4A88B /* Pointer.swift in Sources */, 70510AAD259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */, + 7044C19225C4F5B60011F6E7 /* ParseFile+combine.swift in Sources */, + 7044C1A025C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */, F97B461F24D9C6F200F4A88B /* ParseStorage.swift in Sources */, + 7044C1AE25C4FC080011F6E7 /* Query+combine.swift in Sources */, F97B45D324D9C6F200F4A88B /* AnyDecodable.swift in Sources */, 70C550A125B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */, F97B463C24D9C74400F4A88B /* API+Commands.swift in Sources */, @@ -1549,6 +1676,7 @@ 700396F925A394AE0052CB31 /* ParseLiveQueryDelegate.swift in Sources */, F97B465B24D9C78C00F4A88B /* Increment.swift in Sources */, 7003960A25A184EF0052CB31 /* ParseLiveQuery.swift in Sources */, + 7044C17625C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */, F97B45E324D9C6F200F4A88B /* AnyEncodable.swift in Sources */, 700396EB25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */, 70572672259033A700F0ADD5 /* ParseFileManager.swift in Sources */, @@ -1567,6 +1695,7 @@ F97B45EB24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */, F97B460324D9C6F200F4A88B /* NoBody.swift in Sources */, 700395BB25A1470F0052CB31 /* Subscription.swift in Sources */, + 7016ED5725C4C32B00038648 /* ParseInstallation+combine.swift in Sources */, 7003972B25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */, 700395D225A147BE0052CB31 /* ParseSubscription.swift in Sources */, F97B45F724D9C6F200F4A88B /* ParseError.swift in Sources */, @@ -1581,6 +1710,7 @@ F97B45CF24D9C6F200F4A88B /* ParseCoding.swift in Sources */, F97B465724D9C78C00F4A88B /* Remove.swift in Sources */, F97B45FB24D9C6F200F4A88B /* ParseACL.swift in Sources */, + 7016ED6525C4C46B00038648 /* ParseObject+combine.swift in Sources */, 70BDA2B4250536FF00FC2237 /* ParseInstallation.swift in Sources */, F97B462824D9C72700F4A88B /* API.swift in Sources */, 70647E9D259E3A9A004C1004 /* ParseType.swift in Sources */, @@ -1588,6 +1718,8 @@ 70110D582506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45DF24D9C6F200F4A88B /* AnyCodable.swift in Sources */, 70C5507825B49D3A00B5DBC2 /* ParseRole.swift in Sources */, + 7044C18425C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */, + 7016ED3325C3BA2000038648 /* ParseUser+combine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1608,27 +1740,37 @@ 709B98532556ECAA00507778 /* ParsePointerTests.swift in Sources */, 709B984C2556ECAA00507778 /* APICommandTests.swift in Sources */, 709B984D2556ECAA00507778 /* AnyDecodableTests.swift in Sources */, + 7044C24525C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, + 7044C1E125C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, 70C5504825B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, 709B98572556ECAA00507778 /* ParseACLTests.swift in Sources */, + 7016ED4225C4A25A00038648 /* ParseUserCombineTests.swift in Sources */, 705727BC2593FF8C00F0ADD5 /* ParseFileTests.swift in Sources */, 709B984F2556ECAA00507778 /* AnyCodableTests.swift in Sources */, 7003957825A0EE770052CB31 /* BatchUtilsTests.swift in Sources */, 705A99FB259807F900B3547F /* ParseFileManagerTests.swift in Sources */, + 7044C20825C5D6780011F6E7 /* ParseQueryCombineTests.swift in Sources */, 70C5508725B4A68700B5DBC2 /* ParseOperationTests.swift in Sources */, 7004C26125B6920B005E0AD9 /* ParseRoleTests.swift in Sources */, 9167871A259BC5D600BB5B4E /* ParseCloudTests.swift in Sources */, 70D1BD8925B8C37200A42E7C /* ParseRelationTests.swift in Sources */, 7003963D25A288100052CB31 /* ParseLiveQueryTests.swift in Sources */, 709B98592556ECAA00507778 /* MockURLResponse.swift in Sources */, + 7044C22225C5E0160011F6E7 /* ParseConfigCombineTests.swift in Sources */, 709B98522556ECAA00507778 /* ParseUserTests.swift in Sources */, 709B984E2556ECAA00507778 /* ParseGeoPointTests.swift in Sources */, + 7044C1BD25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */, 709B984B2556ECAA00507778 /* MockURLProtocol.swift in Sources */, 709B98552556ECAA00507778 /* ParseQueryTests.swift in Sources */, + 7044C1FB25C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */, 70C5502425B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, 709B98502556ECAA00507778 /* KeychainStoreTests.swift in Sources */, 709B98562556ECAA00507778 /* ParseObjectTests.swift in Sources */, + 7044C22F25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift in Sources */, 709B985A2556ECAA00507778 /* ParseObjectBatchTests.swift in Sources */, + 7044C21525C5DE490011F6E7 /* ParseCloudCombineTests.swift in Sources */, 70A2D86D25B3ADB6001BEB7D /* ParseAnonymousTests.swift in Sources */, + 7044C1EE25C5CC930011F6E7 /* ParseOperationCombineTests.swift in Sources */, 709B98582556ECAA00507778 /* AnyEncodableTests.swift in Sources */, 70A2D82125B36A7D001BEB7D /* ParseAuthenticationTests.swift in Sources */, 70D1BE5F25BB312A00A42E7C /* ParseConfigTests.swift in Sources */, @@ -1645,27 +1787,37 @@ 70F2E2B7254F283000B2EA5C /* ParsePointerTests.swift in Sources */, 70F2E2B5254F283000B2EA5C /* ParseEncoderTests.swift in Sources */, 70F2E2C2254F283000B2EA5C /* APICommandTests.swift in Sources */, + 7044C24425C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, + 7044C1E025C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, 70C5504725B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, 70F2E2BC254F283000B2EA5C /* ParseObjectTests.swift in Sources */, + 7016ED4125C4A25A00038648 /* ParseUserCombineTests.swift in Sources */, 705727BB2593FF8B00F0ADD5 /* ParseFileTests.swift in Sources */, 70F2E2BD254F283000B2EA5C /* AnyDecodableTests.swift in Sources */, 7003957725A0EE770052CB31 /* BatchUtilsTests.swift in Sources */, 705A99FA259807F900B3547F /* ParseFileManagerTests.swift in Sources */, + 7044C20725C5D6780011F6E7 /* ParseQueryCombineTests.swift in Sources */, 70C5508625B4A68700B5DBC2 /* ParseOperationTests.swift in Sources */, 7004C25725B6920A005E0AD9 /* ParseRoleTests.swift in Sources */, 91678710259BC5D600BB5B4E /* ParseCloudTests.swift in Sources */, 70D1BD8825B8C37200A42E7C /* ParseRelationTests.swift in Sources */, 7003963C25A288100052CB31 /* ParseLiveQueryTests.swift in Sources */, 70F2E2C1254F283000B2EA5C /* AnyCodableTests.swift in Sources */, + 7044C22125C5E0160011F6E7 /* ParseConfigCombineTests.swift in Sources */, 70F2E2B3254F283000B2EA5C /* ParseUserTests.swift in Sources */, 70F2E2C0254F283000B2EA5C /* MockURLResponse.swift in Sources */, + 7044C1BC25C52E410011F6E7 /* ParseInstallationCombineTests.swift in Sources */, 70F2E2BE254F283000B2EA5C /* ParseObjectBatchTests.swift in Sources */, 70F2E2BF254F283000B2EA5C /* MockURLProtocol.swift in Sources */, + 7044C1FA25C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */, 70C5502325B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, 70F2E2BB254F283000B2EA5C /* ParseGeoPointTests.swift in Sources */, 70F2E2B8254F283000B2EA5C /* AnyEncodableTests.swift in Sources */, + 7044C22E25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift in Sources */, 70F2E2B4254F283000B2EA5C /* ParseQueryTests.swift in Sources */, + 7044C21425C5DE490011F6E7 /* ParseCloudCombineTests.swift in Sources */, 70A2D86C25B3ADB6001BEB7D /* ParseAnonymousTests.swift in Sources */, + 7044C1ED25C5CC930011F6E7 /* ParseOperationCombineTests.swift in Sources */, 70F2E2BA254F283000B2EA5C /* ParseInstallationTests.swift in Sources */, 70A2D82025B36A7D001BEB7D /* ParseAuthenticationTests.swift in Sources */, 70D1BE5525BB312900A42E7C /* ParseConfigTests.swift in Sources */, @@ -1683,6 +1835,7 @@ F97B45E924D9C6F200F4A88B /* Query.swift in Sources */, F97B463624D9C74400F4A88B /* URLSession+extensions.swift in Sources */, 70C5503B25B406B800B5DBC2 /* ParseSession.swift in Sources */, + 7044C1CB25C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */, 707A3BF425B0A4F0000D215C /* ParseAuthentication.swift in Sources */, 70D1BE7625BB43EB00A42E7C /* BaseConfig.swift in Sources */, F97B460524D9C6F200F4A88B /* NoBody.swift in Sources */, @@ -1695,7 +1848,10 @@ 700395F525A171320052CB31 /* LiveQueryable.swift in Sources */, F97B45FD24D9C6F200F4A88B /* ParseACL.swift in Sources */, 70510AAF259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */, + 7044C19425C4F5B60011F6E7 /* ParseFile+combine.swift in Sources */, + 7044C1A225C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */, F97B465124D9C78C00F4A88B /* Add.swift in Sources */, + 7044C1B025C4FC080011F6E7 /* Query+combine.swift in Sources */, F97B461124D9C6F200F4A88B /* ParseObject.swift in Sources */, 70C550A325B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */, F97B460D24D9C6F200F4A88B /* Fetchable.swift in Sources */, @@ -1707,6 +1863,7 @@ 700396FB25A394AE0052CB31 /* ParseLiveQueryDelegate.swift in Sources */, F97B463A24D9C74400F4A88B /* Responses.swift in Sources */, 7003960C25A184EF0052CB31 /* ParseLiveQuery.swift in Sources */, + 7044C17825C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */, F97B45DD24D9C6F200F4A88B /* Extensions.swift in Sources */, 700396ED25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */, 70572674259033A700F0ADD5 /* ParseFileManager.swift in Sources */, @@ -1725,6 +1882,7 @@ 70110D5A2506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45F924D9C6F200F4A88B /* ParseError.swift in Sources */, 700395BD25A1470F0052CB31 /* Subscription.swift in Sources */, + 7016ED5925C4C32B00038648 /* ParseInstallation+combine.swift in Sources */, 7003972D25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */, 700395D425A147BE0052CB31 /* ParseSubscription.swift in Sources */, F97B460124D9C6F200F4A88B /* ParseFile.swift in Sources */, @@ -1739,6 +1897,7 @@ F97B463E24D9C74400F4A88B /* API+Commands.swift in Sources */, F97B462A24D9C72700F4A88B /* API.swift in Sources */, F97B463224D9C74400F4A88B /* BatchUtils.swift in Sources */, + 7016ED6725C4C46B00038648 /* ParseObject+combine.swift in Sources */, F97B45F124D9C6F200F4A88B /* BaseParseUser.swift in Sources */, F97B45D924D9C6F200F4A88B /* ParseEncoder.swift in Sources */, 70647E9F259E3A9A004C1004 /* ParseType.swift in Sources */, @@ -1746,6 +1905,8 @@ 912C9BFD24D302B2009947C3 /* Parse.swift in Sources */, F97B461924D9C6F200F4A88B /* Queryable.swift in Sources */, 70C5507A25B49D3A00B5DBC2 /* ParseRole.swift in Sources */, + 7044C18625C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */, + 7016ED3525C3BA2000038648 /* ParseUser+combine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1758,6 +1919,7 @@ F97B45E824D9C6F200F4A88B /* Query.swift in Sources */, F97B463524D9C74400F4A88B /* URLSession+extensions.swift in Sources */, 70C5503A25B406B800B5DBC2 /* ParseSession.swift in Sources */, + 7044C1CA25C5B2B10011F6E7 /* ParseAuthentication+combine.swift in Sources */, 707A3BF325B0A4F0000D215C /* ParseAuthentication.swift in Sources */, 70D1BE7525BB43EB00A42E7C /* BaseConfig.swift in Sources */, F97B460424D9C6F200F4A88B /* NoBody.swift in Sources */, @@ -1770,7 +1932,10 @@ 700395F425A171320052CB31 /* LiveQueryable.swift in Sources */, F97B45FC24D9C6F200F4A88B /* ParseACL.swift in Sources */, 70510AAE259EE25E00FEA700 /* LiveQuerySocket.swift in Sources */, + 7044C19325C4F5B60011F6E7 /* ParseFile+combine.swift in Sources */, + 7044C1A125C4FA870011F6E7 /* ParseOperation+combine.swift in Sources */, F97B465024D9C78B00F4A88B /* Add.swift in Sources */, + 7044C1AF25C4FC080011F6E7 /* Query+combine.swift in Sources */, F97B461024D9C6F200F4A88B /* ParseObject.swift in Sources */, 70C550A225B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */, F97B460C24D9C6F200F4A88B /* Fetchable.swift in Sources */, @@ -1782,6 +1947,7 @@ 700396FA25A394AE0052CB31 /* ParseLiveQueryDelegate.swift in Sources */, F97B463924D9C74400F4A88B /* Responses.swift in Sources */, 7003960B25A184EF0052CB31 /* ParseLiveQuery.swift in Sources */, + 7044C17725C4ECFF0011F6E7 /* ParseCloud+combine.swift in Sources */, F97B45DC24D9C6F200F4A88B /* Extensions.swift in Sources */, 700396EC25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */, 70572673259033A700F0ADD5 /* ParseFileManager.swift in Sources */, @@ -1800,6 +1966,7 @@ 70110D592506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45F824D9C6F200F4A88B /* ParseError.swift in Sources */, 700395BC25A1470F0052CB31 /* Subscription.swift in Sources */, + 7016ED5825C4C32B00038648 /* ParseInstallation+combine.swift in Sources */, 7003972C25A3B0140052CB31 /* ParseURLSessionDelegate.swift in Sources */, 700395D325A147BE0052CB31 /* ParseSubscription.swift in Sources */, F97B460024D9C6F200F4A88B /* ParseFile.swift in Sources */, @@ -1814,6 +1981,7 @@ F97B463D24D9C74400F4A88B /* API+Commands.swift in Sources */, F97B462924D9C72700F4A88B /* API.swift in Sources */, F97B463124D9C74400F4A88B /* BatchUtils.swift in Sources */, + 7016ED6625C4C46B00038648 /* ParseObject+combine.swift in Sources */, F97B45F024D9C6F200F4A88B /* BaseParseUser.swift in Sources */, F97B45D824D9C6F200F4A88B /* ParseEncoder.swift in Sources */, 70647E9E259E3A9A004C1004 /* ParseType.swift in Sources */, @@ -1821,6 +1989,8 @@ 912C9BE024D302B0009947C3 /* Parse.swift in Sources */, F97B461824D9C6F200F4A88B /* Queryable.swift in Sources */, 70C5507925B49D3A00B5DBC2 /* ParseRole.swift in Sources */, + 7044C18525C4EFC10011F6E7 /* ParseConfig+combine.swift in Sources */, + 7016ED3425C3BA2000038648 /* ParseUser+combine.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -2055,13 +2225,15 @@ 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.1.2; + MARKETING_VERSION = 1.1.3; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SKIP_INSTALL = YES; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = 12.0; + WATCHOS_DEPLOYMENT_TARGET = 5.0; }; name = Debug; }; @@ -2077,13 +2249,15 @@ 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.1.2; + MARKETING_VERSION = 1.1.3; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SKIP_INSTALL = YES; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; + TVOS_DEPLOYMENT_TARGET = 12.0; + WATCHOS_DEPLOYMENT_TARGET = 5.0; }; name = Release; }; @@ -2141,13 +2315,15 @@ 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.1.2; + MARKETING_VERSION = 1.1.3; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 5.0; + TVOS_DEPLOYMENT_TARGET = 12.0; + WATCHOS_DEPLOYMENT_TARGET = 5.0; }; name = Debug; }; @@ -2165,13 +2341,15 @@ 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.1.2; + MARKETING_VERSION = 1.1.3; PRODUCT_BUNDLE_IDENTIFIER = com.parse.ParseSwift; PRODUCT_NAME = ParseSwift; SDKROOT = macosx; SKIP_INSTALL = YES; SWIFT_SWIFT3_OBJC_INFERENCE = Off; SWIFT_VERSION = 5.0; + TVOS_DEPLOYMENT_TARGET = 12.0; + WATCHOS_DEPLOYMENT_TARGET = 5.0; }; name = Release; }; @@ -2310,7 +2488,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.1.2; + MARKETING_VERSION = 1.1.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-watchOS"; @@ -2320,7 +2498,8 @@ SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 4.0; + TVOS_DEPLOYMENT_TARGET = 12.0; + WATCHOS_DEPLOYMENT_TARGET = 5.0; }; name = Debug; }; @@ -2338,7 +2517,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.1.2; + MARKETING_VERSION = 1.1.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-watchOS"; PRODUCT_NAME = ParseSwift; @@ -2347,7 +2526,8 @@ SKIP_INSTALL = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 4; - WATCHOS_DEPLOYMENT_TARGET = 4.0; + TVOS_DEPLOYMENT_TARGET = 12.0; + WATCHOS_DEPLOYMENT_TARGET = 5.0; }; name = Release; }; @@ -2364,7 +2544,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.1.2; + MARKETING_VERSION = 1.1.3; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-tvOS"; @@ -2375,6 +2555,7 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 12.0; + WATCHOS_DEPLOYMENT_TARGET = 5.0; }; name = Debug; }; @@ -2391,7 +2572,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.1.2; + MARKETING_VERSION = 1.1.3; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "com.parse.ParseSwift-tvOS"; PRODUCT_NAME = ParseSwift; @@ -2401,6 +2582,7 @@ SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = 3; TVOS_DEPLOYMENT_TARGET = 12.0; + WATCHOS_DEPLOYMENT_TARGET = 5.0; }; name = Release; }; diff --git a/Scripts/jazzy.sh b/Scripts/jazzy.sh index f7cdfedc8..63ca4af7c 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.1.2 \ + --module-version 1.1.3 \ --theme fullwidth \ --skip-undocumented \ --output ./docs/api \ diff --git a/Sources/ParseSwift/API/API+Commands.swift b/Sources/ParseSwift/API/API+Commands.swift index 60b94abfa..a626a430a 100644 --- a/Sources/ParseSwift/API/API+Commands.swift +++ b/Sources/ParseSwift/API/API+Commands.swift @@ -452,38 +452,41 @@ extension API.Command where T: ParseObject { } // MARK: Batch - Deleting - // swiftlint:disable:next line_length - static func batch(commands: [API.NonParseBodyCommand]) -> RESTBatchCommandNoBodyType { - let commands = commands.compactMap { (command) -> API.NonParseBodyCommand? in + static func batch(commands: [API.NonParseBodyCommand]) -> RESTBatchCommandNoBodyType { + let commands = commands.compactMap { (command) -> API.NonParseBodyCommand? in let path = ParseConfiguration.mountPath + command.path.urlComponent - return API.NonParseBodyCommand( + return API.NonParseBodyCommand( method: command.method, path: .any(path), mapper: command.mapper) } - let mapper = { (data: Data) -> [ParseError?] in + let mapper = { (data: Data) -> [(Result)] in - let decodingType = [ParseError?].self + let decodingType = [BatchResponseItem].self do { let responses = try ParseCoding.jsonDecoder().decode(decodingType, from: data) - return responses.enumerated().map({ (object) -> ParseError? in + return responses.enumerated().map({ (object) -> (Result) in let response = responses[object.offset] - if let error = response { - return error + if response.success != nil { + return .success(()) } else { - return nil + guard let parseError = response.error else { + return .failure(ParseError(code: .unknownError, message: "unknown error")) + } + + return .failure(parseError) } }) } catch { - guard (try? ParseCoding.jsonDecoder().decode(NoBody.self, from: data)) != nil else { - return [ParseError(code: .unknownError, message: "decoding error: \(error)")] + guard let parseError = error as? ParseError else { + return [(.failure(ParseError(code: .unknownError, message: "decoding error: \(error)")))] } - return [nil] + return [(.failure(parseError))] } } let batchCommand = BatchCommandNoBody(requests: commands) - return RESTBatchCommandNoBodyType(method: .POST, path: .batch, body: batchCommand, mapper: mapper) + return RESTBatchCommandNoBodyType(method: .POST, path: .batch, body: batchCommand, mapper: mapper) } } @@ -642,17 +645,21 @@ internal extension API { internal extension API.NonParseBodyCommand { // MARK: Deleting - // swiftlint:disable:next line_length - static func deleteCommand(_ object: T) throws -> API.NonParseBodyCommand where T: ParseObject { + static func deleteCommand(_ object: T) throws -> API.NonParseBodyCommand where T: ParseObject { guard object.isSaved else { throw ParseError(code: .unknownError, message: "Cannot Delete an object without id") } - return API.NonParseBodyCommand( + return API.NonParseBodyCommand( method: .DELETE, path: object.endpoint - ) { (data) -> ParseError? in - try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + ) { (data) -> NoBody in + let error = try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + if let error = error { + throw error + } else { + return NoBody() + } } } } // swiftlint:disable:this file_length diff --git a/Sources/ParseSwift/API/API.swift b/Sources/ParseSwift/API/API.swift index 34fec5f91..f6088e118 100644 --- a/Sources/ParseSwift/API/API.swift +++ b/Sources/ParseSwift/API/API.swift @@ -31,7 +31,7 @@ public struct API { case logout case file(fileName: String) case passwordReset - case verificationEmailRequest + case verificationEmail case functions(name: String) case jobs(name: String) case aggregate(className: String) @@ -70,7 +70,7 @@ public struct API { return "/files/\(fileName)" case .passwordReset: return "/requestPasswordReset" - case .verificationEmailRequest: + case .verificationEmail: return "/verificationEmailRequest" case .functions(name: let name): return "/functions/\(name)" diff --git a/Sources/ParseSwift/API/BatchUtils.swift b/Sources/ParseSwift/API/BatchUtils.swift index 7e6dd92bd..4d01208d6 100644 --- a/Sources/ParseSwift/API/BatchUtils.swift +++ b/Sources/ParseSwift/API/BatchUtils.swift @@ -13,8 +13,8 @@ typealias ParseObjectBatchResponse = [(Result)] // swiftlint:disable line_length typealias RESTBatchCommandType = API.Command, ParseObjectBatchResponse> where T: ParseObject -typealias ParseObjectBatchCommandNoBody = BatchCommandNoBody -typealias ParseObjectBatchResponseNoBody = [ParseError?] +typealias ParseObjectBatchCommandNoBody = BatchCommandNoBody +typealias ParseObjectBatchResponseNoBody = [(Result)] typealias RESTBatchCommandNoBodyType = API.NonParseBodyCommand, ParseObjectBatchResponseNoBody> where T: Encodable /* typealias ParseObjectBatchCommandEncodable = BatchCommand where T: ParseType diff --git a/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift b/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift index bf9c3289f..fa872a43e 100644 --- a/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift +++ b/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift @@ -7,6 +7,9 @@ // import Foundation +#if canImport(Combine) +import Combine +#endif // swiftlint:disable line_length @@ -90,6 +93,41 @@ public extension ParseApple { callbackQueue: callbackQueue, completion: completion) } + + #if canImport(Combine) + + /** + Login a `ParseUser` *asynchronously* using Apple authentication. Publishes when complete. + - parameter user: The `user` from `ASAuthorizationAppleIDCredential`. + - parameter identityToken: The `identityToken` from `ASAuthorizationAppleIDCredential`. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func loginPublisher(user: String, + identityToken: String, + options: API.Options = []) -> Future { + loginPublisher(authData: AuthenticationKeys.id.makeDictionary(user: user, identityToken: identityToken), + options: options) + } + + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func loginPublisher(authData: [String: String]?, + options: API.Options = []) -> Future { + guard AuthenticationKeys.id.verifyMandatoryKeys(authData: authData), + let authData = authData else { + let error = ParseError(code: .unknownError, + message: "Should have authData in consisting of keys \"id\" and \"token\".") + return Future { promise in + promise(.failure(error)) + } + } + return AuthenticatedUser.loginPublisher(Self.__type, + authData: authData, + options: options) + } + + #endif } // MARK: Link @@ -133,6 +171,41 @@ public extension ParseApple { callbackQueue: callbackQueue, completion: completion) } + + #if canImport(Combine) + + /** + Link the *current* `ParseUser` *asynchronously* using Apple authentication. Publishes when complete. + - parameter user: The `user` from `ASAuthorizationAppleIDCredential`. + - parameter identityToken: The `identityToken` from `ASAuthorizationAppleIDCredential`. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func linkPublisher(user: String, + identityToken: String, + options: API.Options = []) -> Future { + linkPublisher(authData: AuthenticationKeys.id.makeDictionary(user: user, identityToken: identityToken), + options: options) + } + + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func linkPublisher(authData: [String: String]?, + options: API.Options = []) -> Future { + guard AuthenticationKeys.id.verifyMandatoryKeys(authData: authData), + let authData = authData else { + let error = ParseError(code: .unknownError, + message: "Should have authData in consisting of keys \"id\" and \"token\".") + return Future { promise in + promise(.failure(error)) + } + } + return AuthenticatedUser.linkPublisher(Self.__type, + authData: authData, + options: options) + } + + #endif } // MARK: 3rd Party Authentication - ParseApple diff --git a/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift b/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift index 523c47969..f8b84b1b3 100644 --- a/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift +++ b/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift @@ -7,6 +7,9 @@ // import Foundation +#if canImport(Combine) +import Combine +#endif /** Provides utility functions for working with Anonymously logged-in users. @@ -68,6 +71,18 @@ public extension ParseAnonymous { callbackQueue: callbackQueue, completion: completion) } + + #if canImport(Combine) + + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func loginPublisher(authData: [String: String]? = nil, + options: API.Options = []) -> Future { + AuthenticatedUser.loginPublisher(__type, + authData: AuthenticationKeys.id.makeDictionary(), + options: options) + } + + #endif } // MARK: Link @@ -81,6 +96,18 @@ public extension ParseAnonymous { completion(.failure(ParseError(code: .unknownError, message: "Not supported"))) } } + + #if canImport(Combine) + + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func linkPublisher(authData: [String: String]?, + options: API.Options) -> Future { + Future { promise in + promise(.failure(ParseError(code: .unknownError, message: "Not supported"))) + } + } + + #endif } // MARK: ParseAnonymous diff --git a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift new file mode 100644 index 000000000..558d0bfc3 --- /dev/null +++ b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+combine.swift @@ -0,0 +1,97 @@ +// +// ParseAuthentication+combine.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) +import Foundation +import Combine + +// MARK: Convenience Implementations - Combine +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseAuthentication { + + func unlinkPublisher(_ user: AuthenticatedUser, + options: API.Options = []) -> Future { + user.unlinkPublisher(__type, options: options) + } + + func unlinkPublisher(options: API.Options = []) -> Future { + guard let current = AuthenticatedUser.current else { + let error = ParseError(code: .invalidLinkedSession, message: "No current ParseUser.") + return Future { promise in + promise(.failure(error)) + } + } + return unlinkPublisher(current, options: options) + } +} + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseUser { + + // MARK: 3rd Party Authentication - Login Combine + + /** + Makes an *asynchronous* request to log in a user with specified credentials. + Publishes an instance of the successfully logged in `ParseUser`. + + This also caches the user locally so that calls to *current* will use the latest logged in user. + - parameter type: The authentication type. + - parameter authData: The data that represents the authentication. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + static func loginPublisher(_ type: String, + authData: [String: String], + options: API.Options = []) -> Future { + Future { promise in + Self.login(type, + authData: authData, + options: options, + completion: promise) + } + } + + /** + Unlink the authentication type *asynchronously*. Publishes when complete. + - parameter type: The type to unlink. The user must be logged in on this device. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func unlinkPublisher(_ type: String, + options: API.Options = []) -> Future { + Future { promise in + self.unlink(type, + options: options, + completion: promise) + } + } + + /** + Makes an *asynchronous* request to link a user with specified credentials. The user should already be logged in. + Publishes an instance of the successfully linked `ParseUser`. + + This also caches the user locally so that calls to *current* will use the latest logged in user. + - parameter type: The authentication type. + - parameter authData: The data that represents the authentication. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + static func linkPublisher(_ type: String, + authData: [String: String], + options: API.Options = []) -> Future { + Future { promise in + Self.link(type, + authData: authData, + options: options, + completion: promise) + } + } + +} + +#endif diff --git a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift index 412d17734..01957b0cf 100644 --- a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift +++ b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift @@ -7,6 +7,9 @@ // import Foundation +#if canImport(Combine) +import Combine +#endif /** Objects that conform to the `ParseAuthentication` protocol provide @@ -74,7 +77,7 @@ public protocol ParseAuthentication: Codable { - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. - It should have the following argument signature: `(Result)`. + It should have the following argument signature: `(Result)`. */ func unlink(options: API.Options, callbackQueue: DispatchQueue, @@ -93,6 +96,53 @@ public protocol ParseAuthentication: Codable { - returns: The user whose autentication type was stripped. This modified user has not been saved. */ func strip(_ user: AuthenticatedUser) -> AuthenticatedUser + + #if canImport(Combine) + /** + Login a `ParseUser` *asynchronously* using the respective authentication type. + - parameter authData: The authData for the respective authentication type. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: The block to execute. + */ + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func loginPublisher(authData: [String: String]?, + options: API.Options) -> Future + + /** + Link the *current* `ParseUser` *asynchronously* using the respective authentication type. + - parameter authData: The authData for the respective authentication type. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: The block to execute. + */ + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func linkPublisher(authData: [String: String]?, + options: API.Options) -> Future + + /** + Unlink the `ParseUser` *asynchronously* from the respective authentication type. + - parameter user: The `ParseUser` to unlink. The user must be logged in on this device. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: The block to execute. + It should have the following argument signature: `(Result)`. + */ + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func unlinkPublisher(_ user: AuthenticatedUser, + options: API.Options) -> Future + + /** + Unlink the *current* `ParseUser` *asynchronously* from the respective authentication type. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: The block to execute. + It should have the following argument signature: `(Result)`. + */ + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func unlinkPublisher(options: API.Options) -> Future + + #endif } // MARK: Convenience Implementations @@ -188,7 +238,7 @@ public extension ParseUser { static func login(_ type: String, authData: [String: String], options: API.Options, - callbackQueue: DispatchQueue, + callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { let body = SignupLoginBody(authData: [type: authData]) @@ -201,6 +251,12 @@ public extension ParseUser { } // MARK: 3rd Party Authentication - Link + /** + Whether the `ParseUser` is logged in with the respective authentication string type. + - parameter type: The authentication type to check. The user must be logged in on this device. + - returns: `true` if the `ParseUser` is logged in via the repective + authentication type. `false` if the user is not. + */ func isLinked(with type: String) -> Bool { guard let authData = self.authData?[type] else { return false @@ -208,9 +264,17 @@ public extension ParseUser { return authData != nil } + /** + Unlink the authentication type *asynchronously*. + - parameter type: The type to unlink. The user must be logged in on this device. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: The block to execute. + It should have the following argument signature: `(Result)`. + */ func unlink(_ type: String, - options: API.Options, - callbackQueue: DispatchQueue, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { guard let current = Self.current, @@ -281,8 +345,8 @@ public extension ParseUser { */ static func link(_ type: String, authData: [String: String], - options: API.Options, - callbackQueue: DispatchQueue, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { guard let current = Self.current else { let error = ParseError(code: .unknownError, message: "Must be logged in to link user") diff --git a/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift b/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift index 2de4525ce..20ef4af09 100644 --- a/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift +++ b/Sources/ParseSwift/LiveQuery/LiveQuerySocket.swift @@ -11,7 +11,7 @@ import Foundation import FoundationNetworking #endif -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) final class LiveQuerySocket: NSObject { private var session: URLSession! var delegates = [URLSessionWebSocketTask: LiveQuerySocketDelegate]() @@ -36,7 +36,7 @@ final class LiveQuerySocket: NSObject { } // MARK: Status -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension LiveQuerySocket { enum Status: String { case open @@ -45,7 +45,7 @@ extension LiveQuerySocket { } // MARK: Connect -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension LiveQuerySocket { func connect(task: URLSessionWebSocketTask, completion: @escaping (Error?) -> Void) throws { @@ -63,7 +63,7 @@ extension LiveQuerySocket { } // MARK: Send -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension LiveQuerySocket { func send(_ data: Data, task: URLSessionWebSocketTask, completion: @escaping (Error?) -> Void) { guard let encodedAsString = String(data: data, encoding: .utf8) else { @@ -80,7 +80,7 @@ extension LiveQuerySocket { } // MARK: Receive -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension LiveQuerySocket { func receive(_ task: URLSessionWebSocketTask) { @@ -105,13 +105,13 @@ extension LiveQuerySocket { } // MARK: URLSession -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension URLSession { static let liveQuery = LiveQuerySocket() } // MARK: URLSessionWebSocketDelegate -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension LiveQuerySocket: URLSessionWebSocketDelegate { func urlSession(_ session: URLSession, webSocketTask: URLSessionWebSocketTask, diff --git a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift index 7e7361c80..a71d42a26 100644 --- a/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift +++ b/Sources/ParseSwift/LiveQuery/ParseLiveQuery.swift @@ -44,7 +44,7 @@ import FoundationNetworking running. Initializing new instances will create a new task/connection to the `ParseLiveQuery` server. When an instance is deinitialized it will automatically close it's connection gracefully. */ -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public final class ParseLiveQuery: NSObject { // Queues let synchronizationQueue: DispatchQueue @@ -211,7 +211,7 @@ public final class ParseLiveQuery: NSObject { } // MARK: Helpers -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery { static var client = try? ParseLiveQuery() @@ -293,7 +293,7 @@ extension ParseLiveQuery { } // MARK: Delegate -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery: LiveQuerySocketDelegate { func status(_ status: LiveQuerySocket.Status) { @@ -483,7 +483,7 @@ extension ParseLiveQuery: LiveQuerySocketDelegate { } // MARK: Connection -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery { /// Manually establish a connection to the `ParseLiveQuery` Server. @@ -557,7 +557,7 @@ extension ParseLiveQuery { } // MARK: SubscriptionRecord -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery { class SubscriptionRecord { @@ -612,7 +612,7 @@ extension ParseLiveQuery { } // MARK: Subscribing -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery { func subscribe(_ query: Query) throws -> Subscription { @@ -641,7 +641,7 @@ extension ParseLiveQuery { } // MARK: Unsubscribing -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery { func unsubscribe(_ query: Query) throws where T: ParseObject { @@ -674,7 +674,7 @@ extension ParseLiveQuery { } // MARK: Updating -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQuery { func update(_ handler: T) throws where T: ParseSubscription { @@ -691,9 +691,9 @@ extension ParseLiveQuery { } // MARK: ParseLiveQuery - Subscribe -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension Query { - #if !os(Linux) + #if canImport(Combine) /** Registers the query for live updates, using the default subscription handler, and the default `ParseLiveQuery` client. Suitable for `ObjectObserved` @@ -760,7 +760,7 @@ public extension Query { } // MARK: ParseLiveQuery - Unsubscribe -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension Query { /** Unsubscribes all current subscriptions for a given query on the default @@ -800,7 +800,7 @@ public extension Query { } // MARK: ParseLiveQuery - Update -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public extension Query { /** Updates an existing subscription with a new query on the default `ParseLiveQuery` client. diff --git a/Sources/ParseSwift/LiveQuery/Protocols/LiveQuerySocketDelegate.swift b/Sources/ParseSwift/LiveQuery/Protocols/LiveQuerySocketDelegate.swift index 1ab766170..e19070e67 100644 --- a/Sources/ParseSwift/LiveQuery/Protocols/LiveQuerySocketDelegate.swift +++ b/Sources/ParseSwift/LiveQuery/Protocols/LiveQuerySocketDelegate.swift @@ -11,7 +11,7 @@ import Foundation import FoundationNetworking #endif -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) protocol LiveQuerySocketDelegate: AnyObject { func status(_ status: LiveQuerySocket.Status) func close(useDedicatedQueue: Bool) diff --git a/Sources/ParseSwift/LiveQuery/Protocols/ParseLiveQueryDelegate.swift b/Sources/ParseSwift/LiveQuery/Protocols/ParseLiveQueryDelegate.swift index 9a717802a..1580fcadc 100644 --- a/Sources/ParseSwift/LiveQuery/Protocols/ParseLiveQueryDelegate.swift +++ b/Sources/ParseSwift/LiveQuery/Protocols/ParseLiveQueryDelegate.swift @@ -14,7 +14,7 @@ import FoundationNetworking // swiftlint:disable line_length ///Receive/respond to notifications from the ParseLiveQuery Server. -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public protocol ParseLiveQueryDelegate: AnyObject { /** @@ -59,7 +59,7 @@ public protocol ParseLiveQueryDelegate: AnyObject { #endif } -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) extension ParseLiveQueryDelegate { func received(_ challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, diff --git a/Sources/ParseSwift/LiveQuery/Subscription.swift b/Sources/ParseSwift/LiveQuery/Subscription.swift index 6e69c9bb6..e23264987 100644 --- a/Sources/ParseSwift/LiveQuery/Subscription.swift +++ b/Sources/ParseSwift/LiveQuery/Subscription.swift @@ -56,14 +56,13 @@ private func == (lhs: Event, rhs: Event) -> Bool { } } -#if !os(Linux) +#if canImport(Combine) /** A default implementation of the `ParseSubscription` protocol. Suitable for `ObjectObserved` as the subscription can be used as a SwiftUI publisher. Meaning it can serve - indepedently as a ViewModel in MVVM. Also provides a publisher for pull responses of query such as: - `find`, `first`, `count`, and `aggregate`. + indepedently as a ViewModel in MVVM. */ -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) open class Subscription: ParseSubscription, ObservableObject { //The query subscribed to. public var query: Query @@ -103,57 +102,6 @@ open class Subscription: ParseSubscription, ObservableObject { } } - /// The objects found in a `find`, `first`, or `aggregate` - /// query. - /// - note: this will only countain one item for `first`. - public internal(set) var results: [T]? { - willSet { - if newValue != nil { - resultsCodable = nil - count = nil - error = nil - objectWillChange.send() - } - } - } - - /// The number of items found in a `count` query. - public internal(set) var count: Int? { - willSet { - if newValue != nil { - results = nil - resultsCodable = nil - error = nil - objectWillChange.send() - } - } - } - - /// Results of a `explain` or `hint` query. - public internal(set) var resultsCodable: AnyCodable? { - willSet { - if newValue != nil { - results = nil - count = nil - error = nil - objectWillChange.send() - } - } - } - - /// If an error occured during a `find`, `first`, `count`, or `aggregate` - /// query. - public internal(set) var error: ParseError? { - willSet { - if newValue != nil { - count = nil - results = nil - resultsCodable = nil - objectWillChange.send() - } - } - } - /** Creates a new subscription that can be used to handle updates. */ @@ -180,154 +128,6 @@ open class Subscription: ParseSubscription, ObservableObject { open func didUnsubscribe() { self.unsubscribed = query } - - /** - Finds objects and publishes them as `results` afterwards. - - - parameter options: A set of header options sent to the server. Defaults to an empty set. - - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - */ - open func find(options: API.Options = [], callbackQueue: DispatchQueue = .main) { - query.find(options: options, callbackQueue: callbackQueue) { result in - switch result { - - case .success(let results): - self.results = results - case .failure(let error): - self.error = error - } - } - } - - /** - Finds objects and publishes them as `resultsCodable` afterwards. - - - parameter explain: Used to toggle the information on the query plan. - - parameter hint: String or Object of index that should be used when executing query. - - parameter options: A set of header options sent to the server. Defaults to an empty set. - - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - */ - open func find(explain: Bool, - hint: String? = nil, - options: API.Options = [], - callbackQueue: DispatchQueue = .main) { - query.find(explain: explain, hint: hint, options: options, callbackQueue: callbackQueue) { result in - switch result { - - case .success(let results): - self.resultsCodable = results - case .failure(let error): - self.error = error - } - } - } - - /** - Gets an object and publishes them as `results` afterwards. - - - warning: This method mutates the query. It will reset the limit to `1`. - - parameter options: A set of header options sent to the server. Defaults to an empty set. - - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - */ - open func first(options: API.Options = [], callbackQueue: DispatchQueue = .main) { - query.first(options: options, callbackQueue: callbackQueue) { result in - switch result { - - case .success(let results): - self.results = [results] - case .failure(let error): - self.error = error - } - } - } - - /** - Gets an object and publishes them as `resultsCodable` afterwards. - - - warning: This method mutates the query. It will reset the limit to `1`. - - parameter explain: Used to toggle the information on the query plan. - - parameter hint: String or Object of index that should be used when executing query. - - parameter options: A set of header options sent to the server. Defaults to an empty set. - - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - */ - open func first(explain: Bool, - hint: String? = nil, - options: API.Options = [], - callbackQueue: DispatchQueue = .main) { - query.first(explain: explain, hint: hint, options: options, callbackQueue: callbackQueue) { result in - switch result { - - case .success(let results): - self.resultsCodable = results - case .failure(let error): - self.error = error - } - } - } - - /** - Counts objects and publishes them as `count` afterwards. - - - parameter options: A set of header options sent to the server. Defaults to an empty set. - - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - */ - open func count(options: API.Options = [], callbackQueue: DispatchQueue = .main) { - query.count(options: options, callbackQueue: callbackQueue) { result in - switch result { - - case .success(let results): - self.count = results - case .failure(let error): - self.error = error - } - } - } - - /** - Counts objects and publishes them as `resultsCodable` afterwards. - - - parameter explain: Used to toggle the information on the query plan. - - parameter hint: String or Object of index that should be used when executing query. - - parameter options: A set of header options sent to the server. Defaults to an empty set. - - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - */ - open func count(explain: Bool, - hint: String? = nil, - options: API.Options = [], - callbackQueue: DispatchQueue = .main) { - query.count(explain: explain, hint: hint, options: options) { result in - switch result { - - case .success(let results): - self.resultsCodable = results - case .failure(let error): - self.error = error - } - } - } - - /** - Executes an aggregate query and publishes the results as `results` afterwards. - - - requires: `.useMasterKey` has to be available and passed as one of the set of `options`. - - parameter pipeline: A pipeline of stages to process query. - - parameter options: A set of header options sent to the server. Defaults to an empty set. - - parameter callbackQueue: The queue to return to after completion. Default value of `.main`. - - warning: This hasn't been tested thoroughly. - */ - open func aggregate(_ pipeline: Query.AggregateType, - options: API.Options = [], - callbackQueue: DispatchQueue = .main) { - query.aggregate(pipeline, options: options, callbackQueue: callbackQueue) { result in - switch result { - - case .success(let results): - self.results = results - case .failure(let error): - self.error = error - } - } - } } #endif diff --git a/Sources/ParseSwift/Objects/ParseInstallation+combine.swift b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift new file mode 100644 index 000000000..a3ab82a76 --- /dev/null +++ b/Sources/ParseSwift/Objects/ParseInstallation+combine.swift @@ -0,0 +1,109 @@ +// +// ParseInstallation+combine.swift +// ParseSwift +// +// Created by Corey Baker on 1/29/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) +import Foundation +import Combine + +// MARK: Combine +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseInstallation { + + // MARK: Fetchable - Combine + /** + Fetches the `ParseInstallation` *aynchronously* with the current data from the server + and sets an error if one occurs. Publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + func fetchPublisher(options: API.Options = []) -> Future { + Future { promise in + self.fetch(options: options, + completion: promise) + } + } + + // MARK: Savable - Combine + /** + Saves the `ParseInstallation` *asynchronously* and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + func savePublisher(options: API.Options = []) -> Future { + Future { promise in + self.save(options: options, + completion: promise) + } + } + + // MARK: Deletable - Combine + /** + Deletes the `ParseInstallation` *asynchronously* and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + func deletePublisher(options: API.Options = []) -> Future { + Future { promise in + self.delete(options: options, completion: promise) + } + } +} + +// MARK: Batch Support - Combine +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +public extension Sequence where Element: ParseInstallation { + /** + Fetches a collection of installations *aynchronously* with the current data from the server and sets + an error if one occurs. Publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + func fetchAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + self.fetchAll(options: options, + completion: promise) + } + } + + /** + Saves a collection of installations *asynchronously* and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + func saveAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + self.saveAll(options: options, + completion: promise) + } + } + + /** + Deletes a collection of installations *asynchronously* and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + func deleteAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + self.deleteAll(options: options, completion: promise) + } + } +} + +#endif diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 412e5113e..6da8db305 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -525,13 +525,13 @@ extension ParseInstallation { - 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: `(ParseError?)`. + 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 + completion: @escaping (Result) -> Void ) { do { try deleteCommand() @@ -539,35 +539,40 @@ extension ParseInstallation { callbackQueue.async { switch result { - case .success(let error): + case .success: Self.updateKeychainIfNeeded([self], deleting: true) - completion(error) + completion(.success(())) case .failure(let error): - completion(error) + completion(.failure(error)) } } } } catch let error as ParseError { callbackQueue.async { - completion(error) + completion(.failure(error)) } } catch { callbackQueue.async { - completion(ParseError(code: .unknownError, message: error.localizedDescription)) + completion(.failure(ParseError(code: .unknownError, message: error.localizedDescription))) } } } - func deleteCommand() throws -> API.NonParseBodyCommand { + func deleteCommand() throws -> API.NonParseBodyCommand { guard isSaved else { throw ParseError(code: .unknownError, message: "Cannot Delete an object without id") } - return API.NonParseBodyCommand( + return API.NonParseBodyCommand( method: .DELETE, path: endpoint - ) { (data) -> ParseError? in - try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + ) { (data) -> NoBody in + let error = try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + if let error = error { + throw error + } else { + return NoBody() + } } } } @@ -858,13 +863,13 @@ public extension Sequence where Element: ParseInstallation { - important: If an object deleted has the same objectId as current, it will automatically update the current. */ func deleteAll(batchLimit limit: Int? = nil, - options: API.Options = []) throws -> [ParseError?] { + options: API.Options = []) throws -> [(Result)] { let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit - var returnBatch = [ParseError?]() + var returnBatch = [(Result)]() let commands = try map { try $0.deleteCommand() } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { - let currentBatch = try API.Command + let currentBatch = try API.Command)> .batch(commands: $0) .execute(options: options) returnBatch.append(contentsOf: currentBatch) @@ -897,11 +902,11 @@ public extension Sequence where Element: ParseInstallation { batchLimit limit: Int? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (Result<[ParseError?], ParseError>) -> Void + completion: @escaping (Result<[(Result)], ParseError>) -> Void ) { let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit do { - var returnBatch = [ParseError?]() + var returnBatch = [(Result)]() let commands = try map({ try $0.deleteCommand() }) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 diff --git a/Sources/ParseSwift/Objects/ParseObject+combine.swift b/Sources/ParseSwift/Objects/ParseObject+combine.swift new file mode 100644 index 000000000..973fe4e43 --- /dev/null +++ b/Sources/ParseSwift/Objects/ParseObject+combine.swift @@ -0,0 +1,106 @@ +// +// ParseObject+combine.swift +// ParseSwift +// +// Created by Corey Baker on 1/29/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) +import Foundation +import Combine + +// MARK: Combine +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseObject { + + /** + Fetches the `ParseObject` *aynchronously* with the current data from the server and sets an error if one occurs. + Publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + func fetchPublisher(options: API.Options = []) -> Future { + Future { promise in + self.fetch(options: options, + completion: promise) + } + } + + /** + Saves the `ParseObject` *asynchronously* and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + func savePublisher(options: API.Options = []) -> Future { + Future { promise in + self.save(options: options, + completion: promise) + } + } + + /** + Deletes the `ParseObject` *asynchronously* and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + func deletePublisher(options: API.Options = []) -> Future { + Future { promise in + self.delete(options: options, completion: promise) + } + } +} + +// MARK: Batch Support - Combine +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +public extension Sequence where Element: ParseObject { + /** + Fetches a collection of objects *aynchronously* with the current data from the server and sets + an error if one occurs. Publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + func fetchAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + self.fetchAll(options: options, + completion: promise) + } + } + + /** + Saves a collection of objects *asynchronously* and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + func saveAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + self.saveAll(options: options, + completion: promise) + } + } + + /** + Deletes a collection of objects *asynchronously* and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + func deleteAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + self.deleteAll(options: options, completion: promise) + } + } +} + +#endif diff --git a/Sources/ParseSwift/Objects/ParseObject.swift b/Sources/ParseSwift/Objects/ParseObject.swift index 3df6eece9..8703ae1c5 100644 --- a/Sources/ParseSwift/Objects/ParseObject.swift +++ b/Sources/ParseSwift/Objects/ParseObject.swift @@ -331,13 +331,13 @@ public extension Sequence where Element: ParseObject { - throws: `ParseError` */ func deleteAll(batchLimit limit: Int? = nil, - options: API.Options = []) throws -> [ParseError?] { + options: API.Options = []) throws -> [(Result)] { let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit - var returnBatch = [ParseError?]() + var returnBatch = [(Result)]() let commands = try map { try $0.deleteCommand() } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { - let currentBatch = try API.Command + let currentBatch = try API.Command)> .batch(commands: $0) .execute(options: options) returnBatch.append(contentsOf: currentBatch) @@ -367,11 +367,11 @@ public extension Sequence where Element: ParseObject { batchLimit limit: Int? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (Result<[ParseError?], ParseError>) -> Void + completion: @escaping (Result<[(Result)], ParseError>) -> Void ) { let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit do { - var returnBatch = [ParseError?]() + var returnBatch = [(Result)]() let commands = try map({ try $0.deleteCommand() }) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 @@ -703,9 +703,7 @@ extension ParseObject { - throws: An error of `ParseError` type. */ public func delete(options: API.Options = []) throws { - if let error = try deleteCommand().execute(options: options) { - throw error - } + _ = try deleteCommand().execute(options: options) } /** @@ -715,37 +713,37 @@ extension ParseObject { - 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: `(ParseError?)`. + It should have the following argument signature: `(Result)`. */ public func delete( options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (ParseError?) -> Void + completion: @escaping (Result) -> Void ) { do { try deleteCommand().executeAsync(options: options) { result in callbackQueue.async { switch result { - case .success(let error): - completion(error) + case .success: + completion(.success(())) case .failure(let error): - completion(error) + completion(.failure(error)) } } } } catch let error as ParseError { callbackQueue.async { - completion(error) + completion(.failure(error)) } } catch { callbackQueue.async { - completion(ParseError(code: .unknownError, message: error.localizedDescription)) + completion(.failure(ParseError(code: .unknownError, message: error.localizedDescription))) } } } - internal func deleteCommand() throws -> API.NonParseBodyCommand { - try API.NonParseBodyCommand.deleteCommand(self) + internal func deleteCommand() throws -> API.NonParseBodyCommand { + try API.NonParseBodyCommand.deleteCommand(self) } }// swiftlint:disable:this file_length diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift new file mode 100644 index 000000000..3b02f554a --- /dev/null +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -0,0 +1,230 @@ +// +// ParseUser+publisher.swift +// ParseSwift +// +// Created by Corey Baker on 1/28/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) +import Foundation +import Combine + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseUser { + + // MARK: Signing Out - Combine + /** + Signs up the user *asynchronously* and publishes value. + + This will also enforce that the username isn't already taken. + + - warning: Make sure that password and username are set before calling this method. + - parameter username: The username of the user. + - parameter password: The password of the user. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + static func signupPublisher(username: String, + password: String, + options: API.Options = []) -> Future { + Future { promise in + Self.signup(username: username, + password: password, + options: options, + completion: promise) + } + } + + /** + Signs up the user *asynchronously* and publishes value. + + This will also enforce that the username isn't already taken. + + - warning: Make sure that password and username are set before calling this method. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func signupPublisher(options: API.Options = []) -> Future { + Future { promise in + self.signup(options: options, + completion: promise) + } + } + + // MARK: Logging In - Combine + /** + Makes an *asynchronous* request to log in a user with specified credentials. + Publishes an instance of the successfully logged in `ParseUser`. + + This also caches the user locally so that calls to *current* will use the latest logged in user. + - parameter username: The username of the user. + - parameter password: The password of the user. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + static func loginPublisher(username: String, + password: String, + options: API.Options = []) -> Future { + Future { promise in + Self.login(username: username, + password: password, + options: options, + completion: promise) + } + } + + /** + Logs in a `ParseUser` *asynchronously* with a session token. + Publishes an instance of the successfully logged in `ParseUser`. + If successful, this saves the session to the keychain, so you can retrieve the currently logged in user + using *current*. + + - parameter sessionToken: The sessionToken of the user to login. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func becomePublisher(sessionToken: String, options: API.Options = []) -> Future { + Future { promise in + self.become(sessionToken: sessionToken, options: options, completion: promise) + } + } + + // MARK: Logging Out - Combine + /** + Logs out the currently logged in user *asynchronously*. Publishes when complete. + + This will also remove the session from the Keychain, log out of linked services + and all future calls to `current` will return `nil`. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + static func logoutPublisher(options: API.Options = []) -> Future { + Future { promise in + Self.logout(options: options, completion: promise) + } + } + + // MARK: Password Reset - Combine + /** + Requests *asynchronously* a password reset email to be sent to the specified email address + associated with the user account. This email allows the user to securely reset their password on the web. + Publishes when complete. + - parameter email: The email address associated with the user that forgot their password. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + static func passwordResetPublisher(email: String, + options: API.Options = []) -> Future { + Future { promise in + Self.passwordReset(email: email, options: options, completion: promise) + } + } + + // MARK: Verification Email Request - Combine + /** + Requests *asynchronously* a verification email be sent to the specified email address + associated with the user account. Publishes when complete. + - parameter email: The email address associated with the user. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + static func verificationEmailPublisher(email: String, + options: API.Options = []) -> Future { + Future { promise in + Self.verificationEmail(email: email, options: options, completion: promise) + } + } + + // MARK: Fetchable - Combine + /** + Fetches the `ParseUser` *aynchronously* with the current data from the server and sets an error if one occurs. + Publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + func fetchPublisher(options: API.Options = []) -> Future { + Future { promise in + self.fetch(options: options, + completion: promise) + } + } + + // MARK: Savable - Combine + /** + Saves the `ParseUser` *asynchronously* and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + func savePublisher(options: API.Options = []) -> Future { + Future { promise in + self.save(options: options, + completion: promise) + } + } + + // MARK: Deletable - Combine + /** + Deletes the `ParseUser` *asynchronously* and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + func deletePublisher(options: API.Options = []) -> Future { + Future { promise in + self.delete(options: options, completion: promise) + } + } +} + +// MARK: Batch Support - Combine +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +public extension Sequence where Element: ParseUser { + /** + Fetches a collection of users *aynchronously* with the current data from the server and sets + an error if one occurs. Publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + func fetchAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + self.fetchAll(options: options, + completion: promise) + } + } + + /** + Saves a collection of users *asynchronously* and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object saved has the same objectId as current, it will automatically update the current. + */ + func saveAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + self.saveAll(options: options, + completion: promise) + } + } + + /** + Deletes a collection of users *asynchronously* and publishes when complete. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + - important: If an object deleted has the same objectId as current, it will automatically update the current. + */ + func deleteAllPublisher(options: API.Options = []) -> Future<[(Result)], ParseError> { + Future { promise in + self.deleteAll(options: options, completion: promise) + } + } +} +#endif diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 8a47102d5..b55adfbef 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -235,7 +235,6 @@ extension ParseUser { 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 become(sessionToken: String, options: API.Options = [], @@ -299,7 +298,15 @@ extension ParseUser { Logs out the currently logged in user in Keychain *synchronously*. */ public static func logout(options: API.Options = []) throws { - _ = try logoutCommand().execute(options: options) + let error = try? logoutCommand().execute(options: options) + //Always let user logout locally, no matter the error. + deleteCurrentContainerFromKeychain() + BaseParseInstallation.deleteCurrentContainerFromKeychain() + BaseConfig.deleteCurrentContainerFromKeychain() + //Wait to throw error + if let parseError = error { + throw parseError + } } /** @@ -313,43 +320,38 @@ extension ParseUser { - parameter completion: A block that will be called when logging out, completes or fails. */ public static func logout(options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (ParseError?) -> Void) { - callbackQueue.async { - logoutCommand().executeAsync(options: options) { result in + completion: @escaping (Result) -> Void) { + logoutCommand().executeAsync(options: options) { result in + callbackQueue.async { + + //Always let user logout locally, no matter the error. + deleteCurrentContainerFromKeychain() + BaseParseInstallation.deleteCurrentContainerFromKeychain() + BaseConfig.deleteCurrentContainerFromKeychain() switch result { - case .success: - completion(nil) + case .success(let error): + if let error = error { + completion(.failure(error)) + } else { + completion(.success(())) + } case .failure(let error): - completion(error) + completion(.failure(error)) } } } } - internal static func logoutCommand() -> API.NonParseBodyCommand { - return API.NonParseBodyCommand(method: .POST, path: .logout) { (data) -> NoBody in - var parseError: ParseError? - var serverResponse = NoBody() - //Always let user logout locally, no matter the error. - deleteCurrentContainerFromKeychain() - BaseParseInstallation.deleteCurrentContainerFromKeychain() - BaseConfig.deleteCurrentContainerFromKeychain() - + internal static func logoutCommand() -> API.NonParseBodyCommand { + return API.NonParseBodyCommand(method: .POST, path: .logout) { (data) -> ParseError? in do { - serverResponse = try ParseCoding.jsonDecoder().decode(NoBody.self, from: data) + let parseError = try ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + return parseError } catch { - if let foundError = error as? ParseError { - parseError = foundError - } else { - parseError = ParseError(code: .unknownError, message: error.localizedDescription) - } - } - guard let error = parseError else { - return serverResponse + return nil } - throw error } } } @@ -379,15 +381,19 @@ extension ParseUser { */ public static func passwordReset(email: String, options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (ParseError?) -> Void) { + completion: @escaping (Result) -> Void) { passwordResetCommand(email: email).executeAsync(options: options) { result in callbackQueue.async { switch result { case .success(let error): - completion(error) + if let error = error { + completion(.failure(error)) + } else { + completion(.success(())) + } case .failure(let error): - completion(error) + completion(.failure(error)) } } } @@ -410,9 +416,9 @@ extension ParseUser { - parameter email: The email address associated with the user. - parameter options: A set of header options sent to the server. Defaults to an empty set. */ - public static func verificationEmailRequest(email: String, - options: API.Options = []) throws { - if let error = try verificationEmailRequestCommand(email: email).execute(options: options) { + public static func verificationEmail(email: String, + options: API.Options = []) throws { + if let error = try verificationEmailCommand(email: email).execute(options: options) { throw error } } @@ -425,30 +431,33 @@ extension ParseUser { - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: A block that will be called when the verification request completes or fails. */ - public static func verificationEmailRequest(email: String, - options: API.Options = [], - callbackQueue: DispatchQueue = .main, - completion: @escaping (ParseError?) -> Void) { - verificationEmailRequestCommand(email: email) + public static func verificationEmail(email: String, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + verificationEmailCommand(email: email) .executeAsync(options: options) { result in callbackQueue.async { switch result { case .success(let error): - completion(error) + if let error = error { + completion(.failure(error)) + } else { + completion(.success(())) + } case .failure(let error): - completion(error) + completion(.failure(error)) } } } } - // swiftlint:disable:next line_length - internal static func verificationEmailRequestCommand(email: String) -> API.NonParseBodyCommand { + internal static func verificationEmailCommand(email: String) -> API.NonParseBodyCommand { let emailBody = EmailBody(email: email) return API.NonParseBodyCommand(method: .POST, - path: .verificationEmailRequest, + path: .verificationEmail, body: emailBody) { (data) -> ParseError? in try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) } @@ -838,50 +847,55 @@ extension ParseUser { - 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: `(ParseError?)`. + 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 + completion: @escaping (Result) -> Void ) { do { try deleteCommand().executeAsync(options: options) { result in switch result { - case .success(let error): + case .success: callbackQueue.async { try? Self.updateKeychainIfNeeded([self], deleting: true) - completion(error) + completion(.success(())) } case .failure(let error): callbackQueue.async { - completion(error) + completion(.failure(error)) } } } } catch let error as ParseError { callbackQueue.async { - completion(error) + completion(.failure(error)) } } catch { callbackQueue.async { - completion(ParseError(code: .unknownError, message: error.localizedDescription)) + completion(.failure(ParseError(code: .unknownError, message: error.localizedDescription))) } } } - func deleteCommand() throws -> API.NonParseBodyCommand { + func deleteCommand() throws -> API.NonParseBodyCommand { guard isSaved else { throw ParseError(code: .unknownError, message: "Cannot Delete an object without id") } - return API.NonParseBodyCommand( + return API.NonParseBodyCommand( method: .DELETE, path: endpoint - ) { (data) -> ParseError? in - try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + ) { (data) -> NoBody in + let error = try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + if let error = error { + throw error + } else { + return NoBody() + } } } } @@ -1170,9 +1184,9 @@ public extension Sequence where Element: ParseUser { - important: If an object deleted has the same objectId as current, it will automatically update the current. */ func deleteAll(batchLimit limit: Int? = nil, - options: API.Options = []) throws -> [ParseError?] { + options: API.Options = []) throws -> [(Result)] { let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit - var returnBatch = [ParseError?]() + var returnBatch = [(Result)]() let commands = try map { try $0.deleteCommand() } let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) try batches.forEach { @@ -1208,11 +1222,11 @@ public extension Sequence where Element: ParseUser { batchLimit limit: Int? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (Result<[ParseError?], ParseError>) -> Void + completion: @escaping (Result<[(Result)], ParseError>) -> Void ) { let batchLimit = limit != nil ? limit! : ParseConstants.batchLimit do { - var returnBatch = [ParseError?]() + var returnBatch = [(Result)]() let commands = try map({ try $0.deleteCommand() }) let batches = BatchUtils.splitArray(commands, valuesPerSegment: batchLimit) var completed = 0 diff --git a/Sources/ParseSwift/Operations/ParseOperation+combine.swift b/Sources/ParseSwift/Operations/ParseOperation+combine.swift new file mode 100644 index 000000000..cef9b79e2 --- /dev/null +++ b/Sources/ParseSwift/Operations/ParseOperation+combine.swift @@ -0,0 +1,32 @@ +// +// ParseOperation+combine.swift +// ParseSwift +// +// Created by Corey Baker on 1/29/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) +import Foundation +import Combine + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseOperation { + + // MARK: Savable - Combine + + /** + Saves the operations on the `ParseObject` *asynchronously* and executes the given callback block. + + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func savePublisher(options: API.Options = []) -> Future { + Future { promise in + self.save(options: options, + completion: promise) + } + } +} + +#endif diff --git a/Sources/ParseSwift/Types/ParseCloud+combine.swift b/Sources/ParseSwift/Types/ParseCloud+combine.swift new file mode 100644 index 000000000..ef3405af5 --- /dev/null +++ b/Sources/ParseSwift/Types/ParseCloud+combine.swift @@ -0,0 +1,48 @@ +// +// ParseCloud+combine.swift +// ParseSwift +// +// Created by Corey Baker on 1/29/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) +import Foundation +import Combine + +// MARK: Combine +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseCloud { + + // MARK: Functions - Combine + + /** + Calls a Cloud Code function *asynchronously* and returns a result of it's execution. + Publishes when complete. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func runFunctionPublisher(options: API.Options = []) -> Future { + Future { promise in + self.runFunction(options: options, + completion: promise) + } + } + + // MARK: Jobs - Combine + + /** + Starts a Cloud Code job *asynchronously* and returns a result with the jobStatusId of the job. + Publishes when complete. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func startJobPublisher(options: API.Options = []) -> Future { + Future { promise in + self.startJob(options: options, + completion: promise) + } + } +} + +#endif diff --git a/Sources/ParseSwift/Types/ParseConfig+combine.swift b/Sources/ParseSwift/Types/ParseConfig+combine.swift new file mode 100644 index 000000000..13ea16656 --- /dev/null +++ b/Sources/ParseSwift/Types/ParseConfig+combine.swift @@ -0,0 +1,46 @@ +// +// ParseConfig+combine.swift +// ParseSwift +// +// Created by Corey Baker on 1/29/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) +import Foundation +import Combine + +// MARK: Combine +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseConfig { + + // MARK: Fetchable - Combine + + /** + Fetch the Config *asynchronously*. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func fetchPublisher(options: API.Options = []) -> Future { + Future { promise in + self.fetch(options: options, + completion: promise) + } + } + + // MARK: Savable - Combine + + /** + Update the Config *asynchronously*. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func savePublisher(options: API.Options = []) -> Future { + Future { promise in + self.save(options: options, + completion: promise) + } + } +} + +#endif diff --git a/Sources/ParseSwift/Types/ParseFile+combine.swift b/Sources/ParseSwift/Types/ParseFile+combine.swift new file mode 100644 index 000000000..d22be7d9e --- /dev/null +++ b/Sources/ParseSwift/Types/ParseFile+combine.swift @@ -0,0 +1,94 @@ +// +// ParseFile+combine.swift +// ParseSwift +// +// Created by Corey Baker on 1/29/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) +import Foundation +import Combine + +// MARK: Combine +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +public extension ParseFile { + + /** + Fetches a file with given url *synchronously*. Publishes when complete. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func fetchPublisher(options: API.Options = []) -> Future { + Future { promise in + self.fetch(options: options, + completion: promise) + } + } + + /** + Fetches a file with given url *synchronously*. Publishes when complete. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter progress: A block that will be called when file updates it's progress. + It should have the following argument signature: `(task: URLSessionDownloadTask, + bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)`. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func fetchPublisher(options: API.Options = [], + progress: @escaping ((URLSessionDownloadTask, + Int64, Int64, Int64) -> Void)) -> Future { + Future { promise in + self.fetch(options: options, + progress: progress, + completion: promise) + } + } + + /** + Creates a file with given data *asynchronously* and executes the given callback block. + Publishes when complete. + A name will be assigned to it by the server. If the file hasn't been downloaded, it will automatically + be downloaded before saved. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func savePublisher(options: API.Options = []) -> Future { + Future { promise in + self.save(options: options, + completion: promise) + } + } + + /** + Creates a file with given data *asynchronously* and executes the given callback block. + A name will be assigned to it by the server. If the file hasn't been downloaded, it will automatically + be downloaded before saved. Publishes when complete. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter progress: A block that will be called when file updates it's progress. + It should have the following argument signature: `(task: URLSessionDownloadTask, + bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)`. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func savePublisher(options: API.Options = [], + progress: ((URLSessionTask, Int64, Int64, Int64) -> Void)? = nil) -> Future { + Future { promise in + self.save(options: options, + progress: progress, + completion: promise) + } + } + + /** + Deletes the file from the Parse Server. Publishes when complete. + - requires: `.useMasterKey` has to be available and passed as one of the set of `options`. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func deletePublisher(options: API.Options = []) -> Future { + Future { promise in + self.delete(options: options, completion: promise) + } + } +} + +#endif diff --git a/Sources/ParseSwift/Types/ParseFile.swift b/Sources/ParseSwift/Types/ParseFile.swift index 8d56bf9e1..6679eaba0 100644 --- a/Sources/ParseSwift/Types/ParseFile.swift +++ b/Sources/ParseSwift/Types/ParseFile.swift @@ -179,19 +179,19 @@ extension ParseFile { - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: A block that will be called when file deletes or fails. - It should have the following argument signature: `(ParseError?)` + It should have the following argument signature: `(Result)` */ public func delete(options: API.Options, callbackQueue: DispatchQueue = .main, - completion: @escaping (ParseError?) -> Void) { + completion: @escaping (Result) -> Void) { var options = options options = options.union(self.options) if !options.contains(.useMasterKey) { callbackQueue.async { - completion(ParseError(code: .unknownError, + completion(.failure(ParseError(code: .unknownError, // swiftlint:disable:next line_length - message: "You must specify \"useMasterKey\" in \"options\" in order to delete a file.")) + message: "You must specify \"useMasterKey\" in \"options\" in order to delete a file."))) } return } @@ -200,9 +200,9 @@ extension ParseFile { switch result { case .success: - completion(nil) + completion(.success(())) case .failure(let error): - completion(error) + completion(.failure(error)) } } } @@ -413,7 +413,7 @@ extension ParseFile { It should have the following argument signature: `(task: URLSessionDownloadTask, bytesWritten: Int64, totalBytesWritten: Int64, totalBytesExpectedToWrite: Int64)`. - parameter completion: A block that will be called when file saves or fails. - It should have the following argument signature: `(Result)` + It should have the following argument signature: `(Result)`. */ public func save(options: API.Options = [], callbackQueue: DispatchQueue = .main, diff --git a/Sources/ParseSwift/Types/Query+combine.swift b/Sources/ParseSwift/Types/Query+combine.swift new file mode 100644 index 000000000..d7b591231 --- /dev/null +++ b/Sources/ParseSwift/Types/Query+combine.swift @@ -0,0 +1,126 @@ +// +// Query+combine.swift +// ParseSwift +// +// Created by Corey Baker on 1/29/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) +import Foundation +import Combine + +// MARK: Combine +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +public extension Query { + + // MARK: Queryable - Combine + + /** + Finds objects *asynchronously* and publishes when complete. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func findPublisher(options: API.Options = []) -> Future<[ResultType], ParseError> { + Future { promise in + self.find(options: options, + completion: promise) + } + } + + /** + Finds objects *asynchronously* and publishes when complete. + - parameter explain: Used to toggle the information on the query plan. + - parameter hint: String or Object of index that should be used when executing query. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func findPublisher(explain: Bool, + hint: String? = nil, + options: API.Options = []) -> Future { + Future { promise in + self.find(explain: explain, + hint: hint, + options: options, + completion: promise) + } + } + + /** + Gets an object *asynchronously* and publishes when complete. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func firstPublisher(options: API.Options = []) -> Future { + Future { promise in + self.first(options: options, + completion: promise) + } + } + + /** + Gets an object *asynchronously* and publishes when complete. + - parameter explain: Used to toggle the information on the query plan. + - parameter hint: String or Object of index that should be used when executing query. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func firstPublisher(explain: Bool, + hint: String? = nil, + options: API.Options = []) -> Future { + Future { promise in + self.first(explain: explain, + hint: hint, + options: options, + completion: promise) + } + } + + /** + Count objects *asynchronously* and publishes when complete. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func countPublisher(options: API.Options = []) -> Future { + Future { promise in + self.count(options: options, + completion: promise) + } + } + + /** + Count objects *asynchronously* and publishes when complete. + - parameter explain: Used to toggle the information on the query plan. + - parameter hint: String or Object of index that should be used when executing query. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func countPublisher(explain: Bool, + hint: String? = nil, + options: API.Options = []) -> Future { + Future { promise in + self.count(explain: explain, + hint: hint, + options: options, + completion: promise) + } + } + + /** + Executes an aggregate query *asynchronously* and publishes when complete. + - requires: `.useMasterKey` has to be available and passed as one of the set of `options`. + - parameter pipeline: A pipeline of stages to process query. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func aggregatePublisher(_ pipeline: AggregateType, + options: API.Options = []) -> Future<[ResultType], ParseError> { + Future { promise in + self.aggregate(pipeline, + options: options, + completion: promise) + } + } +} + +#endif diff --git a/Sources/ParseSwift/Types/Query.swift b/Sources/ParseSwift/Types/Query.swift index 219ff4757..4bed40fbe 100644 --- a/Sources/ParseSwift/Types/Query.swift +++ b/Sources/ParseSwift/Types/Query.swift @@ -754,7 +754,7 @@ public struct Query: Encodable, Equatable where T: ParseObject { - warning: This is only for `ParseLiveQuery`. - parameter keys: A variadic list of fields to receive back instead of the whole `ParseObject`. */ - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public func fields(_ keys: String...) -> Query { var mutableQuery = self mutableQuery.fields = keys @@ -771,7 +771,7 @@ public struct Query: Encodable, Equatable where T: ParseObject { - warning: This is only for `ParseLiveQuery`. - parameter keys: An array of fields to receive back instead of the whole `ParseObject`. */ - @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) public func fields(_ keys: [String]) -> Query { var mutableQuery = self mutableQuery.fields = keys @@ -872,7 +872,7 @@ extension Query: Queryable { - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute. - It should have the following argument signature: `(Result<[AnyResultType], ParseError>)`. + It should have the following argument signature: `(Result)`. */ public func find(explain: Bool, hint: String? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, diff --git a/Tests/ParseSwiftTests/ParseAnonymousCombineTests.swift b/Tests/ParseSwiftTests/ParseAnonymousCombineTests.swift new file mode 100644 index 000000000..f1dff0e3e --- /dev/null +++ b/Tests/ParseSwiftTests/ParseAnonymousCombineTests.swift @@ -0,0 +1,136 @@ +// +// ParseAnonymousCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseAuthenticationCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + 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]?]? + } + + 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() { + 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" + } + } + + override func setUpWithError() throws { + super.setUp() + 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, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testLogin() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + var serverResponse = LoginSignupResponse() + let authData = ParseAnonymous.AuthenticationKeys.id.makeDictionary() + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.anonymous.__type: authData] + serverResponse.createdAt = Date() + serverResponse.updatedAt = serverResponse.createdAt?.addingTimeInterval(+300) + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.anonymous.loginPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user, userOnServer) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertTrue(user.anonymous.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseAppleCombineTests.swift b/Tests/ParseSwiftTests/ParseAppleCombineTests.swift new file mode 100644 index 000000000..feebc740c --- /dev/null +++ b/Tests/ParseSwiftTests/ParseAppleCombineTests.swift @@ -0,0 +1,249 @@ +// +// ParseAppleCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseAppleCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + 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]?]? + } + + 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() { + 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" + } + } + + override func setUpWithError() throws { + super.setUp() + 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, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testLogin() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + var serverResponse = LoginSignupResponse() + let authData = ParseAnonymous.AuthenticationKeys.id.makeDictionary() + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.apple.__type: authData] + serverResponse.createdAt = Date() + serverResponse.updatedAt = serverResponse.createdAt?.addingTimeInterval(+300) + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.apple.loginPublisher(user: "testing", identityToken: "this") + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user, userOnServer) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertTrue(user.apple.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + 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 testLink() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + _ = try loginNormally() + MockURLProtocol.removeAll() + + var serverResponse = LoginSignupResponse() + serverResponse.updatedAt = Date() + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.apple.linkPublisher(user: "testing", identityToken: "this") + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user.updatedAt, userOnServer.updatedAt) + XCTAssertEqual(user.username, "parse") + XCTAssertNil(user.password) + XCTAssertTrue(user.apple.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testUnlink() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + _ = try loginNormally() + MockURLProtocol.removeAll() + + let authData = ParseApple + .AuthenticationKeys.id.makeDictionary(user: "testing", + identityToken: "this") + User.current?.authData = [User.apple.__type: authData] + XCTAssertTrue(User.apple.isLinked) + + var serverResponse = LoginSignupResponse() + serverResponse.updatedAt = Date() + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.apple.unlinkPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user.updatedAt, userOnServer.updatedAt) + XCTAssertEqual(user.username, "parse") + XCTAssertNil(user.password) + XCTAssertFalse(user.apple.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseAuthenticationTests.swift b/Tests/ParseSwiftTests/ParseAuthenticationTests.swift index 827eb358b..e7d95475f 100644 --- a/Tests/ParseSwiftTests/ParseAuthenticationTests.swift +++ b/Tests/ParseSwiftTests/ParseAuthenticationTests.swift @@ -9,6 +9,9 @@ import Foundation import XCTest @testable import ParseSwift +#if canImport(Combine) +import Combine +#endif class ParseAuthenticationTests: XCTestCase { @@ -46,6 +49,26 @@ class ParseAuthenticationTests: XCTestCase { let error = ParseError(code: .unknownError, message: "Not implemented") completion(.failure(error)) } + + #if canImport(Combine) + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func loginPublisher(authData: [String: String]?, + options: API.Options) -> Future { + let error = ParseError(code: .unknownError, message: "Not implemented") + return Future { promise in + promise(.failure(error)) + } + } + + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func linkPublisher(authData: [String: String]?, + options: API.Options) -> Future { + let error = ParseError(code: .unknownError, message: "Not implemented") + return Future { promise in + promise(.failure(error)) + } + } + #endif } override func setUpWithError() throws { diff --git a/Tests/ParseSwiftTests/ParseCloudCombineTests.swift b/Tests/ParseSwiftTests/ParseCloudCombineTests.swift new file mode 100644 index 000000000..367d79bf1 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseCloudCombineTests.swift @@ -0,0 +1,113 @@ +// +// ParseCloudCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseCloudCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + struct Cloud: ParseCloud { + // Those are required for Object + var functionJobName: String + } + + override func setUpWithError() throws { + super.setUp() + 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, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testFunction() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + let response = AnyResultResponse(result: nil) + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let cloud = Cloud(functionJobName: "test") + let publisher = cloud.runFunctionPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { functionResponse in + + XCTAssertEqual(functionResponse, AnyCodable()) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testJob() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + let response = AnyResultResponse(result: nil) + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(response) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let cloud = Cloud(functionJobName: "test") + let publisher = cloud.startJobPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { functionResponse in + + XCTAssertEqual(functionResponse, AnyCodable()) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseConfigCombineTests.swift b/Tests/ParseSwiftTests/ParseConfigCombineTests.swift new file mode 100644 index 000000000..7b2eea0e9 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseConfigCombineTests.swift @@ -0,0 +1,216 @@ +// +// ParseConfigCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseConfigCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + struct Config: ParseConfig { + var welcomeMessage: String? + var winningNumber: Int? + } + + 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 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() { + 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" + } + } + + override func setUpWithError() throws { + super.setUp() + 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, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func userLogin() { + let loginResponse = LoginSignupResponse() + let loginUserName = "hello10" + let loginPassword = "world" + + 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 { + _ = try User.login(username: loginUserName, password: loginPassword) + MockURLProtocol.removeAll() + } catch { + XCTFail("Should login") + } + } + + func testFetch() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + userLogin() + let config = Config() + + var configOnServer = config + configOnServer.welcomeMessage = "Hello" + let serverResponse = ConfigFetchResponse(params: configOnServer) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = config.fetchPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssertEqual(fetched.welcomeMessage, configOnServer.welcomeMessage) + + #if !os(Linux) + //Should be updated in Keychain + guard let keychainConfig: CurrentConfigContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentConfig) else { + XCTFail("Should get object from Keychain") + return + } + XCTAssertEqual(keychainConfig.currentConfig?.welcomeMessage, configOnServer.welcomeMessage) + #endif + + XCTAssertEqual(Config.current?.welcomeMessage, configOnServer.welcomeMessage) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testSave() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + userLogin() + var config = Config() + config.welcomeMessage = "Hello" + + let serverResponse = ConfigUpdateResponse(result: true) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = config.savePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { saved in + + XCTAssertTrue(saved) + + #if !os(Linux) + //Should be updated in Keychain + guard let keychainConfig: CurrentConfigContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentConfig) else { + XCTFail("Should get object from Keychain") + return + } + XCTAssertEqual(keychainConfig.currentConfig?.welcomeMessage, config.welcomeMessage) + #endif + + XCTAssertEqual(Config.current?.welcomeMessage, config.welcomeMessage) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseFileCombineTests.swift b/Tests/ParseSwiftTests/ParseFileCombineTests.swift new file mode 100644 index 000000000..4a8162d02 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseFileCombineTests.swift @@ -0,0 +1,296 @@ +// +// ParseFileCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseFileCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + let temporaryDirectory = "\(NSTemporaryDirectory())test/" + + struct FileUploadResponse: Codable { + let name: String + let url: URL + } + + override func setUpWithError() throws { + super.setUp() + 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, + testing: true) + guard let fileManager = ParseFileManager() else { + throw ParseError(code: .unknownError, message: "Should have initialized file manage") + } + try fileManager.createDirectoryIfNeeded(temporaryDirectory) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + 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 directory = URL(fileURLWithPath: temporaryDirectory, isDirectory: true) + let expectation1 = XCTestExpectation(description: "Delete files1") + fileManager.removeDirectoryContents(directory) { error in + guard let error = error else { + expectation1.fulfill() + return + } + XCTFail(error.localizedDescription) + expectation1.fulfill() + } + let directory2 = defaultDirectoryPath + .appendingPathComponent(ParseConstants.fileDownloadsDirectory, isDirectory: true) + let expectation2 = XCTestExpectation(description: "Delete files2") + fileManager.removeDirectoryContents(directory2) { _ in + expectation2.fulfill() + } + wait(for: [expectation1, expectation2], timeout: 20.0) + } + + func testFetch() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + // swiftlint:disable:next line_length + guard let parseFileURL = URL(string: "http://localhost:1337/1/files/applicationId/d3a37aed0672a024595b766f97133615_logo.svg") else { + XCTFail("Should create URL") + return + } + var parseFile = ParseFile(name: "d3a37aed0672a024595b766f97133615_logo.svg", cloudURL: parseFileURL) + parseFile.url = parseFileURL + + let response = FileUploadResponse(name: "d3a37aed0672a024595b766f97133615_logo.svg", + url: parseFileURL) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = parseFile.fetchPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssertEqual(fetched.name, response.name) + XCTAssertEqual(fetched.url, response.url) + XCTAssertNotNil(fetched.localURL) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testFetchFileProgress() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + // swiftlint:disable:next line_length + guard let parseFileURL = URL(string: "http://localhost:1337/1/files/applicationId/d3a37aed0672a024595b766f97133615_logo.svg") else { + XCTFail("Should create URL") + return + } + var parseFile = ParseFile(name: "d3a37aed0672a024595b766f97133615_logo.svg", cloudURL: parseFileURL) + parseFile.url = parseFileURL + + let response = FileUploadResponse(name: "d3a37aed0672a024595b766f97133615_logo.svg", + url: parseFileURL) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = parseFile.fetchPublisher(progress: { (_, _, totalDownloaded, totalExpected) in + let currentProgess = Double(totalDownloaded)/Double(totalExpected) * 100 + XCTAssertGreaterThan(currentProgess, -1) + }).sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssertEqual(fetched.name, response.name) + XCTAssertEqual(fetched.url, response.url) + XCTAssertNotNil(fetched.localURL) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testSave() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + guard let sampleData = "Hello World".data(using: .utf8) else { + throw ParseError(code: .unknownError, message: "Should have converted to data") + } + let parseFile = ParseFile(name: "sampleData.txt", data: sampleData) + + // swiftlint:disable:next line_length + guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else { + XCTFail("Should create URL") + return + } + let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = parseFile.savePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssertEqual(fetched.name, response.name) + XCTAssertEqual(fetched.url, response.url) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testSaveFileProgress() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + guard let sampleData = "Hello World".data(using: .utf8) else { + throw ParseError(code: .unknownError, message: "Should have converted to data") + } + let parseFile = ParseFile(name: "sampleData.txt", data: sampleData) + + // swiftlint:disable:next line_length + guard let url = URL(string: "http://localhost:1337/1/files/applicationId/89d74fcfa4faa5561799e5076593f67c_sampleData.txt") else { + XCTFail("Should create URL") + return + } + let response = FileUploadResponse(name: "89d74fcfa4faa5561799e5076593f67c_\(parseFile.name)", url: url) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = parseFile.savePublisher(progress: { (_, _, totalDownloaded, totalExpected) in + let currentProgess = Double(totalDownloaded)/Double(totalExpected) * 100 + XCTAssertGreaterThan(currentProgess, -1) + }).sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssertEqual(fetched.name, response.name) + XCTAssertEqual(fetched.url, response.url) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testDelete() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + // swiftlint:disable:next line_length + guard let parseFileURL = URL(string: "http://localhost:1337/1/files/applicationId/d3a37aed0672a024595b766f97133615_logo.svg") else { + XCTFail("Should create URL") + return + } + var parseFile = ParseFile(name: "d3a37aed0672a024595b766f97133615_logo.svg", cloudURL: parseFileURL) + parseFile.url = parseFileURL + + let response = FileUploadResponse(name: "d3a37aed0672a024595b766f97133615_logo.svg", + url: parseFileURL) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = parseFile.deletePublisher(options: [.useMasterKey]) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { _ in + + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseFileTests.swift b/Tests/ParseSwiftTests/ParseFileTests.swift index dfe60ed60..51d271894 100644 --- a/Tests/ParseSwiftTests/ParseFileTests.swift +++ b/Tests/ParseSwiftTests/ParseFileTests.swift @@ -1068,13 +1068,11 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length } let expectation1 = XCTestExpectation(description: "ParseFile async") - parseFile.delete(options: [.useMasterKey]) { error in + parseFile.delete(options: [.useMasterKey]) { result in - guard let error = error else { - expectation1.fulfill() - return + if case let .failure(error) = result { + XCTFail(error.localizedDescription) } - XCTFail(error.localizedDescription) expectation1.fulfill() } wait(for: [expectation1], timeout: 20.0) @@ -1104,12 +1102,10 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length } let expectation1 = XCTestExpectation(description: "ParseFile async") - parseFile.delete(options: [.removeMimeType]) { error in + parseFile.delete(options: [.removeMimeType]) { result in - guard error != nil else { + if case .success = result { XCTFail("Should have thrown error") - expectation1.fulfill() - return } expectation1.fulfill() } diff --git a/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift new file mode 100644 index 000000000..01655f78a --- /dev/null +++ b/Tests/ParseSwiftTests/ParseInstallationCombineTests.swift @@ -0,0 +1,548 @@ +// +// ParseInstallationCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseInstallationCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + 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 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() { + 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 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? + } + + let testInstallationObjectId = "yarr" + + let loginUserName = "hello10" + let loginPassword = "world" + + override func setUpWithError() throws { + super.setUp() + 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, + testing: true) + login() + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func login() { + 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 { + _ = try User.login(username: loginUserName, password: loginPassword) + + } catch { + XCTFail(error.localizedDescription) + } + } + + func update() { + var installation = Installation() + installation.objectId = testInstallationObjectId + 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 installationOnServer.getEncoder().encode(installationOnServer, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + installationOnServer = try installationOnServer.getDecoder().decode(Installation.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + let expectation1 = XCTestExpectation(description: "Update installation1") + DispatchQueue.main.async { + do { + let saved = try installation.save() + guard let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalUpdatedAt = installation.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertGreaterThan(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(saved.ACL) + } catch { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testFetch() { + update() + MockURLProtocol.removeAll() + + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Update installation1") + DispatchQueue.main.async { + guard let installation = Installation.current, + let savedObjectId = installation.objectId else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + XCTAssertEqual(savedObjectId, self.testInstallationObjectId) + + var serverResponse = installation + serverResponse.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + serverResponse.customKey = "newValue" + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let publisher = installation.fetchPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssert(fetched.hasSameObjectId(as: serverResponse)) + XCTAssertEqual(Installation.current?.customKey, serverResponse.customKey) + }) + publisher.store(in: &subscriptions) + } + wait(for: [expectation1], timeout: 20.0) + } + + func testSave() { + update() + MockURLProtocol.removeAll() + + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Update installation1") + DispatchQueue.main.async { + guard var installation = Installation.current, + let savedObjectId = installation.objectId else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + installation.customKey = "newValue" + XCTAssertEqual(savedObjectId, self.testInstallationObjectId) + + var serverResponse = installation + serverResponse.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let publisher = installation.savePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssert(fetched.hasSameObjectId(as: serverResponse)) + XCTAssertEqual(Installation.current?.customKey, serverResponse.customKey) + }) + publisher.store(in: &subscriptions) + } + wait(for: [expectation1], timeout: 20.0) + } + + func testDelete() { + update() + MockURLProtocol.removeAll() + + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Update installation1") + DispatchQueue.main.async { + guard let installation = Installation.current, + let savedObjectId = installation.objectId else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + XCTAssertEqual(savedObjectId, self.testInstallationObjectId) + + var serverResponse = installation + serverResponse.updatedAt = installation.updatedAt?.addingTimeInterval(+300) + serverResponse.customKey = "newValue" + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let publisher = installation.deletePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { _ in + + }) + publisher.store(in: &subscriptions) + } + wait(for: [expectation1], timeout: 20.0) + } + + func testFetchAll() { + update() + MockURLProtocol.removeAll() + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + 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 = QueryResponse(results: [installation], count: 1) + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().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) + } + + let publisher = [installation].fetchAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + 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) + + #if !os(Linux) + //Should 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) + #endif + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + }) + publisher.store(in: &subscriptions) + } + wait(for: [expectation1], timeout: 20.0) + } + + func testSaveAll() { + update() + MockURLProtocol.removeAll() + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + 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 ParseCoding.jsonEncoder().encode(installationOnServer) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().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) + } + + let publisher = [installation].saveAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { saved in + + 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) + + #if !os(Linux) + //Should 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) + #endif + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + }) + publisher.store(in: &subscriptions) + } + wait(for: [expectation1], timeout: 20.0) + } + + func testDeleteAll() { + update() + MockURLProtocol.removeAll() + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + DispatchQueue.main.async { + guard let installation = Installation.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + let installationOnServer = [BatchResponseItem(success: NoBody(), error: nil)] + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(installationOnServer) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = [installation].deleteAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { deleted in + deleted.forEach { + if case let .failure(error) = $0 { + XCTFail("Should have deleted: \(error.localizedDescription)") + } + } + }) + publisher.store(in: &subscriptions) + } + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseInstallationTests.swift b/Tests/ParseSwiftTests/ParseInstallationTests.swift index 53380d9e2..dbac1044e 100644 --- a/Tests/ParseSwiftTests/ParseInstallationTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationTests.swift @@ -690,8 +690,10 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } - installation.delete { error in - XCTAssertNil(error) + installation.delete { result in + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } expectation1.fulfill() } } @@ -1093,8 +1095,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l return } - let error: ParseError? = nil - let installationOnServer = [error] + let installationOnServer = [BatchResponseItem(success: NoBody(), error: nil)] let encoded: Data! do { @@ -1111,7 +1112,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l do { let deleted = try [installation].deleteAll() deleted.forEach { - if let error = $0 { + if case let .failure(error) = $0 { XCTFail("Should have deleted: \(error.localizedDescription)") } } @@ -1136,8 +1137,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l return } - let error: ParseError? = nil - let installationOnServer = [error] + let installationOnServer = [BatchResponseItem(success: NoBody(), error: nil)] let encoded: Data! do { @@ -1156,7 +1156,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l case .success(let deleted): deleted.forEach { - if let error = $0 { + if case let .failure(error) = $0 { XCTFail("Should have deleted: \(error.localizedDescription)") } } diff --git a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift index 2d38712a1..9de6683dd 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift @@ -10,7 +10,7 @@ import Foundation import XCTest @testable import ParseSwift -@available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) class ParseLiveQueryTests: XCTestCase { struct GameScore: ParseObject { //: Those are required for Object @@ -1426,674 +1426,5 @@ class ParseLiveQueryTests: XCTestCase { wait(for: [expectation1, expectation2], timeout: 20.0) } - - func testFind() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - subscription.error = ParseError(code: .objectNotFound, message: "Error") - subscription.count = 5 - subscription.resultsCodable = AnyCodable() - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - let results = QueryResponse(results: [scoreOnServer], count: 1) - MockURLProtocol.mockRequests { _ in - do { - let encoded = try ParseCoding.jsonEncoder().encode(results) - return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) - } catch { - return nil - } - } - - subscription.find(options: [], callbackQueue: .main) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let score = subscription.results?.first else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.error) - XCTAssertNil(subscription.count) - XCTAssert(score.hasSameObjectId(as: scoreOnServer)) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testFirst() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - subscription.error = ParseError(code: .objectNotFound, message: "Error") - subscription.count = 5 - subscription.resultsCodable = AnyCodable() - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - let results = QueryResponse(results: [scoreOnServer], count: 1) - MockURLProtocol.mockRequests { _ in - do { - let encoded = try ParseCoding.jsonEncoder().encode(results) - return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) - } catch { - return nil - } - } - - subscription.first(options: [], callbackQueue: .main) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let score = subscription.results?.first else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.error) - XCTAssertNil(subscription.count) - XCTAssert(score.hasSameObjectId(as: scoreOnServer)) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testCount() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.error = ParseError(code: .objectNotFound, message: "Error") - subscription.results = [scoreOnServer] - subscription.resultsCodable = AnyCodable() - - let results = QueryResponse(results: [scoreOnServer], count: 1) - MockURLProtocol.mockRequests { _ in - do { - let encoded = try ParseCoding.jsonEncoder().encode(results) - return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) - } catch { - return nil - } - } - - subscription.count(options: [], callbackQueue: .main) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let count = subscription.count else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.error) - XCTAssertNil(subscription.results) - XCTAssertEqual(count, 1) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testAggregate() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - subscription.error = ParseError(code: .objectNotFound, message: "Error") - subscription.count = 5 - subscription.resultsCodable = AnyCodable() - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - let results = QueryResponse(results: [scoreOnServer], count: 1) - MockURLProtocol.mockRequests { _ in - do { - let encoded = try ParseCoding.jsonEncoder().encode(results) - return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) - } catch { - return nil - } - } - - subscription.aggregate([["hello": "world"]], options: [], callbackQueue: .main) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let score = subscription.results?.first else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.error) - XCTAssertNil(subscription.count) - XCTAssert(score.hasSameObjectId(as: scoreOnServer)) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testFindExplain() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.error = ParseError(code: .objectNotFound, message: "Error") - subscription.results = [scoreOnServer] - subscription.count = 5 - - let json = AnyResultsResponse(results: ["yolo": "yarr"]) - - let encoded: Data! - do { - encoded = try JSONEncoder().encode(json) - } catch { - XCTFail("Should encode. Error \(error)") - return - } - MockURLProtocol.mockRequests { _ in - return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) - } - - subscription.find(explain: true) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let response = subscription.resultsCodable?.value as? [String: String], - let expected = json.results?.value as? [String: String] else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.error) - XCTAssertNil(subscription.results) - XCTAssertEqual(response, expected) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testFirstExplain() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.error = ParseError(code: .objectNotFound, message: "Error") - subscription.results = [scoreOnServer] - subscription.count = 5 - - let json = AnyResultsResponse(results: ["yolo": "yarr"]) - - let encoded: Data! - do { - encoded = try JSONEncoder().encode(json) - } catch { - XCTFail("Should encode. Error \(error)") - return - } - MockURLProtocol.mockRequests { _ in - return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) - } - - subscription.first(explain: true) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let response = subscription.resultsCodable?.value as? [String: String], - let expected = json.results?.value as? [String: String] else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.error) - XCTAssertNil(subscription.results) - XCTAssertEqual(response, expected) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testCountExplain() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.error = ParseError(code: .objectNotFound, message: "Error") - subscription.results = [scoreOnServer] - subscription.count = 5 - - let json = AnyResultsResponse(results: ["yolo": "yarr"]) - - let encoded: Data! - do { - encoded = try JSONEncoder().encode(json) - } catch { - XCTFail("Should encode. Error \(error)") - return - } - MockURLProtocol.mockRequests { _ in - return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) - } - - subscription.count(explain: true) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let response = subscription.resultsCodable?.value as? [String: String], - let expected = json.results?.value as? [String: String] else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.error) - XCTAssertNil(subscription.results) - XCTAssertEqual(response, expected) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testFindError() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.count = 5 - subscription.results = [scoreOnServer] - subscription.resultsCodable = AnyCodable() - - let serverError = ParseError(code: .invalidServerResponse, message: "Error") - MockURLProtocol.mockRequests { _ in - return MockURLResponse(error: serverError) - } - - subscription.find() - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let error = subscription.error else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.results) - XCTAssertEqual(error.code, serverError.code) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testFirstError() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.count = 5 - subscription.results = [scoreOnServer] - subscription.resultsCodable = AnyCodable() - - let serverError = ParseError(code: .invalidServerResponse, message: "Error") - MockURLProtocol.mockRequests { _ in - return MockURLResponse(error: serverError) - } - - subscription.first() - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let error = subscription.error else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.results) - XCTAssertEqual(error.code, serverError.code) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testCountError() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.count = 5 - subscription.results = [scoreOnServer] - subscription.resultsCodable = AnyCodable() - - let serverError = ParseError(code: .invalidServerResponse, message: "Error") - MockURLProtocol.mockRequests { _ in - return MockURLResponse(error: serverError) - } - - subscription.count() - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let error = subscription.error else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.results) - XCTAssertEqual(error.code, serverError.code) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testAggregateError() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.count = 5 - subscription.results = [scoreOnServer] - subscription.resultsCodable = AnyCodable() - - let serverError = ParseError(code: .invalidServerResponse, message: "Error") - MockURLProtocol.mockRequests { _ in - return MockURLResponse(error: serverError) - } - - subscription.aggregate([["hello": "world"]]) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let error = subscription.error else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.results) - XCTAssertEqual(error.code, serverError.code) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testFindExplainError() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.count = 5 - subscription.results = [scoreOnServer] - subscription.resultsCodable = AnyCodable() - - let serverError = ParseError(code: .invalidServerResponse, message: "Error") - MockURLProtocol.mockRequests { _ in - return MockURLResponse(error: serverError) - } - - subscription.find(explain: true) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let error = subscription.error else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.results) - XCTAssertEqual(error.code, serverError.code) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - - func testFirstExplainError() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.count = 5 - subscription.results = [scoreOnServer] - subscription.resultsCodable = AnyCodable() - - let serverError = ParseError(code: .invalidServerResponse, message: "Error") - MockURLProtocol.mockRequests { _ in - return MockURLResponse(error: serverError) - } - - subscription.first(explain: true) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let error = subscription.error else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.results) - XCTAssertEqual(error.code, serverError.code) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } - func testCountExplainError() throws { - let query = GameScore.query("score" > 9) - guard let subscription = query.subscribe else { - XCTFail("Should create subscription") - return - } - XCTAssertEqual(subscription.query, query) - - let expectation1 = XCTestExpectation(description: "Subscribe Handler") - - var scoreOnServer = GameScore(score: 10) - scoreOnServer.objectId = "yarr" - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() - scoreOnServer.ACL = nil - - subscription.count = 5 - subscription.results = [scoreOnServer] - subscription.resultsCodable = AnyCodable() - - let serverError = ParseError(code: .invalidServerResponse, message: "Error") - MockURLProtocol.mockRequests { _ in - return MockURLResponse(error: serverError) - } - - subscription.count(explain: true) - - DispatchQueue.main.asyncAfter(deadline: .now() + 1) { - - guard let error = subscription.error else { - XCTFail("Should unwrap subscribed.") - expectation1.fulfill() - return - } - XCTAssertNil(subscription.resultsCodable) - XCTAssertNil(subscription.count) - XCTAssertNil(subscription.results) - XCTAssertEqual(error.code, serverError.code) - expectation1.fulfill() - } - - wait(for: [expectation1], timeout: 20.0) - } } #endif diff --git a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift index 17fd0854f..15289aed8 100644 --- a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift @@ -1280,8 +1280,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } func testDeleteAll() { - let error: ParseError? = nil - let response = [error] + let response = [BatchResponseItem(success: NoBody(), error: nil), + BatchResponseItem(success: NoBody(), error: nil)] let encoded: Data! do { @@ -1295,18 +1295,26 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } do { - let fetched = try [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].deleteAll() + let deleted = try [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].deleteAll() - XCTAssertEqual(fetched.count, 1) - guard let firstObject = fetched.first else { + XCTAssertEqual(deleted.count, 2) + guard let firstObject = deleted.first else { XCTFail("Should unwrap") return } - if let error = firstObject { + if case let .failure(error) = firstObject { XCTFail(error.localizedDescription) } + guard let lastObject = deleted.last else { + XCTFail("Should unwrap") + return + } + + if case let .failure(error) = lastObject { + XCTFail(error.localizedDescription) + } } catch { XCTFail(error.localizedDescription) } @@ -1314,7 +1322,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le func testDeleteAllError() { let parseError = ParseError(code: .objectNotFound, message: "Object not found") - let response = [parseError] + let response = [BatchResponseItem(success: nil, error: parseError), + BatchResponseItem(success: nil, error: parseError)] let encoded: Data! do { encoded = try ParseCoding.jsonEncoder().encode(response) @@ -1327,15 +1336,26 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } do { - let fetched = try [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].deleteAll() + let deleted = try [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].deleteAll() + + XCTAssertEqual(deleted.count, 2) + guard let firstObject = deleted.first else { + XCTFail("Should have thrown ParseError") + return + } - XCTAssertEqual(fetched.count, 1) - guard let firstObject = fetched.first else { + if case let .failure(error) = firstObject { + XCTAssertEqual(error.code, parseError.code) + } else { + XCTFail("Should have thrown ParseError") + } + + guard let lastObject = deleted.last else { XCTFail("Should have thrown ParseError") return } - if let error = firstObject { + if case let .failure(error) = lastObject { XCTAssertEqual(error.code, parseError.code) } else { XCTFail("Should have thrown ParseError") @@ -1355,15 +1375,25 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le switch result { - case .success(let fetched): - XCTAssertEqual(fetched.count, 1) - guard let firstObject = fetched.first else { + case .success(let deleted): + XCTAssertEqual(deleted.count, 2) + guard let firstObject = deleted.first else { XCTFail("Should unwrap") expectation1.fulfill() return } - if let error = firstObject { + if case let .failure(error) = firstObject { + XCTFail(error.localizedDescription) + } + + guard let lastObject = deleted.last else { + XCTFail("Should unwrap") + expectation1.fulfill() + return + } + + if case let .failure(error) = lastObject { XCTFail(error.localizedDescription) } @@ -1377,8 +1407,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le } func testDeleteAllAsyncMainQueue() { - let error: ParseError? = nil - let response = [error] + let response = [BatchResponseItem(success: NoBody(), error: nil), + BatchResponseItem(success: NoBody(), error: nil)] do { let encoded = try ParseCoding.jsonEncoder().encode(response) @@ -1402,15 +1432,27 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le switch result { - case .success(let fetched): - XCTAssertEqual(fetched.count, 1) - guard let firstObject = fetched.first else { + case .success(let deleted): + XCTAssertEqual(deleted.count, 2) + guard let firstObject = deleted.first else { + XCTFail("Should have thrown ParseError") + expectation1.fulfill() + return + } + + if case let .failure(error) = firstObject { + XCTAssertEqual(error.code, parseError.code) + } else { + XCTFail("Should have thrown ParseError") + } + + guard let lastObject = deleted.last else { XCTFail("Should have thrown ParseError") expectation1.fulfill() return } - if let error = firstObject { + if case let .failure(error) = lastObject { XCTAssertEqual(error.code, parseError.code) } else { XCTFail("Should have thrown ParseError") @@ -1428,7 +1470,8 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le func testDeleteAllAsyncMainQueueError() { let parseError = ParseError(code: .objectNotFound, message: "Object not found") - let response = [parseError] + let response = [BatchResponseItem(success: nil, error: parseError), + BatchResponseItem(success: nil, error: parseError)] do { let encoded = try ParseCoding.jsonEncoder().encode(response) diff --git a/Tests/ParseSwiftTests/ParseObjectCombine.swift b/Tests/ParseSwiftTests/ParseObjectCombine.swift new file mode 100644 index 000000000..2d70b0666 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseObjectCombine.swift @@ -0,0 +1,467 @@ +// +// ParseObjectCombine.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseObjectCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + 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? + + //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 + } + } + + override func setUpWithError() throws { + super.setUp() + 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, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testFetch() { + var score = GameScore(score: 10) + let objectId = "yarr" + score.objectId = objectId + + var scoreOnServer = score + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + 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) + } + + let publisher = score.fetchPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssert(fetched.hasSameObjectId(as: scoreOnServer)) + guard let fetchedCreatedAt = fetched.createdAt, + let fetchedUpdatedAt = fetched.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer.createdAt, + let originalUpdatedAt = scoreOnServer.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) + XCTAssertEqual(fetchedUpdatedAt, originalUpdatedAt) + XCTAssertNil(fetched.ACL) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testSave() { + let score = GameScore(score: 10) + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + 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) + } + + let publisher = score.savePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { saved in + + XCTAssert(saved.hasSameObjectId(as: scoreOnServer)) + guard let savedCreatedAt = saved.createdAt, + let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer.createdAt, + let originalUpdatedAt = scoreOnServer.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(saved.ACL) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testDelete() { + var score = GameScore(score: 10) + score.objectId = "yarr" + + let scoreOnServer = NoBody() + + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(scoreOnServer) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = score.deletePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { _ in + + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testFetchAll() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + let score = GameScore(score: 10) + let score2 = GameScore(score: 20) + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + var scoreOnServer2 = score2 + scoreOnServer2.objectId = "yolo" + scoreOnServer2.createdAt = Calendar.current.date(byAdding: .init(day: -2), to: Date()) + scoreOnServer2.updatedAt = scoreOnServer2.createdAt + scoreOnServer2.ACL = nil + + let response = QueryResponse(results: [scoreOnServer, scoreOnServer2], count: 2) + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().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) + } + + let publisher = [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].fetchAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssertEqual(fetched.count, 2) + guard let firstObject = try? fetched.first(where: {try $0.get().objectId == "yarr"}), + let secondObject = try? fetched.first(where: {try $0.get().objectId == "yolo"}) else { + XCTFail("Should unwrap") + return + } + + switch firstObject { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: scoreOnServer)) + guard let fetchedCreatedAt = first.createdAt, + let fetchedUpdatedAt = first.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer.createdAt, + let originalUpdatedAt = scoreOnServer.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(fetchedCreatedAt, originalCreatedAt) + XCTAssertEqual(fetchedUpdatedAt, originalUpdatedAt) + XCTAssertNil(first.ACL) + XCTAssertEqual(first.score, scoreOnServer.score) + case .failure(let error): + XCTFail(error.localizedDescription) + } + + switch secondObject { + + case .success(let second): + XCTAssert(second.hasSameObjectId(as: scoreOnServer2)) + guard let savedCreatedAt = second.createdAt, + let savedUpdatedAt = second.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer2.createdAt, + let originalUpdatedAt = scoreOnServer2.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(second.ACL) + XCTAssertEqual(second.score, scoreOnServer2.score) + case .failure(let error): + XCTFail(error.localizedDescription) + } + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testSaveAll() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + let score = GameScore(score: 10) + let score2 = GameScore(score: 20) + + var scoreOnServer = score + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.ACL = nil + + var scoreOnServer2 = score2 + scoreOnServer2.objectId = "yolo" + 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 ParseCoding.jsonEncoder().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) + } + + let publisher = [score, score2].saveAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { saved in + + XCTAssertEqual(saved.count, 2) + switch saved[0] { + + case .success(let first): + XCTAssert(first.hasSameObjectId(as: scoreOnServer)) + guard let savedCreatedAt = first.createdAt, + let savedUpdatedAt = first.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer.createdAt, + let originalUpdatedAt = scoreOnServer.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: scoreOnServer2)) + guard let savedCreatedAt = second.createdAt, + let savedUpdatedAt = second.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = scoreOnServer2.createdAt, + let originalUpdatedAt = scoreOnServer2.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedCreatedAt, originalCreatedAt) + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(second.ACL) + case .failure(let error): + XCTFail(error.localizedDescription) + } + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testDeleteAll() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + let response = [BatchResponseItem(success: NoBody(), error: nil), + BatchResponseItem(success: NoBody(), error: nil)] + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(response) + } catch { + XCTFail("Should have encoded/decoded. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = [GameScore(objectId: "yarr"), GameScore(objectId: "yolo")].deleteAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { deleted in + XCTAssertEqual(deleted.count, 2) + guard let firstObject = deleted.first else { + XCTFail("Should unwrap") + return + } + + if case let .failure(error) = firstObject { + XCTFail(error.localizedDescription) + } + + guard let lastObject = deleted.last else { + XCTFail("Should unwrap") + return + } + + if case let .failure(error) = lastObject { + XCTFail(error.localizedDescription) + } + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseObjectTests.swift b/Tests/ParseSwiftTests/ParseObjectTests.swift index 5062e7e77..5665a9e21 100644 --- a/Tests/ParseSwiftTests/ParseObjectTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectTests.swift @@ -247,7 +247,7 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length var scoreOnServer = score scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = Date() + scoreOnServer.updatedAt = scoreOnServer.createdAt scoreOnServer.ACL = nil let encoded: Data! do { @@ -947,24 +947,20 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length func deleteAsync(score: GameScore, scoreOnServer: GameScore, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Delete object1") - score.delete(options: [], callbackQueue: callbackQueue) { error in + score.delete(options: [], callbackQueue: callbackQueue) { result in - guard let error = error else { - expectation1.fulfill() - return + if case let .failure(error) = result { + XCTFail(error.localizedDescription) } - XCTFail(error.localizedDescription) expectation1.fulfill() } let expectation2 = XCTestExpectation(description: "Delete object2") - score.delete(options: [.useMasterKey], callbackQueue: callbackQueue) { error in + score.delete(options: [.useMasterKey], callbackQueue: callbackQueue) { result in - guard let error = error else { - expectation2.fulfill() - return + if case let .failure(error) = result { + XCTFail(error.localizedDescription) } - XCTFail(error.localizedDescription) expectation2.fulfill() } wait(for: [expectation1, expectation2], timeout: 20.0) @@ -1027,26 +1023,24 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length func deleteAsyncError(score: GameScore, parseError: ParseError, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Delete object1") - score.delete(options: [], callbackQueue: callbackQueue) { error in + score.delete(options: [], callbackQueue: callbackQueue) { result in - guard let error = error else { + if case let .failure(error) = result { + XCTAssertEqual(error.code, parseError.code) + } else { XCTFail("Should have thrown ParseError") - expectation1.fulfill() - return } - XCTAssertEqual(error.code, parseError.code) expectation1.fulfill() } let expectation2 = XCTestExpectation(description: "Delete object2") - score.delete(options: [.useMasterKey], callbackQueue: callbackQueue) { error in + score.delete(options: [.useMasterKey], callbackQueue: callbackQueue) { result in - guard let error = error else { + if case let .failure(error) = result { + XCTAssertEqual(error.code, parseError.code) + } else { XCTFail("Should have thrown ParseError") - expectation2.fulfill() - return } - XCTAssertEqual(error.code, parseError.code) expectation2.fulfill() } wait(for: [expectation1, expectation2], timeout: 20.0) diff --git a/Tests/ParseSwiftTests/ParseOperationCombineTests.swift b/Tests/ParseSwiftTests/ParseOperationCombineTests.swift new file mode 100644 index 000000000..8067b6437 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseOperationCombineTests.swift @@ -0,0 +1,122 @@ +// +// ParseOperationCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseOperationCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + 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? + + //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 + } + } + + override func setUpWithError() throws { + super.setUp() + 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, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testSave() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + var score = GameScore(score: 10) + score.objectId = "yarr" + let operations = score.operation + .increment("score", by: 1) + + var scoreOnServer = score + scoreOnServer.score = 11 + scoreOnServer.updatedAt = Date() + 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 encode/decode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = operations.savePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { saved in + + XCTAssert(saved.hasSameObjectId(as: scoreOnServer)) + guard let savedUpdatedAt = saved.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalUpdatedAt = scoreOnServer.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(savedUpdatedAt, originalUpdatedAt) + XCTAssertNil(saved.ACL) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseOperationTests.swift b/Tests/ParseSwiftTests/ParseOperationTests.swift index b8ae8abde..f784d9305 100644 --- a/Tests/ParseSwiftTests/ParseOperationTests.swift +++ b/Tests/ParseSwiftTests/ParseOperationTests.swift @@ -103,8 +103,7 @@ class ParseOperationTests: XCTestCase { var scoreOnServer = score scoreOnServer.score = 11 - scoreOnServer.createdAt = Date() - scoreOnServer.updatedAt = scoreOnServer.createdAt + scoreOnServer.updatedAt = Date() let encoded: Data! do { diff --git a/Tests/ParseSwiftTests/ParseQueryCombineTests.swift b/Tests/ParseSwiftTests/ParseQueryCombineTests.swift new file mode 100644 index 000000000..f176fc76a --- /dev/null +++ b/Tests/ParseSwiftTests/ParseQueryCombineTests.swift @@ -0,0 +1,360 @@ +// +// ParseQueryCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/30/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseQueryCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + 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? + + //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 + } + } + + override func setUpWithError() throws { + super.setUp() + 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, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testFind() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + var scoreOnServer = GameScore(score: 10) + scoreOnServer.score = 11 + scoreOnServer.objectId = "yolo" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = Date() + scoreOnServer.ACL = nil + + let results = QueryResponse(results: [scoreOnServer], count: 1) + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(results) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let query = GameScore.query() + + let publisher = query.findPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { found in + + guard let object = found.first else { + XCTFail("Should have unwrapped") + return + } + XCTAssert(object.hasSameObjectId(as: scoreOnServer)) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testFindExplain() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + let json = AnyResultsResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query() + + let publisher = query.findPublisher(explain: true) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { queryResult in + + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { + XCTFail("Error: Should cast to string") + return + } + XCTAssertEqual(response, expected) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testFirst() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + var scoreOnServer = GameScore(score: 10) + scoreOnServer.score = 11 + scoreOnServer.objectId = "yolo" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = Date() + scoreOnServer.ACL = nil + + let results = QueryResponse(results: [scoreOnServer], count: 1) + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(results) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let query = GameScore.query() + + let publisher = query.firstPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { found in + + XCTAssert(found.hasSameObjectId(as: scoreOnServer)) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testFirstExplain() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + let json = AnyResultsResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query() + + let publisher = query.firstPublisher(explain: true) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { queryResult in + + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { + XCTFail("Error: Should cast to string") + return + } + XCTAssertEqual(response, expected) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testCount() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + var scoreOnServer = GameScore(score: 10) + scoreOnServer.score = 11 + scoreOnServer.objectId = "yolo" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = Date() + scoreOnServer.ACL = nil + + let results = QueryResponse(results: [scoreOnServer], count: 1) + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(results) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let query = GameScore.query() + + let publisher = query.countPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { found in + + XCTAssertEqual(found, 1) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testCountExplain() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + let json = AnyResultsResponse(results: ["yolo": "yarr"]) + + let encoded: Data! + do { + encoded = try JSONEncoder().encode(json) + } catch { + XCTFail("Should encode. Error \(error)") + return + } + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let query = GameScore.query() + + let publisher = query.countPublisher(explain: true) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { queryResult in + + guard let response = queryResult.value as? [String: String], + let expected = json.results?.value as? [String: String] else { + XCTFail("Error: Should cast to string") + return + } + XCTAssertEqual(response, expected) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testAggregate() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + var scoreOnServer = GameScore(score: 10) + scoreOnServer.objectId = "yarr" + scoreOnServer.createdAt = Date() + scoreOnServer.updatedAt = Date() + scoreOnServer.ACL = nil + + let results = QueryResponse(results: [scoreOnServer], count: 1) + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(results) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let query = GameScore.query() + let pipeline = [[String: String]]() + let publisher = query.aggregatePublisher(pipeline) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { found in + + guard let object = found.first else { + XCTFail("Should have unwrapped") + return + } + XCTAssert(object.hasSameObjectId(as: scoreOnServer)) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseUserCombineTests.swift b/Tests/ParseSwiftTests/ParseUserCombineTests.swift new file mode 100644 index 000000000..b505d4207 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseUserCombineTests.swift @@ -0,0 +1,861 @@ +// +// ParseUserCombineTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/29/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + 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 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() { + 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" + } + } + + let loginUserName = "hello10" + let loginPassword = "world" + + override func setUpWithError() throws { + super.setUp() + 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, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testSignup() { + let loginResponse = LoginSignupResponse() + var subscriptions = Set() + 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 + } + } + + let expectation1 = XCTestExpectation(description: "Signup user1") + let publisher = User.signupPublisher(username: loginUserName, password: loginUserName) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { signedUp in + 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) + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testLogin() { + let loginResponse = LoginSignupResponse() + var subscriptions = Set() + 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 + } + } + + let expectation1 = XCTestExpectation(description: "Login user1") + let publisher = User.loginPublisher(username: loginUserName, password: loginUserName) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { signedUp in + 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) + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func login() { + 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 { + _ = try User.login(username: loginUserName, password: loginPassword) + + } catch { + XCTFail(error.localizedDescription) + } + } + + func testBecome() { + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + guard let user = User.current else { + XCTFail("Should unwrap") + return + } + + var serverResponse = LoginSignupResponse() + serverResponse.createdAt = User.current?.createdAt + serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) + serverResponse.sessionToken = "newValue" + serverResponse.username = "stop" + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Become user1") + let publisher = user.becomePublisher(sessionToken: serverResponse.sessionToken) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { signedUp in + 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) + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testLogout() { + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + let serverResponse = NoBody() + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Logout user1") + let publisher = User.logoutPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { _ in + if let userFromKeychain = BaseParseUser.current { + XCTFail("\(userFromKeychain) wasn't deleted from Keychain during logout") + } + + if let installationFromMemory: CurrentInstallationContainer + = try? ParseStorage.shared.get(valueFor: ParseStorage.Keys.currentInstallation) { + XCTFail("\(installationFromMemory) wasn't deleted from memory during logout") + } + + #if !os(Linux) + if let installationFromKeychain: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) { + XCTFail("\(installationFromKeychain) wasn't deleted from Keychain during logout") + } + #endif + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testLogoutError() { + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + let serverResponse = ParseError(code: .internalServer, message: "Object not found") + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Logout user1") + let publisher = User.logoutPublisher() + .sink(receiveCompletion: { result in + + if case .finished = result { + XCTFail("Should have thrown ParseError") + } + + if let userFromKeychain = BaseParseUser.current { + XCTFail("\(userFromKeychain) wasn't deleted from Keychain during logout") + } + + if let installationFromMemory: CurrentInstallationContainer + = try? ParseStorage.shared.get(valueFor: ParseStorage.Keys.currentInstallation) { + XCTFail("\(installationFromMemory) wasn't deleted from memory during logout") + } + + #if !os(Linux) + if let installationFromKeychain: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) { + XCTFail("\(installationFromKeychain) wasn't deleted from Keychain during logout") + } + #endif + expectation1.fulfill() + + }, receiveValue: { _ in + XCTFail("Should have thrown ParseError") + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testPasswordReset() { + let serverResponse = NoBody() + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Password user1") + let publisher = User.passwordResetPublisher(email: "hello@parse.org") + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { _ in + + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testPasswordResetError() { + let parseError = ParseError(code: .internalServer, message: "Object not found") + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(parseError) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Password user1") + let publisher = User.passwordResetPublisher(email: "hello@parse.org") + .sink(receiveCompletion: { result in + + if case .finished = result { + XCTFail("Should have thrown ParseError") + } + expectation1.fulfill() + + }, receiveValue: { _ in + XCTFail("Should have thrown ParseError") + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testVerificationEmail() { + let serverResponse = NoBody() + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Verification user1") + let publisher = User.verificationEmailPublisher(email: "hello@parse.org") + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { _ in + + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testVerificationEmailError() { + let parseError = ParseError(code: .internalServer, message: "Object not found") + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(parseError) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Verification user1") + let publisher = User.verificationEmailPublisher(email: "hello@parse.org") + .sink(receiveCompletion: { result in + + if case .finished = result { + XCTFail("Should have thrown ParseError") + } + expectation1.fulfill() + + }, receiveValue: { _ in + XCTFail("Should have thrown ParseError") + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testFetch() { + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + guard let user = User.current else { + XCTFail("Should unwrap") + return + } + + var serverResponse = LoginSignupResponse() + serverResponse.createdAt = User.current?.createdAt + serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) + serverResponse.sessionToken = "newValue" + serverResponse.username = "stop" + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Become user1") + let publisher = user.fetchPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + XCTAssertEqual(fetched.objectId, serverResponse.objectId) + + guard let userFromKeychain = BaseParseUser.current else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + + XCTAssertEqual(userFromKeychain.objectId, serverResponse.objectId) + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testSave() { + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + guard let user = User.current else { + XCTFail("Should unwrap") + return + } + + var serverResponse = LoginSignupResponse() + serverResponse.createdAt = User.current?.createdAt + serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) + serverResponse.sessionToken = "newValue" + serverResponse.username = "stop" + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Become user1") + let publisher = user.savePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { saved in + + XCTAssertEqual(saved.objectId, serverResponse.objectId) + + guard let userFromKeychain = BaseParseUser.current else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + + XCTAssertEqual(userFromKeychain.objectId, serverResponse.objectId) + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testDelete() { + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + guard let user = User.current else { + XCTFail("Should unwrap") + return + } + + let serverResponse = NoBody() + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(serverResponse) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Become user1") + let publisher = user.deletePublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { _ in + + if BaseParseUser.current != nil { + XCTFail("Couldn't get CurrentUser from Keychain") + } + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func testFetchAll() { + login() + MockURLProtocol.removeAll() + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Fetch") + + guard var user = User.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + user.updatedAt = user.updatedAt?.addingTimeInterval(+300) + user.customKey = "newValue" + let userOnServer = QueryResponse(results: [user], count: 1) + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(userOnServer) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().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) + } + + let publisher = [user].fetchAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { fetched in + + 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) + + #if !os(Linux) + //Should 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) + #endif + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testSaveAll() { + login() + MockURLProtocol.removeAll() + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + 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 ParseCoding.jsonEncoder().encode(userOnServer) + //Get dates in correct format from ParseDecoding strategy + let encoded1 = try ParseCoding.jsonEncoder().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) + } + + let publisher = [user].saveAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { saved in + + 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) + + #if !os(Linux) + //Should 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) + #endif + case .failure(let error): + XCTFail("Should have fetched: \(error.localizedDescription)") + } + } + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testDeleteAll() { + login() + MockURLProtocol.removeAll() + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + guard let user = User.current else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + + let userOnServer = [BatchResponseItem(success: NoBody(), error: nil)] + + let encoded: Data! + do { + encoded = try ParseCoding.jsonEncoder().encode(userOnServer) + } catch { + XCTFail("Should encode/decode. Error \(error)") + expectation1.fulfill() + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = [user].deleteAllPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { deleted in + deleted.forEach { + if case let .failure(error) = $0 { + XCTFail("Should have deleted: \(error.localizedDescription)") + } + } + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index e7c690df0..230bf52b7 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -75,13 +75,13 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length testing: true) } - override func tearDown() { + override func tearDownWithError() throws { super.tearDown() MockURLProtocol.removeAll() #if !os(Linux) - try? KeychainStore.shared.deleteAll() + try KeychainStore.shared.deleteAll() #endif - try? ParseStorage.shared.deleteAll() + try ParseStorage.shared.deleteAll() } func testFetchCommand() { @@ -972,20 +972,30 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func logoutAsync(callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Logout user1") - User.logout(callbackQueue: callbackQueue) { error in + User.logout(callbackQueue: callbackQueue) { result in - guard let error = error else { + switch result { + + case .success: if let userFromKeychain = BaseParseUser.current { XCTFail("\(userFromKeychain) wasn't deleted from Keychain during logout") } - if let installationFromKeychain = BaseParseInstallation.current { + if let installationFromMemory: CurrentInstallationContainer + = try? ParseStorage.shared.get(valueFor: ParseStorage.Keys.currentInstallation) { + XCTFail("\(installationFromMemory) wasn't deleted from memory during logout") + } + + #if !os(Linux) + if let installationFromKeychain: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) { XCTFail("\(installationFromKeychain) wasn't deleted from Keychain during logout") } - expectation1.fulfill() - return + #endif + + case .failure(let error): + XCTFail(error.localizedDescription) } - XCTFail(error.localizedDescription) expectation1.fulfill() } wait(for: [expectation1], timeout: 20.0) @@ -1067,13 +1077,11 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func passwordResetAsync(callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Logout user1") - User.passwordReset(email: "hello@parse.org", callbackQueue: callbackQueue) { error in + User.passwordReset(email: "hello@parse.org", callbackQueue: callbackQueue) { result in - guard let error = error else { - expectation1.fulfill() - return + if case let .failure(error) = result { + XCTFail(error.localizedDescription) } - XCTFail(error.localizedDescription) expectation1.fulfill() } wait(for: [expectation1], timeout: 10.0) @@ -1097,14 +1105,13 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func passwordResetAsyncError(parseError: ParseError, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Logout user1") - User.passwordReset(email: "hello@parse.org", callbackQueue: callbackQueue) { error in + User.passwordReset(email: "hello@parse.org", callbackQueue: callbackQueue) { result in - guard let error = error else { + if case let .failure(error) = result { + XCTAssertEqual(error.code, parseError.code) + } else { XCTFail("Should have thrown ParseError") - expectation1.fulfill() - return } - XCTAssertEqual(error.code, parseError.code) expectation1.fulfill() } wait(for: [expectation1], timeout: 10.0) @@ -1127,7 +1134,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func testVerificationEmailRequestCommand() throws { let body = EmailBody(email: "hello@parse.org") - let command = User.verificationEmailRequestCommand(email: body.email) + let command = User.verificationEmailCommand(email: body.email) XCTAssertNotNil(command) XCTAssertEqual(command.path.urlComponent, "/verificationEmailRequest") XCTAssertEqual(command.method, API.Method.POST) @@ -1147,7 +1154,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } } do { - try User.verificationEmailRequest(email: "hello@parse.org") + try User.verificationEmail(email: "hello@parse.org") } catch { XCTFail(error.localizedDescription) } @@ -1169,7 +1176,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } do { - try User.verificationEmailRequest(email: "hello@parse.org") + try User.verificationEmail(email: "hello@parse.org") XCTFail("Should have thrown ParseError") } catch { if let error = error as? ParseError { @@ -1180,16 +1187,14 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } } - func verificationEmailRequestAsync(callbackQueue: DispatchQueue) { + func verificationEmailAsync(callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Logout user1") - User.verificationEmailRequest(email: "hello@parse.org", callbackQueue: callbackQueue) { error in + User.verificationEmail(email: "hello@parse.org", callbackQueue: callbackQueue) { result in - guard let error = error else { - expectation1.fulfill() - return + if case let .failure(error) = result { + XCTFail(error.localizedDescription) } - XCTFail(error.localizedDescription) expectation1.fulfill() } wait(for: [expectation1], timeout: 10.0) @@ -1207,20 +1212,19 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } } - self.verificationEmailRequestAsync(callbackQueue: .main) + self.verificationEmailAsync(callbackQueue: .main) } - func verificationEmailRequestAsyncError(parseError: ParseError, callbackQueue: DispatchQueue) { + func verificationEmailAsyncError(parseError: ParseError, callbackQueue: DispatchQueue) { let expectation1 = XCTestExpectation(description: "Logout user1") - User.verificationEmailRequest(email: "hello@parse.org", callbackQueue: callbackQueue) { error in + User.verificationEmail(email: "hello@parse.org", callbackQueue: callbackQueue) { result in - guard let error = error else { + if case let .failure(error) = result { + XCTAssertEqual(error.code, parseError.code) + } else { XCTFail("Should have thrown ParseError") - expectation1.fulfill() - return } - XCTAssertEqual(error.code, parseError.code) expectation1.fulfill() } wait(for: [expectation1], timeout: 10.0) @@ -1238,7 +1242,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } } - self.verificationEmailRequestAsyncError(parseError: parseError, callbackQueue: .main) + self.verificationEmailAsyncError(parseError: parseError, callbackQueue: .main) } func testUserCustomValuesNotSavedToKeychain() { @@ -1330,8 +1334,10 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) } - user.delete { error in - XCTAssertNil(error) + user.delete { result in + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } expectation1.fulfill() } } @@ -1709,8 +1715,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length return } - let error: ParseError? = nil - let userOnServer = [error] + let userOnServer = [BatchResponseItem(success: NoBody(), error: nil)] let encoded: Data! do { @@ -1727,7 +1732,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length do { let deleted = try [user].deleteAll() deleted.forEach { - if let error = $0 { + if case let .failure(error) = $0 { XCTFail("Should have deleted: \(error.localizedDescription)") } } @@ -1752,8 +1757,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length return } - let error: ParseError? = nil - let userOnServer = [error] + let userOnServer = [BatchResponseItem(success: NoBody(), error: nil)] let encoded: Data! do { @@ -1772,7 +1776,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length case .success(let deleted): deleted.forEach { - if let error = $0 { + if case let .failure(error) = $0 { XCTFail("Should have deleted: \(error.localizedDescription)") } } @@ -1816,7 +1820,6 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) serverResponse.sessionToken = "newValue" serverResponse.username = "stop" - serverResponse.password = "this" var userOnServer: User!