diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 51a5d2c87..49373b802 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,6 +6,7 @@ on: branches: '*' env: CI_XCODE_VER: '/Applications/Xcode_11.7.app/Contents/Developer' + CI_XCODE_VER_12: '/Applications/Xcode_12.2.app/Contents/Developer' jobs: xcode-test-ios: @@ -104,8 +105,12 @@ jobs: run: | bundle config path vendor/bundle bundle install + env: + DEVELOPER_DIR: ${{ env.CI_XCODE_VER_12 }} - name: Create Jazzy Docs run: ./Scripts/jazzy.sh + env: + DEVELOPER_DIR: ${{ env.CI_XCODE_VER_12 }} - name: Deploy Jazzy Docs if: github.ref == 'refs/heads/main' uses: peaceiris/actions-gh-pages@v3 diff --git a/Gemfile.lock b/Gemfile.lock index 70bd9347a..fcdac71b9 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -56,7 +56,7 @@ GEM escape (0.0.4) ethon (0.12.0) ffi (>= 1.3.0) - ffi (1.13.1) + ffi (1.14.2) fourflusher (2.3.1) fuzzy_match (2.0.4) gh_inspector (1.1.3) diff --git a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift index e37676353..80e845a44 100644 --- a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift @@ -18,6 +18,7 @@ struct User: ParseUser { var username: String? var email: String? var password: String? + var authData: [String: [String: String]?]? //: Your custom keys var customKey: String? @@ -76,6 +77,30 @@ do { print("Error logging out: \(error)") } +//: Logging in anonymously +User.anonymous.login { result in + switch result { + case .success: + print("Successfully logged in \(User.current)") + case .failure(let error): + print("Error logging in: \(error)") + } +} + +//: Convert the anonymous user to a real new user. +User.current?.username = "bye" +User.current?.password = "world" +User.current?.signup { result in + switch result { + + case .success(let user): + print("Parse signup successful: \(user)") + + case .failure(let error): + print("Error logging in: \(error)") + } +} + //: Password Reset Request - synchronously do { try User.verificationEmailRequest(email: "hello@parse.org") @@ -92,22 +117,6 @@ do { print("Error requesting password reset: \(error)") } -//: Another way to sign up -var newUser = User() -newUser.username = "hello10" -newUser.password = "world" - -newUser.signup { result in - switch result { - - case .success(let user): - print("Parse signup successful: \(user)") - - case .failure(let error): - print("Error logging in: \(error)") - } -} - PlaygroundPage.current.finishExecution() //: [Next](@next) diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index fa6f13cd0..6786675cd 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -106,6 +106,18 @@ 70647E9D259E3A9A004C1004 /* ParseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E9B259E3A9A004C1004 /* ParseType.swift */; }; 70647E9E259E3A9A004C1004 /* ParseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E9B259E3A9A004C1004 /* ParseType.swift */; }; 70647E9F259E3A9A004C1004 /* ParseType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70647E9B259E3A9A004C1004 /* ParseType.swift */; }; + 707A3BF125B0A4F0000D215C /* ParseAuthenticatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3BF025B0A4F0000D215C /* ParseAuthenticatable.swift */; }; + 707A3BF225B0A4F0000D215C /* ParseAuthenticatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3BF025B0A4F0000D215C /* ParseAuthenticatable.swift */; }; + 707A3BF325B0A4F0000D215C /* ParseAuthenticatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3BF025B0A4F0000D215C /* ParseAuthenticatable.swift */; }; + 707A3BF425B0A4F0000D215C /* ParseAuthenticatable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3BF025B0A4F0000D215C /* ParseAuthenticatable.swift */; }; + 707A3C1125B0A8E8000D215C /* ParseAnonymous.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3C1025B0A8E8000D215C /* ParseAnonymous.swift */; }; + 707A3C1225B0A8E8000D215C /* ParseAnonymous.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3C1025B0A8E8000D215C /* ParseAnonymous.swift */; }; + 707A3C1325B0A8E8000D215C /* ParseAnonymous.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3C1025B0A8E8000D215C /* ParseAnonymous.swift */; }; + 707A3C1425B0A8E8000D215C /* ParseAnonymous.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3C1025B0A8E8000D215C /* ParseAnonymous.swift */; }; + 707A3C2025B14BD0000D215C /* ParseApple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3C1F25B14BCF000D215C /* ParseApple.swift */; }; + 707A3C2125B14BD0000D215C /* ParseApple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3C1F25B14BCF000D215C /* ParseApple.swift */; }; + 707A3C2225B14BD0000D215C /* ParseApple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3C1F25B14BCF000D215C /* ParseApple.swift */; }; + 707A3C2325B14BD0000D215C /* ParseApple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3C1F25B14BCF000D215C /* ParseApple.swift */; }; 708D035225215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; }; 708D035325215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; }; 708D035425215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; }; @@ -127,6 +139,12 @@ 709B98582556ECAA00507778 /* AnyEncodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FFF552B2217E729007C3B4E /* AnyEncodableTests.swift */; }; 709B98592556ECAA00507778 /* MockURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911DB12B24C3F7720027F3C7 /* MockURLResponse.swift */; }; 709B985A2556ECAA00507778 /* ParseObjectBatchTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C7DC2024D20F190050419B /* ParseObjectBatchTests.swift */; }; + 70A2D81F25B36A7D001BEB7D /* ParseAuthenticationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A2D81E25B36A7D001BEB7D /* ParseAuthenticationTests.swift */; }; + 70A2D82025B36A7D001BEB7D /* ParseAuthenticationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A2D81E25B36A7D001BEB7D /* ParseAuthenticationTests.swift */; }; + 70A2D82125B36A7D001BEB7D /* ParseAuthenticationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A2D81E25B36A7D001BEB7D /* ParseAuthenticationTests.swift */; }; + 70A2D86B25B3ADB6001BEB7D /* ParseAnonymousTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A2D86A25B3ADB6001BEB7D /* ParseAnonymousTests.swift */; }; + 70A2D86C25B3ADB6001BEB7D /* ParseAnonymousTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A2D86A25B3ADB6001BEB7D /* ParseAnonymousTests.swift */; }; + 70A2D86D25B3ADB6001BEB7D /* ParseAnonymousTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70A2D86A25B3ADB6001BEB7D /* ParseAnonymousTests.swift */; }; 70BC0B33251903D1001556DB /* ParseGeoPointTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70BC0B32251903D1001556DB /* ParseGeoPointTests.swift */; }; 70BC9890252A5B5C00FF3074 /* Objectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70BC988F252A5B5C00FF3074 /* Objectable.swift */; }; 70BC9891252A5B5C00FF3074 /* Objectable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70BC988F252A5B5C00FF3074 /* Objectable.swift */; }; @@ -136,6 +154,16 @@ 70BDA2B4250536FF00FC2237 /* ParseInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70BDA2B2250536FF00FC2237 /* ParseInstallation.swift */; }; 70BDA2B5250536FF00FC2237 /* ParseInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70BDA2B2250536FF00FC2237 /* ParseInstallation.swift */; }; 70BDA2B6250536FF00FC2237 /* ParseInstallation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70BDA2B2250536FF00FC2237 /* ParseInstallation.swift */; }; + 70C5502225B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5502125B3D8F700B5DBC2 /* ParseAppleTests.swift */; }; + 70C5502325B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5502125B3D8F700B5DBC2 /* ParseAppleTests.swift */; }; + 70C5502425B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5502125B3D8F700B5DBC2 /* ParseAppleTests.swift */; }; + 70C5503825B406B800B5DBC2 /* ParseSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5503725B406B800B5DBC2 /* ParseSession.swift */; }; + 70C5503925B406B800B5DBC2 /* ParseSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5503725B406B800B5DBC2 /* ParseSession.swift */; }; + 70C5503A25B406B800B5DBC2 /* ParseSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5503725B406B800B5DBC2 /* ParseSession.swift */; }; + 70C5503B25B406B800B5DBC2 /* ParseSession.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5503725B406B800B5DBC2 /* ParseSession.swift */; }; + 70C5504625B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5504525B40D5200B5DBC2 /* ParseSessionTests.swift */; }; + 70C5504725B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5504525B40D5200B5DBC2 /* ParseSessionTests.swift */; }; + 70C5504825B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5504525B40D5200B5DBC2 /* ParseSessionTests.swift */; }; 70C5655925AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */; }; 70C5655A25AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */; }; 70C5655B25AA147B00BDD57F /* ParseLiveQueryConstants.swift in Sources */ = {isa = PBXBuildFile; fileRef = 70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */; }; @@ -405,12 +433,20 @@ 705A9A2E25991C1400B3547F /* Fileable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Fileable.swift; sourceTree = ""; }; 70647E8D259E3375004C1004 /* LocallyIdentifiable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocallyIdentifiable.swift; sourceTree = ""; }; 70647E9B259E3A9A004C1004 /* ParseType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseType.swift; sourceTree = ""; }; + 707A3BF025B0A4F0000D215C /* ParseAuthenticatable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAuthenticatable.swift; sourceTree = ""; }; + 707A3C1025B0A8E8000D215C /* ParseAnonymous.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAnonymous.swift; sourceTree = ""; }; + 707A3C1F25B14BCF000D215C /* ParseApple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseApple.swift; sourceTree = ""; }; 708D035125215F9B00646C70 /* Deletable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deletable.swift; sourceTree = ""; }; 709B98302556EC7400507778 /* ParseSwiftTeststvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ParseSwiftTeststvOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 709B98342556EC7400507778 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + 70A2D81E25B36A7D001BEB7D /* ParseAuthenticationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAuthenticationTests.swift; sourceTree = ""; }; + 70A2D86A25B3ADB6001BEB7D /* ParseAnonymousTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAnonymousTests.swift; sourceTree = ""; }; 70BC0B32251903D1001556DB /* ParseGeoPointTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseGeoPointTests.swift; sourceTree = ""; }; 70BC988F252A5B5C00FF3074 /* Objectable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Objectable.swift; sourceTree = ""; }; 70BDA2B2250536FF00FC2237 /* ParseInstallation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseInstallation.swift; sourceTree = ""; }; + 70C5502125B3D8F700B5DBC2 /* ParseAppleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAppleTests.swift; sourceTree = ""; }; + 70C5503725B406B800B5DBC2 /* ParseSession.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseSession.swift; sourceTree = ""; }; + 70C5504525B40D5200B5DBC2 /* ParseSessionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseSessionTests.swift; sourceTree = ""; }; 70C5655825AA147B00BDD57F /* ParseLiveQueryConstants.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseLiveQueryConstants.swift; sourceTree = ""; }; 70C7DC1D24D20E530050419B /* ParseUserTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseUserTests.swift; sourceTree = ""; }; 70C7DC1F24D20F180050419B /* ParseQueryTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ParseQueryTests.swift; sourceTree = ""; }; @@ -589,6 +625,9 @@ 7003957525A0EE770052CB31 /* BatchUtilsTests.swift */, 705726ED2592C91C00F0ADD5 /* HashTests.swift */, 4AA8076E1F794C1C008CD551 /* KeychainStoreTests.swift */, + 70A2D86A25B3ADB6001BEB7D /* ParseAnonymousTests.swift */, + 70C5502125B3D8F700B5DBC2 /* ParseAppleTests.swift */, + 70A2D81E25B36A7D001BEB7D /* ParseAuthenticationTests.swift */, 916786EF259BC59600BB5B4E /* ParseCloudTests.swift */, F971F4F524DE381A006CB79B /* ParseEncoderTests.swift */, 705A99F8259807F900B3547F /* ParseFileManagerTests.swift */, @@ -600,6 +639,7 @@ 911DB13524C4FC100027F3C7 /* ParseObjectTests.swift */, 70CE1D882545BF730018D572 /* ParsePointerTests.swift */, 70C7DC1F24D20F180050419B /* ParseQueryTests.swift */, + 70C5504525B40D5200B5DBC2 /* ParseSessionTests.swift */, 70C7DC1D24D20E530050419B /* ParseUserTests.swift */, 7FFF552A2217E729007C3B4E /* AnyCodableTests */, 911DB12A24C3F7260027F3C7 /* NetworkMocking */, @@ -646,18 +686,19 @@ 4AB8B4F61F254AE10070F682 /* ParseSwift */ = { isa = PBXGroup; children = ( + 4AB8B4F71F254AE10070F682 /* Parse.h */, + 4A82B7EE1F254B820063D731 /* Parse.swift */, + 70110D51250680140091CC1D /* ParseConstants.swift */, F97B45C924D9C6F200F4A88B /* API */, - 70510AAA259EE23700FEA700 /* LiveQuery */, + 707A3BEF25B0A3B8000D215C /* Authentication */, F97B45B324D9C6F200F4A88B /* Coding */, 70110D5D250849B30091CC1D /* Internal */, + 70510AAA259EE23700FEA700 /* LiveQuery */, F97B463F24D9C78B00F4A88B /* Mutation Operations */, F97B45C324D9C6F200F4A88B /* Objects */, 70110D5E25084AF80091CC1D /* Protocols */, - F97B45BA24D9C6F200F4A88B /* Types */, F97B45CB24D9C6F200F4A88B /* Storage */, - 4A82B7EE1F254B820063D731 /* Parse.swift */, - 70110D51250680140091CC1D /* ParseConstants.swift */, - 4AB8B4F71F254AE10070F682 /* Parse.h */, + F97B45BA24D9C6F200F4A88B /* Types */, ); path = ParseSwift; sourceTree = ""; @@ -734,6 +775,24 @@ path = LiveQuery; sourceTree = ""; }; + 707A3BEF25B0A3B8000D215C /* Authentication */ = { + isa = PBXGroup; + children = ( + 70A2D81325B358FA001BEB7D /* 3rd Party */, + 70A2D81425B35905001BEB7D /* Internal */, + 707A3C1E25B14BAE000D215C /* Protocols */, + ); + path = Authentication; + sourceTree = ""; + }; + 707A3C1E25B14BAE000D215C /* Protocols */ = { + isa = PBXGroup; + children = ( + 707A3BF025B0A4F0000D215C /* ParseAuthenticatable.swift */, + ); + path = Protocols; + sourceTree = ""; + }; 709B98312556EC7400507778 /* ParseSwiftTeststvOS */ = { isa = PBXGroup; children = ( @@ -742,6 +801,22 @@ path = ParseSwiftTeststvOS; sourceTree = ""; }; + 70A2D81325B358FA001BEB7D /* 3rd Party */ = { + isa = PBXGroup; + children = ( + 707A3C1F25B14BCF000D215C /* ParseApple.swift */, + ); + path = "3rd Party"; + sourceTree = ""; + }; + 70A2D81425B35905001BEB7D /* Internal */ = { + isa = PBXGroup; + children = ( + 707A3C1025B0A8E8000D215C /* ParseAnonymous.swift */, + ); + path = Internal; + sourceTree = ""; + }; 70F2E23B254F246000B2EA5C /* ParseSwiftTeststvOS */ = { isa = PBXGroup; children = ( @@ -827,6 +902,7 @@ children = ( 70BDA2B2250536FF00FC2237 /* ParseInstallation.swift */, F97B45C624D9C6F200F4A88B /* ParseObject.swift */, + 70C5503725B406B800B5DBC2 /* ParseSession.swift */, F97B45C424D9C6F200F4A88B /* ParseUser.swift */, ); path = Objects; @@ -1270,6 +1346,8 @@ 916786E2259B7DDA00BB5B4E /* ParseCloud.swift in Sources */, F97B461624D9C6F200F4A88B /* Queryable.swift in Sources */, F97B45DA24D9C6F200F4A88B /* Extensions.swift in Sources */, + 70C5503825B406B800B5DBC2 /* ParseSession.swift in Sources */, + 707A3BF125B0A4F0000D215C /* ParseAuthenticatable.swift in Sources */, F97B465F24D9C7B500F4A88B /* KeychainStore.swift in Sources */, 705726E02592C2A800F0ADD5 /* ParseHash.swift in Sources */, 70110D52250680140091CC1D /* ParseConstants.swift in Sources */, @@ -1293,6 +1371,7 @@ F97B45E224D9C6F200F4A88B /* AnyEncodable.swift in Sources */, 700396EA25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */, 70572671259033A700F0ADD5 /* ParseFileManager.swift in Sources */, + 707A3C2025B14BD0000D215C /* ParseApple.swift in Sources */, F97B462224D9C6F200F4A88B /* PrimitiveObjectStore.swift in Sources */, F97B45E624D9C6F200F4A88B /* Query.swift in Sources */, 708D035225215F9B00646C70 /* Deletable.swift in Sources */, @@ -1321,6 +1400,7 @@ 70BDA2B3250536FF00FC2237 /* ParseInstallation.swift in Sources */, F97B462724D9C72700F4A88B /* API.swift in Sources */, 70647E9C259E3A9A004C1004 /* ParseType.swift in Sources */, + 707A3C1125B0A8E8000D215C /* ParseAnonymous.swift in Sources */, 70110D572506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45DE24D9C6F200F4A88B /* AnyCodable.swift in Sources */, ); @@ -1334,6 +1414,7 @@ 70CE1D892545BF730018D572 /* ParsePointerTests.swift in Sources */, 911DB12E24C4837E0027F3C7 /* APICommandTests.swift in Sources */, 911DB12C24C3F7720027F3C7 /* MockURLResponse.swift in Sources */, + 70C5504625B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, 70110D5C2506ED0E0091CC1D /* ParseInstallationTests.swift in Sources */, 705727B12593FF8800F0ADD5 /* ParseFileTests.swift in Sources */, 70BC0B33251903D1001556DB /* ParseGeoPointTests.swift in Sources */, @@ -1346,10 +1427,13 @@ 70C7DC2224D20F190050419B /* ParseObjectBatchTests.swift in Sources */, 7FFF552F2217E72A007C3B4E /* AnyCodableTests.swift in Sources */, 4AA807701F794C31008CD551 /* KeychainStoreTests.swift in Sources */, + 70C5502225B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, F971F4F624DE381A006CB79B /* ParseEncoderTests.swift in Sources */, 70C7DC2124D20F190050419B /* ParseQueryTests.swift in Sources */, 9194657824F16E330070296B /* ACLTests.swift in Sources */, + 70A2D86B25B3ADB6001BEB7D /* ParseAnonymousTests.swift in Sources */, 70C7DC1E24D20E530050419B /* ParseUserTests.swift in Sources */, + 70A2D81F25B36A7D001BEB7D /* ParseAuthenticationTests.swift in Sources */, 911DB13324C494390027F3C7 /* MockURLProtocol.swift in Sources */, 7057273A2592CBB100F0ADD5 /* HashTests.swift in Sources */, ); @@ -1363,6 +1447,8 @@ 916786E3259B7DDA00BB5B4E /* ParseCloud.swift in Sources */, F97B461724D9C6F200F4A88B /* Queryable.swift in Sources */, F97B45DB24D9C6F200F4A88B /* Extensions.swift in Sources */, + 70C5503925B406B800B5DBC2 /* ParseSession.swift in Sources */, + 707A3BF225B0A4F0000D215C /* ParseAuthenticatable.swift in Sources */, F97B466024D9C7B500F4A88B /* KeychainStore.swift in Sources */, 705726E12592C2A800F0ADD5 /* ParseHash.swift in Sources */, 70110D53250680140091CC1D /* ParseConstants.swift in Sources */, @@ -1386,6 +1472,7 @@ F97B45E324D9C6F200F4A88B /* AnyEncodable.swift in Sources */, 700396EB25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */, 70572672259033A700F0ADD5 /* ParseFileManager.swift in Sources */, + 707A3C2125B14BD0000D215C /* ParseApple.swift in Sources */, F97B462324D9C6F200F4A88B /* PrimitiveObjectStore.swift in Sources */, F97B45E724D9C6F200F4A88B /* Query.swift in Sources */, 708D035325215F9B00646C70 /* Deletable.swift in Sources */, @@ -1414,6 +1501,7 @@ 70BDA2B4250536FF00FC2237 /* ParseInstallation.swift in Sources */, F97B462824D9C72700F4A88B /* API.swift in Sources */, 70647E9D259E3A9A004C1004 /* ParseType.swift in Sources */, + 707A3C1225B0A8E8000D215C /* ParseAnonymous.swift in Sources */, 70110D582506CE890091CC1D /* BaseParseInstallation.swift in Sources */, F97B45DF24D9C6F200F4A88B /* AnyCodable.swift in Sources */, ); @@ -1436,6 +1524,7 @@ 709B98532556ECAA00507778 /* ParsePointerTests.swift in Sources */, 709B984C2556ECAA00507778 /* APICommandTests.swift in Sources */, 709B984D2556ECAA00507778 /* AnyDecodableTests.swift in Sources */, + 70C5504825B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, 709B98572556ECAA00507778 /* ACLTests.swift in Sources */, 705727BC2593FF8C00F0ADD5 /* ParseFileTests.swift in Sources */, 709B984F2556ECAA00507778 /* AnyCodableTests.swift in Sources */, @@ -1448,10 +1537,13 @@ 709B984E2556ECAA00507778 /* ParseGeoPointTests.swift in Sources */, 709B984B2556ECAA00507778 /* MockURLProtocol.swift in Sources */, 709B98552556ECAA00507778 /* ParseQueryTests.swift in Sources */, + 70C5502425B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, 709B98502556ECAA00507778 /* KeychainStoreTests.swift in Sources */, 709B98562556ECAA00507778 /* ParseObjectTests.swift in Sources */, 709B985A2556ECAA00507778 /* ParseObjectBatchTests.swift in Sources */, + 70A2D86D25B3ADB6001BEB7D /* ParseAnonymousTests.swift in Sources */, 709B98582556ECAA00507778 /* AnyEncodableTests.swift in Sources */, + 70A2D82125B36A7D001BEB7D /* ParseAuthenticationTests.swift in Sources */, 709B98542556ECAA00507778 /* ParseInstallationTests.swift in Sources */, 705727262592CBAF00F0ADD5 /* HashTests.swift in Sources */, ); @@ -1465,6 +1557,7 @@ 70F2E2B7254F283000B2EA5C /* ParsePointerTests.swift in Sources */, 70F2E2B5254F283000B2EA5C /* ParseEncoderTests.swift in Sources */, 70F2E2C2254F283000B2EA5C /* APICommandTests.swift in Sources */, + 70C5504725B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, 70F2E2BC254F283000B2EA5C /* ParseObjectTests.swift in Sources */, 705727BB2593FF8B00F0ADD5 /* ParseFileTests.swift in Sources */, 70F2E2BD254F283000B2EA5C /* AnyDecodableTests.swift in Sources */, @@ -1477,10 +1570,13 @@ 70F2E2C0254F283000B2EA5C /* MockURLResponse.swift in Sources */, 70F2E2BE254F283000B2EA5C /* ParseObjectBatchTests.swift in Sources */, 70F2E2BF254F283000B2EA5C /* MockURLProtocol.swift in Sources */, + 70C5502325B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, 70F2E2BB254F283000B2EA5C /* ParseGeoPointTests.swift in Sources */, 70F2E2B8254F283000B2EA5C /* AnyEncodableTests.swift in Sources */, 70F2E2B4254F283000B2EA5C /* ParseQueryTests.swift in Sources */, + 70A2D86C25B3ADB6001BEB7D /* ParseAnonymousTests.swift in Sources */, 70F2E2BA254F283000B2EA5C /* ParseInstallationTests.swift in Sources */, + 70A2D82025B36A7D001BEB7D /* ParseAuthenticationTests.swift in Sources */, 70F2E2B9254F283000B2EA5C /* KeychainStoreTests.swift in Sources */, 705727302592CBB000F0ADD5 /* HashTests.swift in Sources */, ); @@ -1494,6 +1590,8 @@ 916786E5259B7DDA00BB5B4E /* ParseCloud.swift in Sources */, F97B45E924D9C6F200F4A88B /* Query.swift in Sources */, F97B463624D9C74400F4A88B /* URLSession+extensions.swift in Sources */, + 70C5503B25B406B800B5DBC2 /* ParseSession.swift in Sources */, + 707A3BF425B0A4F0000D215C /* ParseAuthenticatable.swift in Sources */, F97B460524D9C6F200F4A88B /* NoBody.swift in Sources */, 705726E32592C2A800F0ADD5 /* ParseHash.swift in Sources */, F97B45E124D9C6F200F4A88B /* AnyCodable.swift in Sources */, @@ -1517,6 +1615,7 @@ F97B45DD24D9C6F200F4A88B /* Extensions.swift in Sources */, 700396ED25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */, 70572674259033A700F0ADD5 /* ParseFileManager.swift in Sources */, + 707A3C2325B14BD0000D215C /* ParseApple.swift in Sources */, F97B462124D9C6F200F4A88B /* ParseStorage.swift in Sources */, F97B466724D9C88600F4A88B /* SecureStorage.swift in Sources */, 708D035525215F9B00646C70 /* Deletable.swift in Sources */, @@ -1545,6 +1644,7 @@ F97B45F124D9C6F200F4A88B /* BaseParseUser.swift in Sources */, F97B45D924D9C6F200F4A88B /* ParseEncoder.swift in Sources */, 70647E9F259E3A9A004C1004 /* ParseType.swift in Sources */, + 707A3C1425B0A8E8000D215C /* ParseAnonymous.swift in Sources */, 912C9BFD24D302B2009947C3 /* Parse.swift in Sources */, F97B461924D9C6F200F4A88B /* Queryable.swift in Sources */, ); @@ -1558,6 +1658,8 @@ 916786E4259B7DDA00BB5B4E /* ParseCloud.swift in Sources */, F97B45E824D9C6F200F4A88B /* Query.swift in Sources */, F97B463524D9C74400F4A88B /* URLSession+extensions.swift in Sources */, + 70C5503A25B406B800B5DBC2 /* ParseSession.swift in Sources */, + 707A3BF325B0A4F0000D215C /* ParseAuthenticatable.swift in Sources */, F97B460424D9C6F200F4A88B /* NoBody.swift in Sources */, 705726E22592C2A800F0ADD5 /* ParseHash.swift in Sources */, F97B45E024D9C6F200F4A88B /* AnyCodable.swift in Sources */, @@ -1581,6 +1683,7 @@ F97B45DC24D9C6F200F4A88B /* Extensions.swift in Sources */, 700396EC25A3892D0052CB31 /* LiveQuerySocketDelegate.swift in Sources */, 70572673259033A700F0ADD5 /* ParseFileManager.swift in Sources */, + 707A3C2225B14BD0000D215C /* ParseApple.swift in Sources */, F97B462024D9C6F200F4A88B /* ParseStorage.swift in Sources */, F97B466624D9C88600F4A88B /* SecureStorage.swift in Sources */, 708D035425215F9B00646C70 /* Deletable.swift in Sources */, @@ -1609,6 +1712,7 @@ F97B45F024D9C6F200F4A88B /* BaseParseUser.swift in Sources */, F97B45D824D9C6F200F4A88B /* ParseEncoder.swift in Sources */, 70647E9E259E3A9A004C1004 /* ParseType.swift in Sources */, + 707A3C1325B0A8E8000D215C /* ParseAnonymous.swift in Sources */, 912C9BE024D302B0009947C3 /* Parse.swift in Sources */, F97B461824D9C6F200F4A88B /* Queryable.swift in Sources */, ); diff --git a/Sources/ParseSwift/API/Responses.swift b/Sources/ParseSwift/API/Responses.swift index c31dd2309..c812a138b 100644 --- a/Sources/ParseSwift/API/Responses.swift +++ b/Sources/ParseSwift/API/Responses.swift @@ -119,6 +119,7 @@ internal struct LoginSignupResponse: Codable { let objectId: String let sessionToken: String var updatedAt: Date? + let username: String? } // MARK: ParseFile diff --git a/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift b/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift new file mode 100644 index 000000000..f0885df9e --- /dev/null +++ b/Sources/ParseSwift/Authentication/3rd Party/ParseApple.swift @@ -0,0 +1,150 @@ +// +// ParseApple.swift +// ParseSwift +// +// Created by Corey Baker on 1/14/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation + +// swiftlint:disable line_length + +/** + Provides utility functions for working with Apple User Authentication and `ParseUser`'s. + Be sure your Parse Server is configured for [sign in with Apple](https://docs.parseplatform.org/parse-server/guide/#configuring-parse-server-for-sign-in-with-apple). + For information on acquiring Apple sign-in credentials to use with `ParseApple`, refer to [Apple's Documentation](https://developer.apple.com/documentation/authenticationservices/implementing_user_authentication_with_sign_in_with_apple). + */ +public struct ParseApple: ParseAuthenticatable { + + /// Authentication keys required for Apple authentication. + enum AuthenticationKeys: String, Codable { + case id // swiftlint:disable:this identifier_name + case token + + /// Properly makes an authData dictionary with the required keys. + /// - parameter id: Required id. + /// - parameter token: Required token. + /// - returns: Required authData dictionary. + func makeDictionary(user: String, + identityToken: String) -> [String: String] { + [AuthenticationKeys.id.rawValue: user, + AuthenticationKeys.token.rawValue: identityToken] + } + + /// Verifies all mandatory keys are in authData. + /// - parameter authData: Dictionary containing key/values. + /// - returns: `true` if all the mandatory keys are present, `false` otherwise. + func verifyMandatoryKeys(authData: [String: String]?) -> Bool { + guard let authData = authData, + authData[AuthenticationKeys.id.rawValue] != nil, + authData[AuthenticationKeys.token.rawValue] != nil else { + return false + } + return true + } + } + public static var __type: String { // swiftlint:disable:this identifier_name + "apple" + } + public init() { } +} + +// MARK: Login +public extension ParseApple { + /** + Login a `ParseUser` *asynchronously* using Apple authentication. + - 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. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: The block to execute. + */ + func login(user: String, + identityToken: String, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + login(authData: AuthenticationKeys.id.makeDictionary(user: user, identityToken: identityToken), + options: options, + callbackQueue: callbackQueue, + completion: completion) + } + + func login(authData: [String: String]?, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + 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\".") + callbackQueue.async { + completion(.failure(error)) + } + return + } + AuthenticatedUser.login(Self.__type, + authData: authData, + options: options, + callbackQueue: callbackQueue, + completion: completion) + } +} + +// MARK: Link +public extension ParseApple { + + /** + Link the *current* `ParseUser` *asynchronously* using Apple authentication. + - 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. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: The block to execute. + */ + func link(user: String, + identityToken: String, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + link(authData: AuthenticationKeys.id.makeDictionary(user: user, identityToken: identityToken), + options: options, + callbackQueue: callbackQueue, + completion: completion) + } + + func link(authData: [String: String]?, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + 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\".") + callbackQueue.async { + completion(.failure(error)) + } + return + } + AuthenticatedUser.link(Self.__type, + authData: authData, + options: options, + callbackQueue: callbackQueue, + completion: completion) + } +} + +// MARK: 3rd Party Authentication - ParseApple +public extension ParseUser { + + /// An apple `ParseUser`. + static var apple: ParseApple { + ParseApple() + } + + /// An apple `ParseUser`. + var apple: ParseApple { + Self.apple + } +} diff --git a/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift b/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift new file mode 100644 index 000000000..d8138e17c --- /dev/null +++ b/Sources/ParseSwift/Authentication/Internal/ParseAnonymous.swift @@ -0,0 +1,98 @@ +// +// ParseAnonymous.swift +// ParseSwift +// +// Created by Corey Baker on 1/14/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation + +/** + Provides utility functions for working with Anonymously logged-in users. + + Anonymous users have some unique characteristics: + - Anonymous users don't need a user name or password. + - Once logged out, an anonymous user cannot be recovered. + - When the current user is anonymous, the following methods can be used to switch + to a different user or convert the anonymous user into a regular one: + - *signup* converts an anonymous user to a standard user with the given username and password. + Data associated with the anonymous user is retained. + - *login* switches users without converting the anonymous user. + Data associated with the anonymous user will be lost. + - Service *login* (e.g. Apple, Facebook, Twitter) will attempt to convert + the anonymous user into a standard user by linking it to the service. + If a user already exists that is linked to the service, it will instead switch to the existing user. + - Service linking (e.g. Apple, Facebook, Twitter) will convert the anonymous user + into a standard user by linking it to the service. + */ +public struct ParseAnonymous: ParseAuthenticatable { + + enum AuthenticationKeys: String, Codable { + case id // swiftlint:disable:this identifier_name + + func makeDictionary() -> [String: String] { + [AuthenticationKeys.id.rawValue: UUID().uuidString.lowercased()] + } + } + public static var __type: String { // swiftlint:disable:this identifier_name + "anonymous" + } + public init() { } +} + +// MARK: Login +public extension ParseAnonymous { + /** + Login a `ParseUser` *synchronously* 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. + - throws: `ParseError`. + - returns: the linked `ParseUser`. + */ + func login(authData: [String: String]? = nil, + options: API.Options = []) throws -> AuthenticatedUser { + return try AuthenticatedUser + .login(__type, + authData: AuthenticationKeys.id.makeDictionary(), + options: options) + } + + func login(authData: [String: String]? = nil, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + AuthenticatedUser.login(__type, + authData: AuthenticationKeys.id.makeDictionary(), + options: options, + callbackQueue: callbackQueue, + completion: completion) + } +} + +// MARK: Link +public extension ParseAnonymous { + /// Unavailable for `ParseAnonymous`. Will always return an error. + func link(authData: [String: String]? = nil, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + callbackQueue.async { + completion(.failure(ParseError(code: .unknownError, message: "Not supported"))) + } + } +} + +// MARK: ParseAnonymous +public extension ParseUser { + + /// An anonymous `ParseUser`. + static var anonymous: ParseAnonymous { + ParseAnonymous() + } + + /// An anonymous `ParseUser`. + var anonymous: ParseAnonymous { + Self.anonymous + } +} diff --git a/Sources/ParseSwift/Authentication/Protocols/ParseAuthenticatable.swift b/Sources/ParseSwift/Authentication/Protocols/ParseAuthenticatable.swift new file mode 100644 index 000000000..1864f908d --- /dev/null +++ b/Sources/ParseSwift/Authentication/Protocols/ParseAuthenticatable.swift @@ -0,0 +1,323 @@ +// +// ParseAuthenticatable.swift +// ParseSwift +// +// Created by Corey Baker on 1/14/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation + +public protocol ParseAuthenticatable: Codable { + associatedtype AuthenticatedUser: ParseUser + init() + + /// The type of authentication. + static var __type: String { get } // swiftlint:disable:this identifier_name + + /// Returns `true` if the *current* user is linked to the respective authentication type. + var isLinked: Bool { get } + + /** + 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. + */ + func login(authData: [String: String]?, + options: API.Options, + callbackQueue: DispatchQueue, + completion: @escaping (Result) -> Void) + + /** + 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. + */ + func link(authData: [String: String]?, + options: API.Options, + callbackQueue: DispatchQueue, + completion: @escaping (Result) -> Void) + + /** + Whether the `ParseUser` is logged in with the respective authentication type. + - parameter user: The `ParseUser` to check authentication type. 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 user: AuthenticatedUser) -> Bool + + /** + 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)`. + */ + func unlink(_ user: AuthenticatedUser, + options: API.Options, + callbackQueue: DispatchQueue, + completion: @escaping (Result) -> Void) + + /** + 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)`. + */ + func unlink(options: API.Options, + callbackQueue: DispatchQueue, + completion: @escaping (Result) -> Void) + + /** + Strips the *current* user of a respective authentication type. + - returns: The *current* user whose autentication type was stripped. Returns `nil` + if there's no current user. This modified user has not been saved. + */ + func strip() + + /** + Strips the `ParseUser`of a respective authentication type. + - parameter user: The `ParseUser` to strip. The user must be logged in on this device. + - returns: The user whose autentication type was stripped. This modified user has not been saved. + */ + func strip(_ user: AuthenticatedUser) -> AuthenticatedUser +} + +// MARK: Convenience Implementations +public extension ParseAuthenticatable { + + var __type: String { // swiftlint:disable:this identifier_name + Self.__type + } + + var isLinked: Bool { + guard let current = AuthenticatedUser.current else { + return false + } + return current.isLinked(with: __type) + } + + func isLinked(with user: AuthenticatedUser) -> Bool { + user.isLinked(with: __type) + } + + func unlink(_ user: AuthenticatedUser, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + user.unlink(__type, options: options, callbackQueue: callbackQueue, completion: completion) + } + + func unlink(options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + guard let current = AuthenticatedUser.current else { + let error = ParseError(code: .invalidLinkedSession, message: "No current ParseUser.") + callbackQueue.async { + completion(.failure(error)) + } + return + } + unlink(current, options: options, callbackQueue: callbackQueue, completion: completion) + } + + func strip() { + guard let user = AuthenticatedUser.current else { + return + } + AuthenticatedUser.current = strip(user) + } + + func strip(_ user: AuthenticatedUser) -> AuthenticatedUser { + if isLinked(with: user) { + var user = user + user.authData?.updateValue(nil, forKey: __type) + return user + } + return user + } +} + +public extension ParseUser { + + // MARK: 3rd Party Authentication - Login + /** + Makes a *synchronous* request to login a user with specified credentials. + + Returns 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. + - throws: An error of type `ParseError`. + - returns: An instance of the logged in `ParseUser`. + If login failed due to either an incorrect password or incorrect username, it throws a `ParseError`. + */ + static func login(_ type: String, + authData: [String: String], + options: API.Options) throws -> Self { + let body = SignupLoginBody(authData: [type: authData]) + return try signupCommand(body: body).execute(options: options) + } + + /** + Makes an *asynchronous* request to log in a user with specified credentials. + Returns 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. + - 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)`. + */ + static func login(_ type: String, + authData: [String: String], + options: API.Options, + callbackQueue: DispatchQueue, + completion: @escaping (Result) -> Void) { + + let body = SignupLoginBody(authData: [type: authData]) + signupCommand(body: body) + .executeAsync(options: options) { result in + callbackQueue.async { + completion(result) + } + } + } + + // MARK: 3rd Party Authentication - Link + func isLinked(with type: String) -> Bool { + guard let authData = self.authData?[type] else { + return false + } + return authData != nil + } + + func unlink(_ type: String, + options: API.Options, + callbackQueue: DispatchQueue, + completion: @escaping (Result) -> Void) { + + guard let current = Self.current, + current.authData != nil else { + let error = ParseError(code: .unknownError, message: "Must be logged in to unlink user") + callbackQueue.async { + completion(.failure(error)) + } + return + } + + if current.isLinked(with: type) { + guard let authData = current.apple.strip(current).authData else { + let error = ParseError(code: .unknownError, message: "Missing authData.") + callbackQueue.async { + completion(.failure(error)) + } + return + } + let body = SignupLoginBody(authData: authData) + current.linkCommand(body: body) + .executeAsync(options: options) { result in + callbackQueue.async { + completion(result) + } + } + } else { + callbackQueue.async { + completion(.success(self)) + } + } + } + + /** + Makes a *synchronous* request to link a user with specified credentials. The user should already be logged in. + + Returns 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. + - throws: An error of type `ParseError`. + - returns: An instance of the logged in `ParseUser`. + If login failed due to either an incorrect password or incorrect username, it throws a `ParseError`. + */ + static func link(_ type: String, + authData: [String: String], + options: API.Options) throws -> Self { + guard let current = Self.current else { + throw ParseError(code: .unknownError, message: "Must be logged in to link user") + } + let body = SignupLoginBody(authData: [type: authData]) + return try current.linkCommand(body: body).execute(options: options) + } + + /** + Makes an *asynchronous* request to link a user with specified credentials. The user should already be logged in. + Returns 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. + - 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)`. + */ + static func link(_ type: String, + authData: [String: String], + options: API.Options, + callbackQueue: DispatchQueue, + completion: @escaping (Result) -> Void) { + guard let current = Self.current else { + let error = ParseError(code: .unknownError, message: "Must be logged in to link user") + callbackQueue.async { + completion(.failure(error)) + } + return + } + let body = SignupLoginBody(authData: [type: authData]) + current.linkCommand(body: body) + .executeAsync(options: options) { result in + callbackQueue.async { + completion(result) + } + } + } + + internal func linkCommand(body: SignupLoginBody) -> API.NonParseBodyCommand { + + return API.NonParseBodyCommand(method: .PUT, + path: endpoint, + body: body) { (data) -> Self in + let user = try ParseCoding.jsonDecoder().decode(Self.self, from: data) + if let authData = body.authData { + Self.current?.anonymous.strip() + if Self.current?.authData == nil { + Self.current?.authData = authData + } else { + authData.forEach { (key, value) in + Self.current?.authData?[key] = value + } + } + if let updatedAt = user.updatedAt { + Self.current?.updatedAt = updatedAt + } + } + Self.saveCurrentContainerToKeychain() + guard let current = Self.current else { + throw ParseError(code: .unknownError, message: "Should have a current user.") + } + return current + } + } +} diff --git a/Sources/ParseSwift/Internal/BaseParseUser.swift b/Sources/ParseSwift/Internal/BaseParseUser.swift index 8fad515b8..8d44f6f8d 100644 --- a/Sources/ParseSwift/Internal/BaseParseUser.swift +++ b/Sources/ParseSwift/Internal/BaseParseUser.swift @@ -9,6 +9,7 @@ import Foundation /// Used internally to form a concrete type representing `ParseUser`. internal struct BaseParseUser: ParseUser { + var authData: [String: [String: String]?]? var username: String? var email: String? var password: String? diff --git a/Sources/ParseSwift/Internal/ParseHash.swift b/Sources/ParseSwift/Internal/ParseHash.swift index 3752010cb..b640ad067 100644 --- a/Sources/ParseSwift/Internal/ParseHash.swift +++ b/Sources/ParseSwift/Internal/ParseHash.swift @@ -5,7 +5,7 @@ // Created by Corey Baker on 12/22/20. // Copyright © 2020 Parse Community. All rights reserved. // - +#if !os(Linux) import Foundation import CommonCrypto @@ -34,3 +34,4 @@ struct ParseHash { return md5HashFromData(data) } } +#endif diff --git a/Sources/ParseSwift/Objects/ParseInstallation.swift b/Sources/ParseSwift/Objects/ParseInstallation.swift index 0eb3ebd54..cefe0b705 100644 --- a/Sources/ParseSwift/Objects/ParseInstallation.swift +++ b/Sources/ParseSwift/Objects/ParseInstallation.swift @@ -22,7 +22,7 @@ import AppKit checks. A valid `ParseInstallation` can only be instantiated via - `+current` because the required identifier fields + *current* because the required identifier fields are readonly. The `timeZone` and `badge` fields are also readonly properties which are automatically updated to match the device's time zone and application badge when the `ParseInstallation` is saved, thus these fields might not reflect the @@ -80,7 +80,7 @@ public protocol ParseInstallation: ParseObject { // MARK: Default Implementations public extension ParseInstallation { static var className: String { - return "_Installation" + "_Installation" } } @@ -107,6 +107,7 @@ extension ParseInstallation { get { guard let installationInMemory: CurrentInstallationContainer = try? ParseStorage.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { + #if !os(Linux) guard let installationFromKeyChain: CurrentInstallationContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { @@ -119,6 +120,15 @@ extension ParseInstallation { try? ParseStorage.shared.set(newInstallation, for: ParseStorage.Keys.currentInstallation) return newInstallation } + #else + var newInstallation = CurrentInstallationContainer() + let newInstallationId = UUID().uuidString.lowercased() + newInstallation.installationId = newInstallationId + newInstallation.currentInstallation?.createInstallationId(newId: newInstallationId) + newInstallation.currentInstallation?.updateAutomaticInfo() + try? ParseStorage.shared.set(newInstallation, for: ParseStorage.Keys.currentInstallation) + return newInstallation + #endif return installationFromKeyChain } return installationInMemory @@ -145,12 +155,16 @@ extension ParseInstallation { = try? ParseStorage.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { return } + #if !os(Linux) try? KeychainStore.shared.set(currentInstallationInMemory, for: ParseStorage.Keys.currentInstallation) + #endif } internal static func deleteCurrentContainerFromKeychain() { try? ParseStorage.shared.delete(valueFor: ParseStorage.Keys.currentInstallation) + #if !os(Linux) try? KeychainStore.shared.delete(valueFor: ParseStorage.Keys.currentInstallation) + #endif } /** diff --git a/Sources/ParseSwift/Objects/ParseSession.swift b/Sources/ParseSwift/Objects/ParseSession.swift new file mode 100644 index 000000000..a65c928b2 --- /dev/null +++ b/Sources/ParseSwift/Objects/ParseSession.swift @@ -0,0 +1,45 @@ +// +// ParseSession.swift +// ParseSwift +// +// Created by Corey Baker on 1/17/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation + +/** + `ParseSession` is a local representation of a session. + This protocol conforms to `ParseObject` and retains the + same functionality. + */ +public protocol ParseSession: ParseObject { + associatedtype SessionUser: ParseUser + + /// The session token for this session. + var sessionToken: String { get } + + /// The user the session is for. + var user: SessionUser { get } + + /// Whether the session is restricted. + var restricted: Bool? { get } + + /// Information about how the session was created. + var createdWith: [String: String] { get } + + /// Referrs to the `ParseInstallation` where the + /// session logged in from. + var installationId: String { get } + + /// Approximate date when this session will automatically + /// expire. + var expiresAt: Date { get } +} + +// MARK: Default Implementations +public extension ParseSession { + static var className: String { + "_Session" + } +} diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index f3a44a5ae..bd6901c87 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -23,12 +23,28 @@ public protocol ParseUser: ParseObject { It is only meant to be set. */ var password: String? { get set } + + /** + The authentication data for the `ParseUser`. Used by `ParseAnonymous` + or any authentication type that conforms to `ParseAuthentication`. + */ + var authData: [String: [String: String]?]? { get set } } // MARK: SignupLoginBody struct SignupLoginBody: Encodable { - let username: String - let password: String + var username: String? + var password: String? + var authData: [String: [String: String]?]? + + init(username: String, password: String) { + self.username = username + self.password = password + } + + init(authData: [String: [String: String]?]) { + self.authData = authData + } } // MARK: EmailBody @@ -39,7 +55,7 @@ struct EmailBody: Encodable { // MARK: Default Implementations public extension ParseUser { static var className: String { - return "_User" + "_User" } } @@ -66,7 +82,11 @@ extension ParseUser { get { guard let currentUserInMemory: CurrentUserContainer = try? ParseStorage.shared.get(valueFor: ParseStorage.Keys.currentUser) else { + #if !os(Linux) return try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) + #else + return nil + #endif } return currentUserInMemory } @@ -79,12 +99,16 @@ extension ParseUser { = try? ParseStorage.shared.get(valueFor: ParseStorage.Keys.currentUser) else { return } + #if !os(Linux) try? KeychainStore.shared.set(currentUserInMemory, for: ParseStorage.Keys.currentUser) + #endif } internal static func deleteCurrentContainerFromKeychain() { try? ParseStorage.shared.delete(valueFor: ParseStorage.Keys.currentUser) + #if !os(Linux) try? KeychainStore.shared.delete(valueFor: ParseStorage.Keys.currentUser) + #endif BaseParseUser.currentUserContainer = nil } @@ -96,7 +120,13 @@ extension ParseUser { */ public static var current: Self? { get { Self.currentUserContainer?.currentUser } - set { Self.currentUserContainer?.currentUser = newValue } + set { + if Self.currentUserContainer?.currentUser?.username != newValue?.username && newValue != nil { + Self.currentUserContainer?.currentUser = newValue?.anonymous.strip(newValue!) + } else { + Self.currentUserContainer?.currentUser = newValue + } + } } /** @@ -116,13 +146,12 @@ extension ParseUser { Makes a *synchronous* request to login a user with specified credentials. Returns 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. + 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 error: The error object to set on error. - - - throws: An error of type `ParseUser`. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - throws: An error of type `ParseError`. - returns: An instance of the logged in `ParseUser`. If login failed due to either an incorrect password or incorrect username, it throws a `ParseError`. */ @@ -135,9 +164,10 @@ extension ParseUser { Makes an *asynchronous* request to log in a user with specified credentials. Returns 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. + 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. - 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)`. @@ -167,7 +197,6 @@ extension ParseUser { let response = try ParseCoding.jsonDecoder().decode(LoginSignupResponse.self, from: data) var user = try ParseCoding.jsonDecoder().decode(Self.self, from: data) user.username = username - user.password = password Self.currentUserContainer = .init( currentUser: user, @@ -177,6 +206,90 @@ extension ParseUser { return user } } + + /** + Logs in a `ParseUser` *synchronously* with a session token. On success, this saves the session + to the keychain, so you can retrieve the currently logged in user using *current*. + + - 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. + - throws: An Error of `ParseError` type. + */ + public func become(sessionToken: String, options: API.Options = []) throws -> Self { + var newUser = self + newUser.objectId = "me" + var options = options + options.insert(.sessionToken(sessionToken)) + return try newUser.meCommand(sessionToken: sessionToken) + .execute(options: options, + callbackQueue: .main) + } + + /** + Logs in a `ParseUser` *asynchronously* with a session token. On success, this saves the session + to the keychain, so you can retrieve the currently logged in user using *current*. + + - 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. + - parameter callbackQueue: The queue to return to after completion. Default + value of .main. + - parameter completion: The block to execute when completed. + It should have the following argument signature: `(Result)`. + - important: If an object fetched has the same objectId as current, it will automatically update the current. + */ + public func become(sessionToken: String, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + var newUser = self + newUser.objectId = "me" + var options = options + options.insert(.sessionToken(sessionToken)) + do { + try newUser.meCommand(sessionToken: sessionToken) + .executeAsync(options: options, + callbackQueue: callbackQueue) { result in + if case .success(let foundResult) = result { + callbackQueue.async { + completion(.success(foundResult)) + } + } else { + callbackQueue.async { + completion(result) + } + } + } + } catch let error as ParseError { + callbackQueue.async { + completion(.failure(error)) + } + } catch { + callbackQueue.async { + completion(.failure(ParseError(code: .unknownError, message: error.localizedDescription))) + } + } + } + + internal func meCommand(sessionToken: String) throws -> API.Command { + + return API.Command(method: .GET, + path: endpoint) { (data) -> Self in + let user = try ParseCoding.jsonDecoder().decode(Self.self, from: data) + + if let current = Self.current { + if !current.hasSameObjectId(as: user) && self.anonymous.isLinked { + Self.deleteCurrentContainerFromKeychain() + } + } + + Self.currentUserContainer = .init( + currentUser: user, + sessionToken: sessionToken + ) + Self.saveCurrentContainerToKeychain() + return user + } + } } // MARK: Logging Out @@ -195,7 +308,7 @@ extension ParseUser { This will also remove the session from the Keychain, log out of linked services and all future calls to `current` will return `nil`. This is preferable to using `logout`, unless your code is already running from a background thread. - + - 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 logging out, completes or fails. */ @@ -355,7 +468,14 @@ extension ParseUser { */ public static func signup(username: String, password: String, options: API.Options = []) throws -> Self { - try signupCommand(username: username, password: password).execute(options: options) + if Self.current != nil { + Self.current!.username = username + Self.current!.password = password + Self.current!.anonymous.strip() + return try Self.current!.save(options: options) + } + return try signupCommand(body: SignupLoginBody(username: username, password: password)) + .execute(options: options) } /** @@ -368,7 +488,14 @@ extension ParseUser { - returns: Returns whether the sign up was successful. */ public func signup(options: API.Options = []) throws -> Self { - try signupCommand().execute(options: options, callbackQueue: .main) + if let current = Self.current { + if !current.anonymous.isLinked { + return try current.save(options: options) + } else { + throw ParseError(code: .usernameTaken, message: "Cannot sign up a user that has already signed up.") + } + } + return try signupCommand().execute(options: options, callbackQueue: .main) } /** @@ -384,6 +511,16 @@ extension ParseUser { */ public func signup(options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { + if let current = Self.current { + if !current.anonymous.isLinked { + current.save(options: options, callbackQueue: callbackQueue, completion: completion) + } else { + let error = ParseError(code: .usernameTaken, + message: "Cannot sign up a user that has already signed up.") + completion(.failure(error)) + } + return + } signupCommand() .executeAsync(options: options, callbackQueue: callbackQueue) { result in @@ -411,9 +548,16 @@ extension ParseUser { password: String, options: API.Options = [], callbackQueue: DispatchQueue = .main, - completion: @escaping (Result) -> Void - ) { - signupCommand(username: username, password: password) + completion: @escaping (Result) -> Void) { + if Self.current != nil { + Self.current!.username = username + Self.current!.password = password + Self.current!.anonymous.strip() + Self.current!.save(options: options, callbackQueue: callbackQueue, completion: completion) + return + } + let body = SignupLoginBody(username: username, password: password) + signupCommand(body: body) .executeAsync(options: options) { result in callbackQueue.async { completion(result) @@ -421,16 +565,23 @@ extension ParseUser { } } - internal static func signupCommand(username: String, - password: String) -> API.NonParseBodyCommand { + internal static func signupCommand(body: SignupLoginBody) -> API.NonParseBodyCommand { - let body = SignupLoginBody(username: username, password: password) return API.NonParseBodyCommand(method: .POST, path: .users, body: body) { (data) -> Self in let response = try ParseCoding.jsonDecoder().decode(LoginSignupResponse.self, from: data) var user = try ParseCoding.jsonDecoder().decode(Self.self, from: data) - user.username = username - user.password = password + + if user.username == nil { + if let username = body.username { + user.username = username + } + } + if user.authData == nil { + if let authData = body.authData { + user.authData = authData + } + } Self.currentUserContainer = .init( currentUser: user, @@ -447,7 +598,6 @@ extension ParseUser { let response = try ParseCoding.jsonDecoder().decode(LoginSignupResponse.self, from: data) var user = try ParseCoding.jsonDecoder().decode(Self.self, from: data) user.username = self.username - user.password = self.password Self.currentUserContainer = .init( currentUser: user, diff --git a/Sources/ParseSwift/Protocols/Objectable.swift b/Sources/ParseSwift/Protocols/Objectable.swift index 1c84ee0b2..3cb1b9d15 100644 --- a/Sources/ParseSwift/Protocols/Objectable.swift +++ b/Sources/ParseSwift/Protocols/Objectable.swift @@ -53,7 +53,11 @@ extension Objectable { static func createHash(_ object: Encodable) throws -> String { let encoded = try ParseCoding.parseEncoder().encode(object) + #if !os(Linux) return ParseHash.md5HashFromData(encoded) + #else + return String(data: encoded, encoding: .utf8) + #endif } } diff --git a/Sources/ParseSwift/Storage/KeychainStore.swift b/Sources/ParseSwift/Storage/KeychainStore.swift index 48e889609..274108450 100644 --- a/Sources/ParseSwift/Storage/KeychainStore.swift +++ b/Sources/ParseSwift/Storage/KeychainStore.swift @@ -11,6 +11,8 @@ import Foundation import Security #endif +#if !os(Linux) + func getKeychainQueryTemplate(forService service: String) -> [String: String] { var query = [String: String]() if service.count > 0 { @@ -168,3 +170,4 @@ extension KeychainStore /* TypedSubscript */ { } } } +#endif diff --git a/Sources/ParseSwift/Types/ParseACL.swift b/Sources/ParseSwift/Types/ParseACL.swift index 6232754e8..e7957520d 100644 --- a/Sources/ParseSwift/Types/ParseACL.swift +++ b/Sources/ParseSwift/Types/ParseACL.swift @@ -165,7 +165,7 @@ public struct ParseACL: ParseType, Decodable, Equatable, Hashable { } private func toRole(roleName: String) -> String { - return "role:\(roleName)" + "role:\(roleName)" } private mutating func set(_ key: String, access: Access, value: Bool) { @@ -201,8 +201,13 @@ extension ParseACL { public static func defaultACL() throws -> Self { let currentUser = BaseParseUser.current - let aclController: DefaultACL? = - try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.defaultACL) + let aclController: DefaultACL? + + #if !os(Linux) + aclController = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.defaultACL) + #else + aclController = try? ParseStorage.shared.get(valueFor: ParseStorage.Keys.defaultACL) + #endif if aclController != nil { if !aclController!.useCurrentUser { @@ -261,8 +266,11 @@ extension ParseACL { DefaultACL(defaultACL: acl, lastCurrentUser: currentUser, useCurrentUser: withAccessForCurrentUser) } + #if !os(Linux) try? KeychainStore.shared.set(aclController, for: ParseStorage.Keys.defaultACL) - + #else + try? ParseStorage.shared.set(aclController, for: ParseStorage.Keys.defaultACL) + #endif return aclController.defaultACL } diff --git a/Tests/ParseSwiftTests/ACLTests.swift b/Tests/ParseSwiftTests/ACLTests.swift index bd1dcf588..1d181e166 100644 --- a/Tests/ParseSwiftTests/ACLTests.swift +++ b/Tests/ParseSwiftTests/ACLTests.swift @@ -26,11 +26,14 @@ class ACLTests: XCTestCase { override func tearDown() { super.tearDown() + #if !os(Linux) try? KeychainStore.shared.deleteAll() + #endif try? ParseStorage.shared.deleteAll() } struct User: ParseUser { + //: Those are required for Object var objectId: String? var createdAt: Date? @@ -41,12 +44,14 @@ class ACLTests: XCTestCase { 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 @@ -57,6 +62,7 @@ class ACLTests: XCTestCase { var username: String? var email: String? var password: String? + var authData: [String: [String: String]?]? // Your custom keys var customKey: String? diff --git a/Tests/ParseSwiftTests/APICommandTests.swift b/Tests/ParseSwiftTests/APICommandTests.swift index fb9f861fb..3b280ced0 100644 --- a/Tests/ParseSwiftTests/APICommandTests.swift +++ b/Tests/ParseSwiftTests/APICommandTests.swift @@ -28,7 +28,9 @@ class APICommandTests: XCTestCase { override func tearDown() { super.tearDown() MockURLProtocol.removeAll() + #if !os(Linux) try? KeychainStore.shared.deleteAll() + #endif try? ParseStorage.shared.deleteAll() } diff --git a/Tests/ParseSwiftTests/HashTests.swift b/Tests/ParseSwiftTests/HashTests.swift index 6091c15a3..19b7c82c6 100644 --- a/Tests/ParseSwiftTests/HashTests.swift +++ b/Tests/ParseSwiftTests/HashTests.swift @@ -5,7 +5,7 @@ // Created by Corey Baker on 12/22/20. // Copyright © 2020 Parse Community. All rights reserved. // - +#if !os(Linux) import Foundation import XCTest @testable import ParseSwift @@ -19,3 +19,4 @@ class HashTests: XCTestCase { XCTAssertEqual("9c853e20bb12ff256734a992dd224f17", ParseHash.md5HashFromString("foo א")) } } +#endif diff --git a/Tests/ParseSwiftTests/ParseAnonymousTests.swift b/Tests/ParseSwiftTests/ParseAnonymousTests.swift new file mode 100644 index 000000000..3a3a6d3f0 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseAnonymousTests.swift @@ -0,0 +1,369 @@ +// +// ParseAnonymousTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/16/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation +import XCTest +@testable import ParseSwift + +class ParseAnonymousTests: XCTestCase { + + 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 { + try super.setUpWithError() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + 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 testStrip() throws { + + let expectedAuth = ["id": "yolo"] + var user = User() + user.authData = [user.anonymous.__type: expectedAuth] + XCTAssertEqual(user.authData, ["anonymous": expectedAuth]) + let strippedAuth = user.anonymous.strip(user) + XCTAssertEqual(strippedAuth.authData, ["anonymous": nil]) + + } + + func testAuthenticationKeys() throws { + let authData = ParseAnonymous.AuthenticationKeys.id.makeDictionary() + XCTAssertEqual(Array(authData.keys), ["id"]) + XCTAssertNotNil(authData["id"]) + XCTAssertNotEqual(authData["id"], "") + XCTAssertNotEqual(authData["id"], "12345") + } + + func testLogin() throws { + 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 expectation1 = XCTestExpectation(description: "Login") + + User.anonymous.login { result in + switch result { + + case .success(let user): + XCTAssertEqual(user, User.current) + XCTAssertEqual(user, userOnServer) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertTrue(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testReplaceAnonymousWithUser() throws { + let expectedAuth = ["id": "yolo"] + var newUser = User() + newUser.authData = [newUser.anonymous.__type: expectedAuth] + newUser.username = "hello" + newUser.password = "world" + XCTAssertTrue(newUser.anonymous.isLinked(with: newUser)) + + //: Convert the anonymous user to a real new user. + var serverResponse = LoginSignupResponse() + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.anonymous.__type: nil] + serverResponse.createdAt = Date() + serverResponse.updatedAt = serverResponse.createdAt?.addingTimeInterval(+300) + + 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 expectation1 = XCTestExpectation(description: "Login") + + newUser.anonymous.login { result in + switch result { + + case .success(let user): + XCTAssertEqual(user, User.current) + XCTAssertEqual(user, userOnServer) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertFalse(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testReplaceAnonymousWithUsernameChange() throws { + let expectedAuth = ["id": "yolo"] + var user = try loginNormally() + user.authData = [user.anonymous.__type: expectedAuth] + User.current = user + XCTAssertEqual(user, User.current) + XCTAssertTrue(user.anonymous.isLinked) + + //Convert the anonymous user to a real new user. + User.current?.username = "hello" + User.current?.password = "world" + User.current?.authData = [user.anonymous.__type: nil] + var userOnServer = User.current! + userOnServer.updatedAt = user.updatedAt?.addingTimeInterval(+300) + + let encoded: Data! + do { + encoded = try userOnServer.getEncoder().encode(userOnServer, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try userOnServer.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.removeAll() + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let expectation1 = XCTestExpectation(description: "Login") + + User.current?.signup { result in + switch result { + + case .success(let user): + XCTAssertEqual(user, User.current) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertFalse(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func loginAnonymousUser() throws { + let authData = ["id": "yolo"] + + //: Convert the anonymous user to a real new user. + var serverResponse = LoginSignupResponse() + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.anonymous.__type: 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 user = try User.anonymous.login() + XCTAssertEqual(user, User.current) + XCTAssertEqual(user, userOnServer) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertTrue(user.anonymous.isLinked) + } + + func testReplaceAnonymousWithBecome() throws { // swiftlint:disable:this function_body_length + XCTAssertNil(User.current?.objectId) + try loginAnonymousUser() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + XCTAssertTrue(User.anonymous.isLinked) + + 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" + serverResponse.password = "this" + + 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 expectation1 = XCTestExpectation(description: "Fetch user1") + user.become(sessionToken: "newValue") { result in + + switch result { + case .success(let become): + XCTAssert(become.hasSameObjectId(as: userOnServer)) + guard let becomeCreatedAt = become.createdAt, + let becomeUpdatedAt = become.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = user.createdAt, + let originalUpdatedAt = user.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(becomeCreatedAt, originalCreatedAt) + XCTAssertGreaterThan(becomeUpdatedAt, originalUpdatedAt) + XCTAssertNil(become.ACL) + + //Should be updated in memory + XCTAssertEqual(User.current?.updatedAt, becomeUpdatedAt) + XCTAssertFalse(User.anonymous.isLinked) + + #if !os(Linux) + //Should be updated in Keychain + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) else { + XCTFail("Should get object from Keychain") + return + } + XCTAssertEqual(keychainUser.currentUser?.updatedAt, becomeUpdatedAt) + #endif + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } +} diff --git a/Tests/ParseSwiftTests/ParseAppleTests.swift b/Tests/ParseSwiftTests/ParseAppleTests.swift new file mode 100644 index 000000000..a068846c0 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseAppleTests.swift @@ -0,0 +1,364 @@ +// +// ParseAppleTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/16/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation +import XCTest +@testable import ParseSwift + +class ParseAppleTests: XCTestCase { + 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 { + try super.setUpWithError() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + 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 testAuthenticationKeys() throws { + let authData = ParseApple + .AuthenticationKeys.id.makeDictionary(user: "testing", + identityToken: "this") + XCTAssertEqual(authData, ["id": "testing", "token": "this"]) + } + + func testLogin() throws { + var serverResponse = LoginSignupResponse() + let authData = ParseApple + .AuthenticationKeys.id.makeDictionary(user: "testing", + identityToken: "this") + 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 expectation1 = XCTestExpectation(description: "Login") + + User.apple.login(user: "testing", identityToken: "this") { result in + switch result { + + case .success(let user): + XCTAssertEqual(user, User.current) + XCTAssertEqual(user, userOnServer) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertTrue(user.apple.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func loginAnonymousUser() throws { + let authData = ["id": "yolo"] + + //: Convert the anonymous user to a real new user. + var serverResponse = LoginSignupResponse() + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.anonymous.__type: 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 user = try User.anonymous.login() + XCTAssertEqual(user, User.current) + XCTAssertEqual(user, userOnServer) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertTrue(user.anonymous.isLinked) + } + + func testReplaceAnonymousWithApple() throws { + try loginAnonymousUser() + MockURLProtocol.removeAll() + let authData = ParseApple + .AuthenticationKeys.id.makeDictionary(user: "testing", + identityToken: "this") + + var serverResponse = LoginSignupResponse() + 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 expectation1 = XCTestExpectation(description: "Login") + + User.apple.login(user: "testing", identityToken: "this") { result in + switch result { + + case .success(let user): + XCTAssertEqual(user, User.current) + XCTAssertEqual(user, userOnServer) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertTrue(user.apple.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testReplaceAnonymousWithLinkedApple() throws { + try loginAnonymousUser() + 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 expectation1 = XCTestExpectation(description: "Login") + + User.apple.link(user: "testing", identityToken: "this") { result in + switch result { + + case .success(let user): + XCTAssertEqual(user, User.current) + XCTAssertEqual(user.updatedAt, userOnServer.updatedAt) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertTrue(user.apple.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testLinkLoggedInUserWithApple() throws { + _ = 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 expectation1 = XCTestExpectation(description: "Login") + + User.apple.link(user: "testing", identityToken: "this") { result in + switch result { + + case .success(let user): + XCTAssertEqual(user, User.current) + XCTAssertEqual(user.updatedAt, userOnServer.updatedAt) + XCTAssertEqual(user.username, "parse") + XCTAssertNil(user.password) + XCTAssertTrue(user.apple.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testUnlink() throws { + _ = 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 expectation1 = XCTestExpectation(description: "Login") + + User.apple.unlink { result in + switch result { + + case .success(let user): + XCTAssertEqual(user, User.current) + XCTAssertEqual(user.updatedAt, userOnServer.updatedAt) + XCTAssertEqual(user.username, "parse") + XCTAssertNil(user.password) + XCTAssertFalse(user.apple.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } +} diff --git a/Tests/ParseSwiftTests/ParseAuthenticationTests.swift b/Tests/ParseSwiftTests/ParseAuthenticationTests.swift new file mode 100644 index 000000000..48395db93 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseAuthenticationTests.swift @@ -0,0 +1,110 @@ +// +// ParseAuthenticationTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/16/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation +import XCTest +@testable import ParseSwift + +class ParseAuthenticationTests: XCTestCase { + + 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 TestAuth: ParseAuthenticatable { + static var __type: String { // swiftlint:disable:this identifier_name + "test" + } + func login(authData: [String: String]?, + options: API.Options, + callbackQueue: DispatchQueue, + completion: @escaping (Result) -> Void) { + let error = ParseError(code: .unknownError, message: "Not implemented") + completion(.failure(error)) + } + + func link(authData: [String: String]?, + options: API.Options, + callbackQueue: DispatchQueue, + completion: @escaping (Result) -> Void) { + let error = ParseError(code: .unknownError, message: "Not implemented") + completion(.failure(error)) + } + } + + override func setUpWithError() throws { + try super.setUpWithError() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testLinkCommand() { + var user = User() + let objectId = "yarr" + user.objectId = objectId + + let body = SignupLoginBody(authData: ["test": ["id": "yolo"]]) + + let command = user.linkCommand(body: body) + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/users/\(objectId)") + XCTAssertEqual(command.method, API.Method.PUT) + XCTAssertNil(command.params) + XCTAssertNotNil(command.body) + XCTAssertEqual(command.body?.authData, body.authData) + } + + func testIsLinkedWithString() throws { + + let expectedAuth = ["id": "yolo"] + var user = User() + let auth = TestAuth() + user.authData = [auth.__type: expectedAuth] + XCTAssertEqual(user.authData, ["test": expectedAuth]) + XCTAssertTrue(user.isLinked(with: "test")) + } + + func testAuthStrip() throws { + + let expectedAuth = ["id": "yolo"] + var user = User() + let auth = TestAuth() + user.authData = [auth.__type: expectedAuth] + XCTAssertEqual(user.authData, ["test": expectedAuth]) + let strippedAuth = auth.strip(user) + XCTAssertEqual(strippedAuth.authData, ["test": nil]) + } +} diff --git a/Tests/ParseSwiftTests/ParseCloudTests.swift b/Tests/ParseSwiftTests/ParseCloudTests.swift index 42d96f064..b539d5615 100644 --- a/Tests/ParseSwiftTests/ParseCloudTests.swift +++ b/Tests/ParseSwiftTests/ParseCloudTests.swift @@ -41,7 +41,9 @@ class ParseCloudTests: XCTestCase { // swiftlint:disable:this type_body_length override func tearDownWithError() throws { super.tearDown() MockURLProtocol.removeAll() + #if !os(Linux) try KeychainStore.shared.deleteAll() + #endif try ParseStorage.shared.deleteAll() } diff --git a/Tests/ParseSwiftTests/ParseFileManagerTests.swift b/Tests/ParseSwiftTests/ParseFileManagerTests.swift index 78ee3539d..240e99b0a 100644 --- a/Tests/ParseSwiftTests/ParseFileManagerTests.swift +++ b/Tests/ParseSwiftTests/ParseFileManagerTests.swift @@ -39,7 +39,9 @@ class ParseFileManagerTests: XCTestCase { override func tearDownWithError() throws { try super.tearDownWithError() MockURLProtocol.removeAll() + #if !os(Linux) try KeychainStore.shared.deleteAll() + #endif try ParseStorage.shared.deleteAll() guard let fileManager = ParseFileManager(), diff --git a/Tests/ParseSwiftTests/ParseFileTests.swift b/Tests/ParseSwiftTests/ParseFileTests.swift index 6772303bd..dfe60ed60 100644 --- a/Tests/ParseSwiftTests/ParseFileTests.swift +++ b/Tests/ParseSwiftTests/ParseFileTests.swift @@ -40,7 +40,9 @@ class ParseFileTests: XCTestCase { // swiftlint:disable:this type_body_length override func tearDownWithError() throws { try super.tearDownWithError() MockURLProtocol.removeAll() + #if !os(Linux) try KeychainStore.shared.deleteAll() + #endif try ParseStorage.shared.deleteAll() guard let fileManager = ParseFileManager(), diff --git a/Tests/ParseSwiftTests/ParseGeoPointTests.swift b/Tests/ParseSwiftTests/ParseGeoPointTests.swift index 5e35adff7..ddbf27397 100644 --- a/Tests/ParseSwiftTests/ParseGeoPointTests.swift +++ b/Tests/ParseSwiftTests/ParseGeoPointTests.swift @@ -29,7 +29,9 @@ class ParseGeoPointTests: XCTestCase { override func tearDown() { super.tearDown() MockURLProtocol.removeAll() + #if !os(Linux) try? KeychainStore.shared.deleteAll() + #endif try? ParseStorage.shared.deleteAll() } diff --git a/Tests/ParseSwiftTests/ParseInstallationTests.swift b/Tests/ParseSwiftTests/ParseInstallationTests.swift index 77488cdd5..7c18b90bd 100644 --- a/Tests/ParseSwiftTests/ParseInstallationTests.swift +++ b/Tests/ParseSwiftTests/ParseInstallationTests.swift @@ -18,6 +18,7 @@ import XCTest class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_length struct User: ParseUser { + //: Those are required for Object var objectId: String? var createdAt: Date? @@ -28,12 +29,14 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l 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 @@ -44,6 +47,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l var username: String? var email: String? var password: String? + var authData: [String: [String: String]?]? // Your custom keys var customKey: String? @@ -98,7 +102,9 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l override func tearDown() { super.tearDown() MockURLProtocol.removeAll() + #if !os(Linux) try? KeychainStore.shared.deleteAll() + #endif try? ParseStorage.shared.deleteAll() } @@ -167,11 +173,13 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l func testInstallationCustomValuesNotSavedToKeychain() { Installation.current?.customKey = "Changed" Installation.saveCurrentContainerToKeychain() + #if !os(Linux) guard let keychainInstallation: CurrentInstallationContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { return } XCTAssertNil(keychainInstallation.currentInstallation?.customKey) + #endif } func testInstallationImmutableFieldsCannotBeChangedInMemory() { @@ -249,6 +257,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l Installation.saveCurrentContainerToKeychain() + #if !os(Linux) guard let keychainInstallation: CurrentInstallationContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) else { expectation1.fulfill() @@ -263,6 +272,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l XCTAssertEqual(originalAppVersion, keychainInstallation.currentInstallation?.appVersion) XCTAssertEqual(originalParseVersion, keychainInstallation.currentInstallation?.parseVersion) XCTAssertEqual(originalLocaleIdentifier, keychainInstallation.currentInstallation?.localeIdentifier) + #endif expectation1.fulfill() } wait(for: [expectation1], timeout: 20.0) @@ -528,7 +538,8 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) - //Shold be updated in Keychain + //Should be updated in Keychain + #if !os(Linux) guard let keychainInstallation: CurrentInstallationContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation), let keychainUpdatedCurrentDate = keychainInstallation.currentInstallation?.updatedAt else { @@ -537,7 +548,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l return } XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) - + #endif } catch { XCTFail(error.localizedDescription) } @@ -608,7 +619,8 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) - //Shold be updated in Keychain + #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 { @@ -617,6 +629,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l return } XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif case .failure(let error): XCTFail(error.localizedDescription) } @@ -774,7 +787,8 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) - //Shold be updated in Keychain + #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 { @@ -783,6 +797,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l return } XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif case .failure(let error): XCTFail("Should have fetched: \(error.localizedDescription)") } @@ -862,7 +877,8 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) - //Shold be updated in Keychain + #if !os(Linux) + //Should be updated in Keychain guard let keychainInstallation: CurrentInstallationContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation), let keychainUpdatedCurrentDate = keychainInstallation @@ -872,6 +888,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l return } XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif case .failure(let error): XCTFail("Should have fetched: \(error.localizedDescription)") } @@ -973,7 +990,8 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l } XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) - //Shold be updated in Keychain + #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 { @@ -982,6 +1000,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l return } XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif case .failure(let error): XCTFail("Should have fetched: \(error.localizedDescription)") } @@ -1060,8 +1079,8 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l return } XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) - - //Shold be updated in Keychain + #if !os(Linux) + //Should be updated in Keychain guard let keychainInstallation: CurrentInstallationContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation), let keychainUpdatedCurrentDate = keychainInstallation @@ -1071,6 +1090,7 @@ class ParseInstallationTests: XCTestCase { // swiftlint:disable:this type_body_l return } XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif case .failure(let error): XCTFail("Should have fetched: \(error.localizedDescription)") } diff --git a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift index 40e0b72fa..c3e66cff4 100644 --- a/Tests/ParseSwiftTests/ParseLiveQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseLiveQueryTests.swift @@ -56,8 +56,10 @@ class ParseLiveQueryTests: XCTestCase { override func tearDownWithError() throws { try super.tearDownWithError() MockURLProtocol.removeAll() - try? KeychainStore.shared.deleteAll() - try? ParseStorage.shared.deleteAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() URLSession.liveQuery.closeAll() } diff --git a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift index 56149d946..4bcf8771e 100644 --- a/Tests/ParseSwiftTests/ParseObjectBatchTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectBatchTests.swift @@ -48,7 +48,9 @@ class ParseObjectBatchTests: XCTestCase { // swiftlint:disable:this type_body_le override func tearDown() { super.tearDown() MockURLProtocol.removeAll() + #if !os(Linux) try? KeychainStore.shared.deleteAll() + #endif try? ParseStorage.shared.deleteAll() } diff --git a/Tests/ParseSwiftTests/ParseObjectTests.swift b/Tests/ParseSwiftTests/ParseObjectTests.swift index bbc4b51f1..5726f655b 100644 --- a/Tests/ParseSwiftTests/ParseObjectTests.swift +++ b/Tests/ParseSwiftTests/ParseObjectTests.swift @@ -202,7 +202,9 @@ class ParseObjectTests: XCTestCase { // swiftlint:disable:this type_body_length override func tearDownWithError() throws { super.tearDown() MockURLProtocol.removeAll() + #if !os(Linux) try KeychainStore.shared.deleteAll() + #endif try ParseStorage.shared.deleteAll() guard let fileManager = ParseFileManager(), diff --git a/Tests/ParseSwiftTests/ParsePointerTests.swift b/Tests/ParseSwiftTests/ParsePointerTests.swift index 247881c69..379a13938 100644 --- a/Tests/ParseSwiftTests/ParsePointerTests.swift +++ b/Tests/ParseSwiftTests/ParsePointerTests.swift @@ -44,7 +44,9 @@ class ParsePointerTests: XCTestCase { override func tearDownWithError() throws { try super.tearDownWithError() MockURLProtocol.removeAll() + #if !os(Linux) try KeychainStore.shared.deleteAll() + #endif try ParseStorage.shared.deleteAll() } diff --git a/Tests/ParseSwiftTests/ParseQueryTests.swift b/Tests/ParseSwiftTests/ParseQueryTests.swift index ba94ccf19..5521fefa9 100644 --- a/Tests/ParseSwiftTests/ParseQueryTests.swift +++ b/Tests/ParseSwiftTests/ParseQueryTests.swift @@ -52,7 +52,9 @@ class ParseQueryTests: XCTestCase { // swiftlint:disable:this type_body_length override func tearDown() { super.tearDown() MockURLProtocol.removeAll() + #if !os(Linux) try? KeychainStore.shared.deleteAll() + #endif try? ParseStorage.shared.deleteAll() } diff --git a/Tests/ParseSwiftTests/ParseSessionTests.swift b/Tests/ParseSwiftTests/ParseSessionTests.swift new file mode 100644 index 000000000..b3629dc7d --- /dev/null +++ b/Tests/ParseSwiftTests/ParseSessionTests.swift @@ -0,0 +1,93 @@ +// +// ParseSessionTests.swift +// ParseSwift +// +// Created by Corey Baker on 1/17/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation + +import XCTest +@testable import ParseSwift + +class ParseSessionTests: XCTestCase { + + 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 Session: ParseSession { + + var sessionToken: String + var user: ParseSessionTests.User + var restricted: Bool? + var createdWith: [String: String] + var installationId: String + var expiresAt: Date + + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + init() { + sessionToken = "hello" + user = User() + restricted = false + createdWith = ["yolo": "yaw"] + installationId = "yes" + expiresAt = Date() + } + } + + override func setUpWithError() throws { + try super.setUpWithError() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + + } + + override func tearDownWithError() throws { + try super.tearDownWithError() + MockURLProtocol.removeAll() + #if !os(Linux) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testFetchCommand() throws { + var session = Session() + session.objectId = "me" + do { + let command = try session.fetchCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/classes/_Session/me") + XCTAssertEqual(command.method, API.Method.GET) + XCTAssertNil(command.params) + XCTAssertNil(command.body) + XCTAssertNil(command.data) + } catch { + XCTFail(error.localizedDescription) + } + } +} diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index 10f9d8354..48d048ee4 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -13,6 +13,7 @@ import XCTest class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length struct User: ParseUser { + //: Those are required for Object var objectId: String? var createdAt: Date? @@ -23,12 +24,14 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length 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 @@ -39,6 +42,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length var username: String? var email: String? var password: String? + var authData: [String: [String: String]?]? // Your custom keys var customKey: String? @@ -74,7 +78,9 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length override func tearDown() { super.tearDown() MockURLProtocol.removeAll() + #if !os(Linux) try? KeychainStore.shared.deleteAll() + #endif try? ParseStorage.shared.deleteAll() } @@ -211,13 +217,15 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertEqual(User.current?.updatedAt, fetchedUpdatedAt) XCTAssertEqual(User.current?.customKey, userOnServer.customKey) - //Shold be updated in Keychain + //Should be updated in Keychain + #if !os(Linux) guard let keychainUser: CurrentUserContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) else { XCTFail("Should get object from Keychain") return } XCTAssertEqual(keychainUser.currentUser?.updatedAt, fetchedUpdatedAt) + #endif } catch { XCTFail(error.localizedDescription) @@ -277,13 +285,15 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length //Should be updated in memory XCTAssertEqual(User.current?.updatedAt, fetchedUpdatedAt) - //Shold be updated in Keychain + #if !os(Linux) + //Should be updated in Keychain guard let keychainUser: CurrentUserContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) else { XCTFail("Should get object from Keychain") return } XCTAssertEqual(keychainUser.currentUser?.updatedAt, fetchedUpdatedAt) + #endif case .failure(let error): XCTFail(error.localizedDescription) } @@ -454,13 +464,15 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length //Should be updated in memory XCTAssertEqual(User.current?.updatedAt, fetchedUpdatedAt) - //Shold be updated in Keychain + #if !os(Linux) + //Should be updated in Keychain guard let keychainUser: CurrentUserContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) else { XCTFail("Should get object from Keychain") return } XCTAssertEqual(keychainUser.currentUser?.updatedAt, fetchedUpdatedAt) + #endif } catch { XCTFail(error.localizedDescription) @@ -518,13 +530,16 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length //Should be updated in memory XCTAssertEqual(User.current?.updatedAt, fetchedUpdatedAt) - //Shold be updated in Keychain + #if !os(Linux) + //Should be updated in Keychain guard let keychainUser: CurrentUserContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) else { XCTFail("Should get object from Keychain") return } XCTAssertEqual(keychainUser.currentUser?.updatedAt, fetchedUpdatedAt) + #endif + case .failure(let error): XCTFail(error.localizedDescription) } @@ -698,7 +713,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length func testSignupCommandWithBody() { let body = SignupLoginBody(username: "test", password: "user") - let command = User.signupCommand(username: "test", password: "user") + let command = User.signupCommand(body: body) XCTAssertNotNil(command) XCTAssertEqual(command.path.urlComponent, "/users") XCTAssertEqual(command.method, API.Method.POST) @@ -725,7 +740,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNotNil(signedUp.updatedAt) XCTAssertNotNil(signedUp.email) XCTAssertNotNil(signedUp.username) - XCTAssertNotNil(signedUp.password) + XCTAssertNil(signedUp.password) XCTAssertNotNil(signedUp.objectId) XCTAssertNotNil(signedUp.sessionToken) XCTAssertNotNil(signedUp.customKey) @@ -740,7 +755,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNotNil(userFromKeychain.updatedAt) XCTAssertNotNil(userFromKeychain.email) XCTAssertNotNil(userFromKeychain.username) - XCTAssertNotNil(userFromKeychain.password) + XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) XCTAssertNotNil(userFromKeychain.sessionToken) XCTAssertNil(userFromKeychain.ACL) @@ -762,7 +777,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNotNil(signedUp.updatedAt) XCTAssertNotNil(signedUp.email) XCTAssertNotNil(signedUp.username) - XCTAssertNotNil(signedUp.password) + XCTAssertNil(signedUp.password) XCTAssertNotNil(signedUp.objectId) XCTAssertNotNil(signedUp.sessionToken) XCTAssertNotNil(signedUp.customKey) @@ -777,7 +792,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNotNil(userFromKeychain.updatedAt) XCTAssertNotNil(userFromKeychain.email) XCTAssertNotNil(userFromKeychain.username) - XCTAssertNotNil(userFromKeychain.password) + XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) XCTAssertNotNil(userFromKeychain.sessionToken) XCTAssertNil(userFromKeychain.ACL) @@ -831,7 +846,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNotNil(loggedIn.updatedAt) XCTAssertNotNil(loggedIn.email) XCTAssertNotNil(loggedIn.username) - XCTAssertNotNil(loggedIn.password) + XCTAssertNil(loggedIn.password) XCTAssertNotNil(loggedIn.objectId) XCTAssertNotNil(loggedIn.sessionToken) XCTAssertNotNil(loggedIn.customKey) @@ -846,7 +861,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNotNil(userFromKeychain.updatedAt) XCTAssertNotNil(userFromKeychain.email) XCTAssertNotNil(userFromKeychain.username) - XCTAssertNotNil(userFromKeychain.password) + XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) XCTAssertNotNil(userFromKeychain.sessionToken) XCTAssertNil(userFromKeychain.ACL) @@ -869,7 +884,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNotNil(loggedIn.updatedAt) XCTAssertNotNil(loggedIn.email) XCTAssertNotNil(loggedIn.username) - XCTAssertNotNil(loggedIn.password) + XCTAssertNil(loggedIn.password) XCTAssertNotNil(loggedIn.objectId) XCTAssertNotNil(loggedIn.sessionToken) XCTAssertNotNil(loggedIn.customKey) @@ -884,7 +899,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNotNil(userFromKeychain.updatedAt) XCTAssertNotNil(userFromKeychain.email) XCTAssertNotNil(userFromKeychain.username) - XCTAssertNotNil(userFromKeychain.password) + XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) XCTAssertNotNil(userFromKeychain.sessionToken) XCTAssertNil(userFromKeychain.ACL) @@ -1224,12 +1239,14 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length testLogin() User.current?.customKey = "Changed" User.saveCurrentContainerToKeychain() + #if !os(Linux) guard let keychainUser: CurrentUserContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) else { XCTFail("Should get object from Keychain") return } XCTAssertNil(keychainUser.currentUser?.customKey) + #endif } func testDeleteCommand() { @@ -1380,7 +1397,8 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) - //Shold be updated in Keychain + #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 { @@ -1389,6 +1407,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length return } XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif case .failure(let error): XCTFail("Should have fetched: \(error.localizedDescription)") } @@ -1468,7 +1487,8 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) - //Shold be updated in Keychain + #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 { @@ -1477,6 +1497,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length return } XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif case .failure(let error): XCTFail("Should have fetched: \(error.localizedDescription)") } @@ -1555,7 +1576,8 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) - //Shold be updated in Keychain + #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 { @@ -1564,6 +1586,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length return } XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif case .failure(let error): XCTFail("Should have fetched: \(error.localizedDescription)") } @@ -1643,7 +1666,8 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } XCTAssertEqual(updatedCurrentDate, serverUpdatedAt) - //Shold be updated in Keychain + #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 { @@ -1652,6 +1676,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length return } XCTAssertEqual(keychainUpdatedCurrentDate, serverUpdatedAt) + #endif case .failure(let error): XCTFail("Should have fetched: \(error.localizedDescription)") } @@ -1753,5 +1778,161 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } wait(for: [expectation1], timeout: 20.0) } + + func testMeCommand() { + var user = User() + user.objectId = "me" + do { + let command = try user.meCommand(sessionToken: "yolo") + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/users/me") + XCTAssertEqual(command.method, API.Method.GET) + XCTAssertNil(command.params) + XCTAssertNil(command.body) + XCTAssertNil(command.data) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testBecome() { // swiftlint:disable:this function_body_length + testLogin() + 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" + serverResponse.password = "this" + + 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) + } + + do { + let become = try user.become(sessionToken: "newValue") + XCTAssert(become.hasSameObjectId(as: userOnServer)) + guard let becomeCreatedAt = become.createdAt, + let becomeUpdatedAt = become.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = user.createdAt, + let originalUpdatedAt = user.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(becomeCreatedAt, originalCreatedAt) + XCTAssertGreaterThan(becomeUpdatedAt, originalUpdatedAt) + XCTAssertNil(become.ACL) + + //Should be updated in memory + XCTAssertEqual(User.current?.updatedAt, becomeUpdatedAt) + + //Should be updated in Keychain + #if !os(Linux) + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) else { + XCTFail("Should get object from Keychain") + return + } + XCTAssertEqual(keychainUser.currentUser?.updatedAt, becomeUpdatedAt) + #endif + + } catch { + XCTFail(error.localizedDescription) + } + } + + func testBecomeAsync() { // swiftlint:disable:this function_body_length + XCTAssertNil(User.current?.objectId) + testLogin() + 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" + serverResponse.password = "this" + + 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 expectation1 = XCTestExpectation(description: "Fetch user1") + user.become(sessionToken: "newValue") { result in + + switch result { + case .success(let become): + XCTAssert(become.hasSameObjectId(as: userOnServer)) + guard let becomeCreatedAt = become.createdAt, + let becomeUpdatedAt = become.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = user.createdAt, + let originalUpdatedAt = user.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(becomeCreatedAt, originalCreatedAt) + XCTAssertGreaterThan(becomeUpdatedAt, originalUpdatedAt) + XCTAssertNil(become.ACL) + + //Should be updated in memory + XCTAssertEqual(User.current?.updatedAt, becomeUpdatedAt) + + #if !os(Linux) + //Should be updated in Keychain + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) else { + XCTFail("Should get object from Keychain") + return + } + XCTAssertEqual(keychainUser.currentUser?.updatedAt, becomeUpdatedAt) + #endif + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } } // swiftlint:disable:this file_length