diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ed069d32..d151930dc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.2.1...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ +__New features__ +- Add ParseTwitter and ParseFacebook authentication ([#97](https://github.com/parse-community/Parse-Swift/pull/97)), thanks to [Abdulaziz Alhomaidhi](https://github.com/abs8090). + ### 1.2.1 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.2.0...1.2.1) diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index 11da8acdd..4f8527e9c 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -312,6 +312,26 @@ 7FFF552E2217E72A007C3B4E /* AnyEncodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FFF552B2217E729007C3B4E /* AnyEncodableTests.swift */; }; 7FFF552F2217E72A007C3B4E /* AnyCodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FFF552C2217E729007C3B4E /* AnyCodableTests.swift */; }; 7FFF55302217E72A007C3B4E /* AnyDecodableTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7FFF552D2217E729007C3B4E /* AnyDecodableTests.swift */; }; + 89899CCF2603CE3A002E2043 /* ParseFacebook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899CCE2603CE3A002E2043 /* ParseFacebook.swift */; }; + 89899CD02603CE3A002E2043 /* ParseFacebook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899CCE2603CE3A002E2043 /* ParseFacebook.swift */; }; + 89899CD12603CE3A002E2043 /* ParseFacebook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899CCE2603CE3A002E2043 /* ParseFacebook.swift */; }; + 89899CD22603CE3A002E2043 /* ParseFacebook.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899CCE2603CE3A002E2043 /* ParseFacebook.swift */; }; + 89899D282603CF35002E2043 /* ParseTwitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899CC02603CE2A002E2043 /* ParseTwitter.swift */; }; + 89899D322603CF35002E2043 /* ParseTwitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899CC02603CE2A002E2043 /* ParseTwitter.swift */; }; + 89899D332603CF36002E2043 /* ParseTwitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899CC02603CE2A002E2043 /* ParseTwitter.swift */; }; + 89899D342603CF36002E2043 /* ParseTwitter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899CC02603CE2A002E2043 /* ParseTwitter.swift */; }; + 89899D592603CF3E002E2043 /* ParseTwitterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899CDC2603CE73002E2043 /* ParseTwitterTests.swift */; }; + 89899D632603CF3E002E2043 /* ParseTwitterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899CDC2603CE73002E2043 /* ParseTwitterTests.swift */; }; + 89899D642603CF3F002E2043 /* ParseTwitterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899CDC2603CE73002E2043 /* ParseTwitterTests.swift */; }; + 89899D772603CF66002E2043 /* ParseFacebookTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899CF32603CE9D002E2043 /* ParseFacebookTests.swift */; }; + 89899D812603CF67002E2043 /* ParseFacebookTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899CF32603CE9D002E2043 /* ParseFacebookTests.swift */; }; + 89899D822603CF67002E2043 /* ParseFacebookTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899CF32603CE9D002E2043 /* ParseFacebookTests.swift */; }; + 89899D9F26045998002E2043 /* ParseTwitterCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899D9E26045998002E2043 /* ParseTwitterCombineTests.swift */; }; + 89899DA026045998002E2043 /* ParseTwitterCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899D9E26045998002E2043 /* ParseTwitterCombineTests.swift */; }; + 89899DA126045998002E2043 /* ParseTwitterCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899D9E26045998002E2043 /* ParseTwitterCombineTests.swift */; }; + 89899DB526045DC4002E2043 /* ParseFacebookCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899DB426045DC4002E2043 /* ParseFacebookCombineTests.swift */; }; + 89899DB626045DC4002E2043 /* ParseFacebookCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899DB426045DC4002E2043 /* ParseFacebookCombineTests.swift */; }; + 89899DB726045DC4002E2043 /* ParseFacebookCombineTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 89899DB426045DC4002E2043 /* ParseFacebookCombineTests.swift */; }; 911DB12C24C3F7720027F3C7 /* MockURLResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911DB12B24C3F7720027F3C7 /* MockURLResponse.swift */; }; 911DB12E24C4837E0027F3C7 /* APICommandTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911DB12D24C4837E0027F3C7 /* APICommandTests.swift */; }; 911DB13324C494390027F3C7 /* MockURLProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = 911DB13224C494390027F3C7 /* MockURLProtocol.swift */; }; @@ -614,6 +634,12 @@ 7FFF552B2217E729007C3B4E /* AnyEncodableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyEncodableTests.swift; sourceTree = ""; }; 7FFF552C2217E729007C3B4E /* AnyCodableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyCodableTests.swift; sourceTree = ""; }; 7FFF552D2217E729007C3B4E /* AnyDecodableTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AnyDecodableTests.swift; sourceTree = ""; }; + 89899CC02603CE2A002E2043 /* ParseTwitter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseTwitter.swift; sourceTree = ""; }; + 89899CCE2603CE3A002E2043 /* ParseFacebook.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFacebook.swift; sourceTree = ""; }; + 89899CDC2603CE73002E2043 /* ParseTwitterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseTwitterTests.swift; sourceTree = ""; }; + 89899CF32603CE9D002E2043 /* ParseFacebookTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFacebookTests.swift; sourceTree = ""; }; + 89899D9E26045998002E2043 /* ParseTwitterCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseTwitterCombineTests.swift; sourceTree = ""; }; + 89899DB426045DC4002E2043 /* ParseFacebookCombineTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseFacebookCombineTests.swift; sourceTree = ""; }; 911DB12B24C3F7720027F3C7 /* MockURLResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLResponse.swift; sourceTree = ""; }; 911DB12D24C4837E0027F3C7 /* APICommandTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICommandTests.swift; sourceTree = ""; }; 911DB13224C494390027F3C7 /* MockURLProtocol.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MockURLProtocol.swift; sourceTree = ""; }; @@ -791,6 +817,8 @@ 7044C21F25C5E0160011F6E7 /* ParseConfigCombineTests.swift */, 70D1BE0625BB2BF400A42E7C /* ParseConfigTests.swift */, F971F4F524DE381A006CB79B /* ParseEncoderTests.swift */, + 89899DB426045DC4002E2043 /* ParseFacebookCombineTests.swift */, + 89899CF32603CE9D002E2043 /* ParseFacebookTests.swift */, 7044C1F825C5CFAB0011F6E7 /* ParseFileCombineTests.swift */, 705A99F8259807F900B3547F /* ParseFileManagerTests.swift */, 705727882593FF8000F0ADD5 /* ParseFileTests.swift */, @@ -811,6 +839,8 @@ 70D1BD8625B8C37200A42E7C /* ParseRelationTests.swift */, 7004C22D25B69077005E0AD9 /* ParseRoleTests.swift */, 70C5504525B40D5200B5DBC2 /* ParseSessionTests.swift */, + 89899D9E26045998002E2043 /* ParseTwitterCombineTests.swift */, + 89899CDC2603CE73002E2043 /* ParseTwitterTests.swift */, 7016ED3F25C4A25A00038648 /* ParseUserCombineTests.swift */, 70C7DC1D24D20E530050419B /* ParseUserTests.swift */, 7FFF552A2217E729007C3B4E /* AnyCodableTests */, @@ -984,6 +1014,8 @@ children = ( 707A3C1F25B14BCF000D215C /* ParseApple.swift */, 70386A3725D998D90048EC1B /* ParseLDAP.swift */, + 89899CC02603CE2A002E2043 /* ParseTwitter.swift */, + 89899CCE2603CE3A002E2043 /* ParseFacebook.swift */, ); path = "3rd Party"; sourceTree = ""; @@ -1562,7 +1594,9 @@ 70C550A025B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */, F97B463B24D9C74400F4A88B /* API+Commands.swift in Sources */, F97B464624D9C78B00F4A88B /* ParseOperation.swift in Sources */, + 89899CCF2603CE3A002E2043 /* ParseFacebook.swift in Sources */, 705A9A2F25991C1400B3547F /* Fileable.swift in Sources */, + 89899D342603CF36002E2043 /* ParseTwitter.swift in Sources */, F97B464A24D9C78B00F4A88B /* Delete.swift in Sources */, 70647E8E259E3375004C1004 /* LocallyIdentifiable.swift in Sources */, F97B460624D9C6F200F4A88B /* ParseUser.swift in Sources */, @@ -1621,12 +1655,15 @@ buildActionMask = 2147483647; files = ( 911DB13624C4FC100027F3C7 /* ParseObjectTests.swift in Sources */, + 89899D592603CF3E002E2043 /* ParseTwitterTests.swift in Sources */, 70CE1D892545BF730018D572 /* ParsePointerTests.swift in Sources */, + 89899D772603CF66002E2043 /* ParseFacebookTests.swift in Sources */, 70386A4625D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */, 911DB12E24C4837E0027F3C7 /* APICommandTests.swift in Sources */, 911DB12C24C3F7720027F3C7 /* MockURLResponse.swift in Sources */, 7044C24325C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, 7044C1DF25C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, + 89899D9F26045998002E2043 /* ParseTwitterCombineTests.swift in Sources */, 70C5504625B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, 70110D5C2506ED0E0091CC1D /* ParseInstallationTests.swift in Sources */, 7016ED4025C4A25A00038648 /* ParseUserCombineTests.swift in Sources */, @@ -1650,6 +1687,7 @@ 4AA807701F794C31008CD551 /* KeychainStoreTests.swift in Sources */, 7044C1F925C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */, 70C5502225B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, + 89899DB526045DC4002E2043 /* ParseFacebookCombineTests.swift in Sources */, F971F4F624DE381A006CB79B /* ParseEncoderTests.swift in Sources */, 70C7DC2124D20F190050419B /* ParseQueryTests.swift in Sources */, 7044C22D25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift in Sources */, @@ -1697,7 +1735,9 @@ 70C550A125B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */, F97B463C24D9C74400F4A88B /* API+Commands.swift in Sources */, F97B464724D9C78B00F4A88B /* ParseOperation.swift in Sources */, + 89899CD02603CE3A002E2043 /* ParseFacebook.swift in Sources */, 705A9A3025991C1400B3547F /* Fileable.swift in Sources */, + 89899D332603CF36002E2043 /* ParseTwitter.swift in Sources */, F97B464B24D9C78B00F4A88B /* Delete.swift in Sources */, 70647E8F259E3375004C1004 /* LocallyIdentifiable.swift in Sources */, F97B460724D9C6F200F4A88B /* ParseUser.swift in Sources */, @@ -1765,12 +1805,15 @@ buildActionMask = 2147483647; files = ( 709B98512556ECAA00507778 /* ParseEncoderTests.swift in Sources */, + 89899D642603CF3F002E2043 /* ParseTwitterTests.swift in Sources */, 709B98532556ECAA00507778 /* ParsePointerTests.swift in Sources */, + 89899D822603CF67002E2043 /* ParseFacebookTests.swift in Sources */, 70386A4825D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */, 709B984C2556ECAA00507778 /* APICommandTests.swift in Sources */, 709B984D2556ECAA00507778 /* AnyDecodableTests.swift in Sources */, 7044C24525C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, 7044C1E125C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, + 89899DA126045998002E2043 /* ParseTwitterCombineTests.swift in Sources */, 70C5504825B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, 709B98572556ECAA00507778 /* ParseACLTests.swift in Sources */, 7016ED4225C4A25A00038648 /* ParseUserCombineTests.swift in Sources */, @@ -1794,6 +1837,7 @@ 709B98552556ECAA00507778 /* ParseQueryTests.swift in Sources */, 7044C1FB25C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */, 70C5502425B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, + 89899DB726045DC4002E2043 /* ParseFacebookCombineTests.swift in Sources */, 709B98502556ECAA00507778 /* KeychainStoreTests.swift in Sources */, 709B98562556ECAA00507778 /* ParseObjectTests.swift in Sources */, 7044C22F25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift in Sources */, @@ -1814,12 +1858,15 @@ buildActionMask = 2147483647; files = ( 70F2E2B6254F283000B2EA5C /* ParseACLTests.swift in Sources */, + 89899D632603CF3E002E2043 /* ParseTwitterTests.swift in Sources */, 70F2E2B7254F283000B2EA5C /* ParsePointerTests.swift in Sources */, + 89899D812603CF67002E2043 /* ParseFacebookTests.swift in Sources */, 70386A4725D99C8B0048EC1B /* ParseLDAPTests.swift in Sources */, 70F2E2B5254F283000B2EA5C /* ParseEncoderTests.swift in Sources */, 70F2E2C2254F283000B2EA5C /* APICommandTests.swift in Sources */, 7044C24425C5EA360011F6E7 /* ParseAppleCombineTests.swift in Sources */, 7044C1E025C5C70D0011F6E7 /* ParseObjectCombine.swift in Sources */, + 89899DA026045998002E2043 /* ParseTwitterCombineTests.swift in Sources */, 70C5504725B40D5200B5DBC2 /* ParseSessionTests.swift in Sources */, 70F2E2BC254F283000B2EA5C /* ParseObjectTests.swift in Sources */, 7016ED4125C4A25A00038648 /* ParseUserCombineTests.swift in Sources */, @@ -1843,6 +1890,7 @@ 70F2E2BF254F283000B2EA5C /* MockURLProtocol.swift in Sources */, 7044C1FA25C5CFAB0011F6E7 /* ParseFileCombineTests.swift in Sources */, 70C5502325B3D8F700B5DBC2 /* ParseAppleTests.swift in Sources */, + 89899DB626045DC4002E2043 /* ParseFacebookCombineTests.swift in Sources */, 70F2E2BB254F283000B2EA5C /* ParseGeoPointTests.swift in Sources */, 70F2E2B8254F283000B2EA5C /* AnyEncodableTests.swift in Sources */, 7044C22E25C5E4E90011F6E7 /* ParseAnonymousCombineTests.swift in Sources */, @@ -1890,7 +1938,9 @@ 70C550A325B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */, F97B460D24D9C6F200F4A88B /* Fetchable.swift in Sources */, F97B45ED24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */, + 89899CD22603CE3A002E2043 /* ParseFacebook.swift in Sources */, 705A9A3225991C1400B3547F /* Fileable.swift in Sources */, + 89899D282603CF35002E2043 /* ParseTwitter.swift in Sources */, F97B45F524D9C6F200F4A88B /* Pointer.swift in Sources */, 70647E91259E3375004C1004 /* LocallyIdentifiable.swift in Sources */, F97B460924D9C6F200F4A88B /* ParseUser.swift in Sources */, @@ -1976,7 +2026,9 @@ 70C550A225B4A9F600B5DBC2 /* RemoveRelation.swift in Sources */, F97B460C24D9C6F200F4A88B /* Fetchable.swift in Sources */, F97B45EC24D9C6F200F4A88B /* ParseGeoPoint.swift in Sources */, + 89899CD12603CE3A002E2043 /* ParseFacebook.swift in Sources */, 705A9A3125991C1400B3547F /* Fileable.swift in Sources */, + 89899D322603CF35002E2043 /* ParseTwitter.swift in Sources */, F97B45F424D9C6F200F4A88B /* Pointer.swift in Sources */, 70647E90259E3375004C1004 /* LocallyIdentifiable.swift in Sources */, F97B460824D9C6F200F4A88B /* ParseUser.swift in Sources */, diff --git a/Sources/ParseSwift/Authentication/3rd Party/ParseFacebook.swift b/Sources/ParseSwift/Authentication/3rd Party/ParseFacebook.swift new file mode 100644 index 000000000..4671f19a8 --- /dev/null +++ b/Sources/ParseSwift/Authentication/3rd Party/ParseFacebook.swift @@ -0,0 +1,369 @@ +// +// ParseFacebook.swift +// ParseSwift +// +// Created by Abdulaziz Alhomaidhi on 3/18/21. +// Copyright © 2021 Parse Community. All rights reserved. + +import Foundation +#if canImport(Combine) +import Combine +#endif + +// swiftlint:disable line_length + +/** + Provides utility functions for working with Facebook User Authentication and `ParseUser`'s. + Be sure your Parse Server is configured for [sign in with Facebook](https://docs.parseplatform.org/parse-server/guide/#configuring-parse-server-for-sign-in-with-facebook). + For information on acquiring Facebook sign-in credentials to use with `ParseFacebook`, refer to [Facebook's Documentation](https://developers.facebook.com/docs/facebook-login/limited-login). + */ +public struct ParseFacebook: ParseAuthentication { + + /// Authentication keys required for Facebook authentication. + enum AuthenticationKeys: String, Codable { + case id // swiftlint:disable:this identifier_name + case authenticationToken + case accessToken + case expirationDate + + enum CodingKeys: String, CodingKey { // swiftlint:disable:this nesting + case id // swiftlint:disable:this identifier_name + case token + case accessToken = "access_token" + case expirationDate = "expiration_date" + } + + /// Properly makes an authData dictionary with the required keys. + /// - parameter userId: Required id for the user. + /// - parameter authenticationToken: Required identity token for Facebook limited login. + /// - parameter accessToken: Required identity token for Facebook graph API. + /// - parameter expirationDate: Required expiration data for Facebook login. + /// - returns: authData dictionary. + func makeDictionary(userId: String, + accessToken: String?, + authenticationToken: String?, + expirationDate: Date) -> [String: String] { + + let dateString = DateFormatter.facebookDateFormatter.string(from: expirationDate) + var returnDictionary = [AuthenticationKeys.id.rawValue: userId, + AuthenticationKeys.expirationDate.rawValue: dateString] + + if let accessToken = accessToken { + returnDictionary[AuthenticationKeys.accessToken.rawValue] = accessToken + } else if let authenticationToken = authenticationToken { + returnDictionary[AuthenticationKeys.authenticationToken.rawValue] = authenticationToken + } + return returnDictionary + } + + /// 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 authData[AuthenticationKeys.id.rawValue] != nil, + authData[AuthenticationKeys.expirationDate.rawValue] != nil else { + return false + } + + if authData[AuthenticationKeys.accessToken.rawValue] != nil || + authData[AuthenticationKeys.authenticationToken.rawValue] != nil { + return true + } + return false + } + } + + public static var __type: String { // swiftlint:disable:this identifier_name + "facebook" + } + + public init() { } +} + +// MARK: Login +public extension ParseFacebook { + + /** + Login a `ParseUser` *asynchronously* using Facebook authentication for limited login. + - parameter userId: The `Facebook userId` from `FacebookSDK`. + - parameter authenticationToken: The `authenticationToken` from `FacebookSDK`. + - parameter expirationDate: Required expiration data for Facebook 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. + */ + func login(userId: String, + authenticationToken: String, + expirationDate: Date, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + + let facebookAuthData = AuthenticationKeys.id + .makeDictionary(userId: userId, accessToken: nil, + authenticationToken: authenticationToken, + expirationDate: expirationDate) + login(authData: facebookAuthData, + options: options, + callbackQueue: callbackQueue, + completion: completion) + } + + /** + Login a `ParseUser` *asynchronously* using Facebook authentication for graph API login. + - parameter userId: The `Facebook userId` from `FacebookSDK`. + - parameter accessToken: The `accessToken` from `FacebookSDK`. + - parameter expirationDate: Required expiration data for Facebook 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. + */ + func login(userId: String, + accessToken: String, + expirationDate: Date, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + + let facebookAuthData = AuthenticationKeys.id + .makeDictionary(userId: userId, + accessToken: accessToken, + authenticationToken: nil, + expirationDate: expirationDate) + login(authData: facebookAuthData, + 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) else { + callbackQueue.async { + completion(.failure(.init(code: .unknownError, + message: "Should have authData in consisting of keys \"id\", \"expirationDate\" and \"authenticationToken\" or \"accessToken\"."))) + } + return + } + AuthenticatedUser.login(Self.__type, + authData: authData, + options: options, + callbackQueue: callbackQueue, + completion: completion) + } + + #if canImport(Combine) + /** + Login a `ParseUser` *asynchronously* using Facebook authentication for limited login. Publishes when complete. + - parameter userId: The `userId` from `FacebookSDK`. + - parameter authenticationToken: The `authenticationToken` from `FacebookSDK`. + - parameter expirationDate: Required expiration data for Facebook login. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func loginPublisher(userId: String, + authenticationToken: String, + expirationDate: Date, + options: API.Options = []) -> Future { + Future { promise in + self.login(userId: userId, + authenticationToken: authenticationToken, + expirationDate: expirationDate, + options: options, + completion: promise) + } + } + + /** + Login a `ParseUser` *asynchronously* using Facebook authentication for graph API login. Publishes when complete. + - parameter userId: The `userId` from `FacebookSDK`. + - parameter accessToken: The `accessToken` from `FacebookSDK`. + - parameter expirationDate: Required expiration data for Facebook login. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func loginPublisher(userId: String, + accessToken: String, + expirationDate: Date, + options: API.Options = []) -> Future { + Future { promise in + self.login(userId: userId, + accessToken: accessToken, + expirationDate: expirationDate, + options: options, + completion: promise) + } + } + + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func loginPublisher(authData: [String: String], + options: API.Options = []) -> Future { + Future { promise in + self.login(authData: authData, + options: options, + completion: promise) + } + } + #endif +} + +// MARK: Link +public extension ParseFacebook { + + /** + Link the *current* `ParseUser` *asynchronously* using Facebook authentication for limited login. + - parameter userId: The `userId` from `FacebookSDK`. + - parameter authenticationToken: The `authenticationToken` from `FacebookSDK`. + - parameter expirationDate: Required expiration data for Facebook 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. + */ + func link(userId: String, + authenticationToken: String, + expirationDate: Date, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + let facebookAuthData = AuthenticationKeys.id + .makeDictionary(userId: userId, + accessToken: nil, + authenticationToken: authenticationToken, + expirationDate: expirationDate) + link(authData: facebookAuthData, + options: options, + callbackQueue: callbackQueue, + completion: completion) + } + + /** + Link the *current* `ParseUser` *asynchronously* using Facebook authentication for graph API login. + - parameter userId: The `userId` from `FacebookSDK`. + - parameter accessToken: The `accessToken` from `FacebookSDK`. + - parameter expirationDate: the `expirationDate` from `FacebookSDK`. + - 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(userId: String, + accessToken: String, + expirationDate: Date, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + let facebookAuthData = AuthenticationKeys.id + .makeDictionary(userId: userId, + accessToken: accessToken, + authenticationToken: nil, + expirationDate: expirationDate) + link(authData: facebookAuthData, + 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) else { + callbackQueue.async { + completion(.failure(.init(code: .unknownError, + message: "Should have authData in consisting of keys \"id\", \"expirationDate\" and \"authenticationToken\" or \"accessToken\"."))) + } + return + } + AuthenticatedUser.link(Self.__type, + authData: authData, + options: options, + callbackQueue: callbackQueue, + completion: completion) + } + + #if canImport(Combine) + + /** + Link the *current* `ParseUser` *asynchronously* using Facebook authentication for limited login. Publishes when complete. + - parameter userId: The `userId` from `FacebookSDK`. + - parameter authenticationToken: The `authenticationToken` from `FacebookSDK`. + - parameter expirationDate: the `expirationDate` from `FacebookSDK`. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func linkPublisher(userId: String, + authenticationToken: String, + expirationDate: Date, + options: API.Options = []) -> Future { + Future { promise in + self.link(userId: userId, + authenticationToken: authenticationToken, + expirationDate: expirationDate, + options: options, + completion: promise) + } + } + + /** + Link the *current* `ParseUser` *asynchronously* using Facebook authentication for graph API login. Publishes when complete. + - parameter userId: The `userId` from `FacebookSDK`. + - parameter accessToken: The `accessToken` from `FacebookSDK`. + - parameter expirationDate: the `expirationDate` from `FacebookSDK`. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func linkPublisher(userId: String, + accessToken: String, + expirationDate: Date, + options: API.Options = []) -> Future { + Future { promise in + self.link(userId: userId, + accessToken: accessToken, + expirationDate: expirationDate, + options: options, + completion: promise) + } + } + + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func linkPublisher(authData: [String: String], + options: API.Options = []) -> Future { + Future { promise in + self.link(authData: authData, + options: options, + completion: promise) + } + } + + #endif +} + +// MARK: 3rd Party Authentication - ParseFacebook +public extension ParseUser { + + /// A facebook `ParseUser`. + static var facebook: ParseFacebook { + ParseFacebook() + } + + /// An facebook `ParseUser`. + var facebook: ParseFacebook { + Self.facebook + } +} + +// MARK: Convenience +internal extension DateFormatter { + static let facebookDateFormatter: DateFormatter = { + var dateFormatter = DateFormatter() + dateFormatter.locale = Locale(identifier: "en_US_POSIX") + dateFormatter.timeZone = TimeZone.init(secondsFromGMT: 0) + dateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'" + return dateFormatter + }() +} diff --git a/Sources/ParseSwift/Authentication/3rd Party/ParseLDAP.swift b/Sources/ParseSwift/Authentication/3rd Party/ParseLDAP.swift index 9177f4c88..d17240539 100644 --- a/Sources/ParseSwift/Authentication/3rd Party/ParseLDAP.swift +++ b/Sources/ParseSwift/Authentication/3rd Party/ParseLDAP.swift @@ -80,7 +80,7 @@ public extension ParseLDAP { guard AuthenticationKeys.id.verifyMandatoryKeys(authData: authData) else { callbackQueue.async { completion(.failure(.init(code: .unknownError, - message: "Should have authData in consisting of keys \"id\" and \"token\"."))) + message: "Should have authData in consisting of keys \"id\" and \"password\"."))) } return } @@ -153,7 +153,7 @@ public extension ParseLDAP { completion: @escaping (Result) -> Void) { guard AuthenticationKeys.id.verifyMandatoryKeys(authData: authData) else { let error = ParseError(code: .unknownError, - message: "Should have authData in consisting of keys \"id\" and \"token\".") + message: "Should have authData in consisting of keys \"id\" and \"password\".") callbackQueue.async { completion(.failure(error)) } diff --git a/Sources/ParseSwift/Authentication/3rd Party/ParseTwitter.swift b/Sources/ParseSwift/Authentication/3rd Party/ParseTwitter.swift new file mode 100644 index 000000000..7e7d0a789 --- /dev/null +++ b/Sources/ParseSwift/Authentication/3rd Party/ParseTwitter.swift @@ -0,0 +1,311 @@ +// +// ParseTwitter.swift +// ParseSwift +// +// Created by Abdulaziz Alhomaidhi on 3/17/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation +#if canImport(Combine) +import Combine +#endif + +// swiftlint:disable line_length +// swiftlint:disable function_parameter_count + +/** + Provides utility functions for working with Twitter User Authentication and `ParseUser`'s. + Be sure your Parse Server is configured for [sign in with Twitter](https://docs.parseplatform.org/parse-server/guide/#configuring-parse-server-for-sign-in-with-twitter). + For information on acquiring Twitter sign-in credentials to use with `ParseTwitter`, refer to [Twitter's Documentation](https://developer.twitter.com/en/docs/authentication/guides/log-in-with-twitter). + */ +public struct ParseTwitter: ParseAuthentication { + + /// Authentication keys required for Twitter authentication. + enum AuthenticationKeys: String, Codable { + case id // swiftlint:disable:this identifier_name + case consumerKey + case consumerSecret + case authToken + case authTokenSecret + case screenName + + enum CodingKeys: String, CodingKey { // swiftlint:disable:this nesting + case id // swiftlint:disable:this identifier_name + case consumerKey = "consumer_key" + case consumerSecret = "consumer_secret" + case authToken = "auth_token" + case authTokenSecret = "auth_token_secret" + case screenName = "screen_name" + } + + /// Properly makes an authData dictionary with the required keys. + /// - parameter userId: Required id. + /// - parameter screenName: The `Twitter screenName` from `Twitter`. + /// - parameter consumerKey: The `Twitter consumerKey` from `Twitter`. + /// - parameter consumerSecret: The `Twitter consumerSecret` from `Twitter`. + /// - parameter authToken: Required Twitter authToken obtained from Twitter. + /// - parameter authTokenSecret: Required Twitter authSecretToken obtained from Twitter. + /// - returns: authData dictionary. + func makeDictionary(userId: String, + screenName: String?, + consumerKey: String, + consumerSecret: String, + authToken: String, + authTokenSecret: String) -> [String: String] { + var dictionary = [AuthenticationKeys.id.rawValue: userId, + AuthenticationKeys.consumerKey.rawValue: consumerKey, + AuthenticationKeys.consumerSecret.rawValue: consumerSecret, + AuthenticationKeys.authToken.rawValue: authToken, + AuthenticationKeys.authTokenSecret.rawValue: authTokenSecret] + if let screenName = screenName { + dictionary[AuthenticationKeys.screenName.rawValue] = screenName + } + return dictionary + } + + /// 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 authData[AuthenticationKeys.id.rawValue] != nil, + authData[AuthenticationKeys.consumerKey.rawValue] != nil, + authData[AuthenticationKeys.consumerSecret.rawValue] != nil, + authData[AuthenticationKeys.authToken.rawValue] != nil, + authData[AuthenticationKeys.authTokenSecret.rawValue] != nil else { + return false + } + return true + } + } + public static var __type: String { // swiftlint:disable:this identifier_name + "twitter" + } + public init() { } +} + +// MARK: Login +public extension ParseTwitter { + /** + Login a `ParseUser` *asynchronously* using Twitter authentication. + - parameter userId: The `Twitter userId` from `Twitter`. + - parameter screenName: The `Twitter screenName` from `Twitter`. + - parameter consumerKey: The `Twitter consumerKey` from `Twitter`. + - parameter consumerSecret: The `Twitter consumerSecret` from `Twitter`. + - parameter authToken: The Twitter `authToken` obtained from Twitter. + - parameter authTokenSecret: The Twitter `authSecretToken` obtained from Twitter. + - 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(userId: String, + screenName: String? = nil, + authToken: String, + authTokenSecret: String, + consumerKey: String, + consumerSecret: String, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + + let twitterAuthData = AuthenticationKeys.id + .makeDictionary(userId: userId, + screenName: screenName, + consumerKey: consumerKey, + consumerSecret: consumerSecret, + authToken: authToken, + authTokenSecret: authTokenSecret) + login(authData: twitterAuthData, + 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) else { + callbackQueue.async { + completion(.failure(.init(code: .unknownError, + message: + """ + Should have authData consisting of keys \"id,\" + \"screenName,\" \"consumerKey,\" \"consumerSecret,\" + \"authToken,\" and \"authTokenSecret\". + """))) + } + return + } + AuthenticatedUser.login(Self.__type, + authData: authData, + options: options, + callbackQueue: callbackQueue, + completion: completion) + } + + #if canImport(Combine) + + /** + Login a `ParseUser` *asynchronously* using Twitter authentication. Publishes when complete. + - parameter user: The `userId` from `Twitter`. + - parameter screenName: The `user screenName` from `Twitter`. + - parameter consumerKey: The `consumerKey` from `Twitter`. + - parameter consumerSecret: The `consumerSecret` from `Twitter`. + - parameter authToken: The Twitter `authToken` obtained from Twitter. + - parameter authTokenSecret: The Twitter `authSecretToken` obtained from Twitter. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func loginPublisher(userId: String, + screenName: String? = nil, + consumerKey: String, + consumerSecret: String, + authToken: String, + authTokenSecret: String, + options: API.Options = []) -> Future { + Future { promise in + self.login(userId: userId, + screenName: screenName, + authToken: consumerKey, + authTokenSecret: consumerSecret, + consumerKey: authToken, + consumerSecret: authTokenSecret, + options: options, + completion: promise) + } + } + + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func loginPublisher(authData: [String: String], + options: API.Options = []) -> Future { + Future { promise in + self.login(authData: authData, + options: options, + completion: promise) + } + } + + #endif +} + +// MARK: Link +public extension ParseTwitter { + + /** + Link the *current* `ParseUser` *asynchronously* using Twitter authentication. Publishes when complete. + - parameter user: The `userId` from `Twitter`. + - parameter screenName: The `user screenName` from `Twitter`. + - parameter consumerKey: The `consumerKey` from `Twitter`. + - parameter consumerSecret: The `consumerSecret` from `Twitter`. + - parameter authToken: The Twitter `authToken` obtained from Twitter. + - parameter authTokenSecret: The Twitter `authSecretToken` obtained from Twitter. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + func link(userId: String, + screenName: String? = nil, + consumerKey: String, + consumerSecret: String, + authToken: String, + authTokenSecret: String, + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + let twitterAuthData = AuthenticationKeys.id + .makeDictionary(userId: userId, + screenName: screenName, + consumerKey: consumerKey, + consumerSecret: consumerKey, + authToken: authToken, + authTokenSecret: authTokenSecret) + link(authData: twitterAuthData, + 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) else { + let error = ParseError(code: .unknownError, + message: + """ + Should have authData consisting of keys \"id,\" + \"screenName,\" \"consumerKey,\" \"consumerSecret,\" + \"authToken,\" and \"authTokenSecret\". + """) + callbackQueue.async { + completion(.failure(error)) + } + return + } + AuthenticatedUser.link(Self.__type, + authData: authData, + options: options, + callbackQueue: callbackQueue, + completion: completion) + } + + #if canImport(Combine) + + /** + Link the *current* `ParseUser` *asynchronously* using Twitter authentication. Publishes when complete. + - parameter user: The `user` from `Twitter`. + - parameter screenName: The `user screenName` from `Twitter`. + - parameter consumerKey: The `consumerKey` from `Twitter`. + - parameter consumerSecret: The `consumerSecret` from `Twitter`. + - parameter authToken: The Twitter `authToken` obtained from Twitter. + - parameter authTokenSecret: The Twitter `authSecretToken` obtained from Twitter. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func linkPublisher(userId: String, + screenName: String? = nil, + consumerKey: String, + consumerSecret: String, + authToken: String, + authTokenSecret: String, + options: API.Options = []) -> Future { + Future { promise in + self.link(userId: userId, + screenName: screenName, + consumerKey: consumerKey, + consumerSecret: consumerSecret, + authToken: authToken, + authTokenSecret: authTokenSecret, + options: options, + completion: promise) + } + } + + @available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) + func linkPublisher(authData: [String: String], + options: API.Options = []) -> Future { + Future { promise in + self.link(authData: authData, + options: options, + completion: promise) + } + } + + #endif +} + +// MARK: 3rd Party Authentication - ParseTwitter +public extension ParseUser { + + /// A twitter `ParseUser`. + static var twitter: ParseTwitter { + ParseTwitter() + } + + /// A twitter`ParseUser`. + var twitter: ParseTwitter { + Self.twitter + } +} diff --git a/Tests/ParseSwiftTests/ParseAppleTests.swift b/Tests/ParseSwiftTests/ParseAppleTests.swift index b1cd0e1da..f049362f9 100644 --- a/Tests/ParseSwiftTests/ParseAppleTests.swift +++ b/Tests/ParseSwiftTests/ParseAppleTests.swift @@ -169,6 +169,62 @@ class ParseAppleTests: XCTestCase { wait(for: [expectation1], timeout: 20.0) } + func testLoginAuthData() throws { + var serverResponse = LoginSignupResponse() + guard let tokenData = "this".data(using: .utf8) else { + XCTFail("Couldn't convert token data to string") + return + } + + let authData = try ParseApple + .AuthenticationKeys.id.makeDictionary(user: "testing", + identityToken: tokenData) + 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(authData: authData) { 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) + + //Test stripping + user.apple.strip() + XCTAssertFalse(user.apple.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + func testLoginWrongKeys() throws { _ = try loginNormally() MockURLProtocol.removeAll() @@ -373,6 +429,57 @@ class ParseAppleTests: XCTestCase { wait(for: [expectation1], timeout: 20.0) } + func testLinkLoggedInAuthData() 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") + + guard let tokenData = "this".data(using: .utf8) else { + XCTFail("Couldn't convert token data to string") + return + } + + let authData = try ParseApple + .AuthenticationKeys.id.makeDictionary(user: "testing", + identityToken: tokenData) + + User.apple.link(authData: authData) { 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 testLinkLoggedInUserWrongKeys() throws { _ = try loginNormally() MockURLProtocol.removeAll() diff --git a/Tests/ParseSwiftTests/ParseFacebookCombineTests.swift b/Tests/ParseSwiftTests/ParseFacebookCombineTests.swift new file mode 100644 index 000000000..90976f10f --- /dev/null +++ b/Tests/ParseSwiftTests/ParseFacebookCombineTests.swift @@ -0,0 +1,517 @@ +// +// ParseFacebookCombineTests.swift +// ParseSwift +// +// Created by Abdulaziz Alhomaidhi on 3/19/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseFacebookCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + struct User: ParseUser { + + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + } + + struct LoginSignupResponse: ParseUser { + + var objectId: String? + var createdAt: Date? + var sessionToken: String + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + + // Your custom keys + var customKey: String? + + init() { + let date = Date() + self.createdAt = date + self.updatedAt = date + self.objectId = "yarr" + self.ACL = nil + self.customKey = "blah" + self.sessionToken = "myToken" + self.username = "hello10" + self.email = "hello@parse.com" + } + } + + 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) && !os(Android) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testLimitedLogin() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + let expirationDate = Date() + var serverResponse = LoginSignupResponse() + let authData = ParseAnonymous.AuthenticationKeys.id.makeDictionary() + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.facebook.__type: authData] + serverResponse.createdAt = Date() + serverResponse.updatedAt = serverResponse.createdAt?.addingTimeInterval(+300) + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.facebook.loginPublisher(userId: "testing", authenticationToken: "authenticationToken", + expirationDate: expirationDate) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user, userOnServer) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertTrue(user.facebook.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testGraphAPILogin() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + let expirationDate = Date() + var serverResponse = LoginSignupResponse() + let authData = ParseAnonymous.AuthenticationKeys.id.makeDictionary() + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.facebook.__type: authData] + serverResponse.createdAt = Date() + serverResponse.updatedAt = serverResponse.createdAt?.addingTimeInterval(+300) + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.facebook.loginPublisher(userId: "testing", accessToken: "accessToken", + expirationDate: expirationDate) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user, userOnServer) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertTrue(user.facebook.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testLoginAuthData() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + let expirationDate = Date() + var serverResponse = LoginSignupResponse() + let authData = ParseAnonymous.AuthenticationKeys.id.makeDictionary() + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.facebook.__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 faceookAuthData = ParseFacebook + .AuthenticationKeys.id.makeDictionary(userId: "testing", + accessToken: "accessToken", + authenticationToken: nil, + expirationDate: expirationDate) + + let publisher = User.facebook.loginPublisher(authData: faceookAuthData) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user, userOnServer) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertTrue(user.facebook.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func loginNormally() throws -> User { + let loginResponse = LoginSignupResponse() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + return try User.login(username: "parse", password: "user") + } + + func testLinkLimitedLogin() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + let expirationDate = Date() + _ = try loginNormally() + MockURLProtocol.removeAll() + + var serverResponse = LoginSignupResponse() + serverResponse.updatedAt = Date() + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.facebook.linkPublisher(userId: "testing", authenticationToken: "authenticationToken", + expirationDate: expirationDate) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user.updatedAt, userOnServer.updatedAt) + XCTAssertEqual(user.username, "parse") + XCTAssertNil(user.password) + XCTAssertTrue(user.facebook.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testLinkGraphAPILogin() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + let expirationDate = Date() + _ = try loginNormally() + MockURLProtocol.removeAll() + + var serverResponse = LoginSignupResponse() + serverResponse.updatedAt = Date() + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.facebook.linkPublisher(userId: "testing", + accessToken: "accessToken", + expirationDate: expirationDate) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user.updatedAt, userOnServer.updatedAt) + XCTAssertEqual(user.username, "parse") + XCTAssertNil(user.password) + XCTAssertTrue(user.facebook.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testLinkAuthData() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + let expirationDate = Date() + _ = 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 authData = ParseFacebook + .AuthenticationKeys.id.makeDictionary(userId: "testing", + accessToken: "accessToken", + authenticationToken: nil, + expirationDate: expirationDate) + + let publisher = User.facebook.linkPublisher(authData: authData) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user.updatedAt, userOnServer.updatedAt) + XCTAssertEqual(user.username, "parse") + XCTAssertNil(user.password) + XCTAssertTrue(user.facebook.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testUnlinkLimitedLogin() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + let expirationDate = Date() + _ = try loginNormally() + MockURLProtocol.removeAll() + + let authData = ParseFacebook + .AuthenticationKeys.id.makeDictionary(userId: "testing", + accessToken: nil, + authenticationToken: "authenticationToken", + expirationDate: expirationDate) + User.current?.authData = [User.facebook.__type: authData] + XCTAssertTrue(User.facebook.isLinked) + + var serverResponse = LoginSignupResponse() + serverResponse.updatedAt = Date() + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.facebook.unlinkPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user.updatedAt, userOnServer.updatedAt) + XCTAssertEqual(user.username, "parse") + XCTAssertNil(user.password) + XCTAssertFalse(user.facebook.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testUnlinkGraphAPILogin() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + let expirationDate = Date() + _ = try loginNormally() + MockURLProtocol.removeAll() + + let authData = ParseFacebook + .AuthenticationKeys.id.makeDictionary(userId: "testing", + accessToken: "accessToken", + authenticationToken: nil, + expirationDate: expirationDate) + User.current?.authData = [User.facebook.__type: authData] + XCTAssertTrue(User.facebook.isLinked) + + var serverResponse = LoginSignupResponse() + serverResponse.updatedAt = Date() + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.facebook.unlinkPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user.updatedAt, userOnServer.updatedAt) + XCTAssertEqual(user.username, "parse") + XCTAssertNil(user.password) + XCTAssertFalse(user.facebook.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseFacebookTests.swift b/Tests/ParseSwiftTests/ParseFacebookTests.swift new file mode 100644 index 000000000..317f39556 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseFacebookTests.swift @@ -0,0 +1,799 @@ +// +// ParseFacebookTests.swift +// ParseSwift +// +// Created by Abdulaziz Alhomaidhi on 3/18/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation +import XCTest +@testable import ParseSwift + +class ParseFacebookTests: 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() { + let date = Date() + self.createdAt = date + self.updatedAt = date + self.objectId = "yarr" + self.ACL = nil + self.customKey = "blah" + self.sessionToken = "myToken" + self.username = "hello10" + self.email = "hello@parse.com" + } + } + + 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) && !os(Android) + 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 testAuthenticationKeysLimitedLogin() throws { + let expirationDate = Date() + let authData = ParseFacebook + .AuthenticationKeys.id.makeDictionary(userId: "testing", + accessToken: nil, + authenticationToken: "authenticationToken", + expirationDate: expirationDate) + let dateString = DateFormatter + .facebookDateFormatter + .string(from: expirationDate) + XCTAssertEqual(authData, ["id": "testing", + "authenticationToken": "authenticationToken", + "expirationDate": dateString]) + } + + func testVerifyMandatoryKeys() throws { + let dateString = DateFormatter.facebookDateFormatter.string(from: Date()) + let authData = ["id": "testing", + "authenticationToken": "authenticationToken", + "expirationDate": dateString] + let authDataWrong = ["id": "testing", "hello": "test"] + XCTAssertTrue(ParseFacebook + .AuthenticationKeys.id.verifyMandatoryKeys(authData: authData)) + XCTAssertFalse(ParseFacebook + .AuthenticationKeys.id.verifyMandatoryKeys(authData: authDataWrong)) + } + + func testAuthenticationKeysGraphAPILogin() throws { + let expirationDate = Date() + let authData = ParseFacebook + .AuthenticationKeys.id.makeDictionary(userId: "testing", + accessToken: "accessToken", + authenticationToken: nil, + expirationDate: expirationDate) + let dateString = DateFormatter.facebookDateFormatter.string(from: expirationDate) + XCTAssertEqual(authData, ["id": "testing", "accessToken": "accessToken", "expirationDate": dateString]) + } + + func testLimitedLogin() throws { + var serverResponse = LoginSignupResponse() + let expirationDate = Date() + let authData = ParseFacebook + .AuthenticationKeys.id.makeDictionary(userId: "testing", + accessToken: nil, + authenticationToken: "authenticationToken", + expirationDate: expirationDate) + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.facebook.__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.facebook.login(userId: "testing", authenticationToken: "authenticationToken", + expirationDate: expirationDate) { 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.facebook.isLinked) + + //Test stripping + user.facebook.strip() + XCTAssertFalse(user.facebook.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testGraphAPILogin() throws { + var serverResponse = LoginSignupResponse() + let expirationDate = Date() + let authData = ParseFacebook + .AuthenticationKeys.id.makeDictionary(userId: "testing", + accessToken: "accessToken", + authenticationToken: nil, + expirationDate: expirationDate) + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.facebook.__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.facebook.login(userId: "testing", accessToken: "accessToken", expirationDate: expirationDate) { 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.facebook.isLinked) + + //Test stripping + user.facebook.strip() + XCTAssertFalse(user.facebook.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testLoginAuthData() throws { + var serverResponse = LoginSignupResponse() + let expirationDate = Date() + let authData = ParseFacebook + .AuthenticationKeys.id.makeDictionary(userId: "testing", + accessToken: nil, + authenticationToken: "authenticationToken", + expirationDate: expirationDate) + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.facebook.__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.facebook.login(authData: authData) { 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.facebook.isLinked) + + //Test stripping + user.facebook.strip() + XCTAssertFalse(user.facebook.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testLoginWrongKeys() throws { + _ = try loginNormally() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Login") + let currentDate = DateFormatter.facebookDateFormatter.string(from: Date()) + let authData = ["id": "hello", + "expirationDate": currentDate] + User.facebook.login(authData: authData) { result in + + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("consisting of keys")) + } else { + XCTFail("Should have returned error") + } + 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 testReplaceAnonymousWithFacebookLimitedLogin() throws { + try loginAnonymousUser() + MockURLProtocol.removeAll() + let expirationDate = Date() + + let authData = ParseFacebook + .AuthenticationKeys.id.makeDictionary(userId: "testing", + accessToken: nil, + authenticationToken: "authenticationToken", + expirationDate: expirationDate) + + var serverResponse = LoginSignupResponse() + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.facebook.__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.facebook.login(userId: "testing", authenticationToken: "authenticationToken", + expirationDate: expirationDate ) { 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.facebook.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testReplaceAnonymousWithFacebookGraphAPILogin() throws { + try loginAnonymousUser() + MockURLProtocol.removeAll() + let expirationDate = Date() + + let authData = ParseFacebook + .AuthenticationKeys.id.makeDictionary(userId: "testing", + accessToken: "this", + authenticationToken: nil, + expirationDate: expirationDate) + + var serverResponse = LoginSignupResponse() + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.facebook.__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.facebook.login(userId: "testing", accessToken: "accessToken", expirationDate: expirationDate ) { 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.facebook.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testReplaceAnonymousWithLinkedFacebookLimitedLogin() throws { + try loginAnonymousUser() + MockURLProtocol.removeAll() + var serverResponse = LoginSignupResponse() + serverResponse.updatedAt = Date() + let expirationDate = 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.facebook.link(userId: "testing", authenticationToken: "authenticationToken", + expirationDate: expirationDate) { 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.facebook.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testReplaceAnonymousWithLinkedFacebookGraphAPILogin() throws { + try loginAnonymousUser() + MockURLProtocol.removeAll() + var serverResponse = LoginSignupResponse() + serverResponse.updatedAt = Date() + let expirationDate = 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.facebook.link(userId: "testing", accessToken: "acceeToken", expirationDate: expirationDate) { 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.facebook.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testLinkLoggedInUserWithFacebookLimitedLogin() throws { + _ = try loginNormally() + MockURLProtocol.removeAll() + let expirationDate = Date() + 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.facebook.link(userId: "testing", authenticationToken: "authenticationToken", + expirationDate: expirationDate) { 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.facebook.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testLinkLoggedInUserWithFacebookGraphAPILogin() throws { + _ = try loginNormally() + MockURLProtocol.removeAll() + let expirationDate = Date() + 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.facebook.link(userId: "testing", accessToken: "accessToken", expirationDate: expirationDate) { 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.facebook.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testLinkLoggedInAuthData() throws { + _ = try loginNormally() + MockURLProtocol.removeAll() + let expirationDate = Date() + 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") + + let authData = ParseFacebook + .AuthenticationKeys.id.makeDictionary(userId: "testing", + accessToken: nil, + authenticationToken: "authenticationToken", + expirationDate: expirationDate) + + User.facebook.link(authData: authData) { 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.facebook.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testLinkWrongKeys() throws { + _ = try loginNormally() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Login") + let currentDate = DateFormatter.facebookDateFormatter.string(from: Date()) + let authData = ["id": "hello", + "expirationDate": currentDate] + User.facebook.link(authData: authData) { result in + + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("consisting of keys")) + } else { + XCTFail("Should have returned error") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testUnlinkLimitedLogin() throws { + _ = try loginNormally() + MockURLProtocol.removeAll() + let expirationDate = Date() + + let authData = ParseFacebook + .AuthenticationKeys.id.makeDictionary(userId: "testing", + accessToken: nil, + authenticationToken: "authenticationToken", + expirationDate: expirationDate) + User.current?.authData = [User.facebook.__type: authData] + XCTAssertTrue(User.facebook.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.facebook.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.facebook.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testUnlinkGraphAPILogin() throws { + _ = try loginNormally() + MockURLProtocol.removeAll() + let expirationDate = Date() + + let authData = ParseFacebook + .AuthenticationKeys.id.makeDictionary(userId: "testing", + accessToken: "accessToken", + authenticationToken: nil, + expirationDate: expirationDate) + User.current?.authData = [User.facebook.__type: authData] + XCTAssertTrue(User.facebook.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.facebook.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.facebook.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } +} diff --git a/Tests/ParseSwiftTests/ParseTwitterCombineTests.swift b/Tests/ParseSwiftTests/ParseTwitterCombineTests.swift new file mode 100644 index 000000000..f14ba1ec2 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseTwitterCombineTests.swift @@ -0,0 +1,373 @@ +// +// ParseTwitterCombineTests.swift +// ParseSwift +// +// Created by Abdulaziz Alhomaidhi on 3/19/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +#if canImport(Combine) + +import Foundation +import XCTest +import Combine +@testable import ParseSwift + +@available(macOS 10.15, iOS 13.0, macCatalyst 13.0, watchOS 6.0, tvOS 13.0, *) +class ParseTwitterCombineTests: XCTestCase { // swiftlint:disable:this type_body_length + + struct User: ParseUser { + + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + } + + struct LoginSignupResponse: ParseUser { + + var objectId: String? + var createdAt: Date? + var sessionToken: String + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + + // Your custom keys + var customKey: String? + + init() { + let date = Date() + self.createdAt = date + self.updatedAt = date + self.objectId = "yarr" + self.ACL = nil + self.customKey = "blah" + self.sessionToken = "myToken" + self.username = "hello10" + self.email = "hello@parse.com" + } + } + + 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) && !os(Android) + try KeychainStore.shared.deleteAll() + #endif + try ParseStorage.shared.deleteAll() + } + + func testLogin() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + var serverResponse = LoginSignupResponse() + let authData = ParseAnonymous.AuthenticationKeys.id.makeDictionary() + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.twitter.__type: authData] + serverResponse.createdAt = Date() + serverResponse.updatedAt = serverResponse.createdAt?.addingTimeInterval(+300) + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.twitter.loginPublisher(userId: "testing", screenName: "screenName", + consumerKey: "consumerKey", consumerSecret: "consumerSecret", + authToken: "tokenData", authTokenSecret: "authTokenSecret") + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user, userOnServer) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertTrue(user.twitter.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testLoginAuthData() { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + var serverResponse = LoginSignupResponse() + let authData = ParseAnonymous.AuthenticationKeys.id.makeDictionary() + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.twitter.__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 twitterAuthData = ParseTwitter + .AuthenticationKeys.id.makeDictionary(userId: "testing", + screenName: "screenName", + consumerKey: "consumerKey", + consumerSecret: "consumerSecret", + authToken: "authToken", + authTokenSecret: "authTokenSecret") + + let publisher = User.twitter.loginPublisher(authData: twitterAuthData) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user, userOnServer) + XCTAssertEqual(user.username, "hello") + XCTAssertEqual(user.password, "world") + XCTAssertTrue(user.twitter.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func loginNormally() throws -> User { + let loginResponse = LoginSignupResponse() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + return try User.login(username: "parse", password: "user") + } + + func testLink() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + _ = try loginNormally() + MockURLProtocol.removeAll() + + var serverResponse = LoginSignupResponse() + serverResponse.updatedAt = Date() + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.twitter.linkPublisher(userId: "testing", + screenName: "screenName", + consumerKey: "consumerKey", + consumerSecret: "consumerSecret", + authToken: "tokenData", + authTokenSecret: "authTokenSecret") + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user.updatedAt, userOnServer.updatedAt) + XCTAssertEqual(user.username, "parse") + XCTAssertNil(user.password) + XCTAssertTrue(user.twitter.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testLinkAuthData() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + _ = try loginNormally() + MockURLProtocol.removeAll() + + var serverResponse = LoginSignupResponse() + serverResponse.updatedAt = Date() + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let twitterAuthData = ParseTwitter + .AuthenticationKeys.id.makeDictionary(userId: "testing", + screenName: "screenName", + consumerKey: "consumerKey", + consumerSecret: "consumerSecret", + authToken: "authToken", + authTokenSecret: "authTokenSecret") + let publisher = User.twitter.linkPublisher(authData: twitterAuthData) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user.updatedAt, userOnServer.updatedAt) + XCTAssertEqual(user.username, "parse") + XCTAssertNil(user.password) + XCTAssertTrue(user.twitter.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + + func testUnlink() throws { + var subscriptions = Set() + let expectation1 = XCTestExpectation(description: "Save") + + _ = try loginNormally() + MockURLProtocol.removeAll() + + let authData = ParseTwitter + .AuthenticationKeys.id.makeDictionary(userId: "testing", + screenName: "screenName", + consumerKey: "consumerKey", + consumerSecret: "consumerSecret", + authToken: "tokenData", + authTokenSecret: "authTokenSecret") + User.current?.authData = [User.twitter.__type: authData] + XCTAssertTrue(User.twitter.isLinked) + + var serverResponse = LoginSignupResponse() + serverResponse.updatedAt = Date() + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let publisher = User.twitter.unlinkPublisher() + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + + XCTAssertEqual(user, User.current) + XCTAssertEqual(user.updatedAt, userOnServer.updatedAt) + XCTAssertEqual(user.username, "parse") + XCTAssertNil(user.password) + XCTAssertFalse(user.twitter.isLinked) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } +} + +#endif diff --git a/Tests/ParseSwiftTests/ParseTwitterTests.swift b/Tests/ParseSwiftTests/ParseTwitterTests.swift new file mode 100644 index 000000000..305e09585 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseTwitterTests.swift @@ -0,0 +1,561 @@ +// +// ParseTwitterTests.swift +// ParseSwift +// +// Created by Abdulaziz Alhomaidhi on 3/17/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation +import XCTest +@testable import ParseSwift + +class ParseTwitterTests: 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() { + let date = Date() + self.createdAt = date + self.updatedAt = date + self.objectId = "yarr" + self.ACL = nil + self.customKey = "blah" + self.sessionToken = "myToken" + self.username = "hello10" + self.email = "hello@parse.com" + } + } + + 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) && !os(Android) + 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 = ParseTwitter + .AuthenticationKeys.id.makeDictionary(userId: "testing", + screenName: "screenName", + consumerKey: "consumerKey", + consumerSecret: "consumerSecret", + authToken: "authToken", + authTokenSecret: "authTokenSecret") + XCTAssertEqual(authData, ["id": "testing", + "screenName": "screenName", + "consumerKey": "consumerKey", + "consumerSecret": "consumerSecret", + "authToken": "authToken", + "authTokenSecret": "authTokenSecret"]) + } + + func testVerifyMandatoryKeys() throws { + let authData = ["id": "testing", + "screenName": "screenName", + "consumerKey": "consumerKey", + "consumerSecret": "consumerSecret", + "authToken": "authToken", + "authTokenSecret": "authTokenSecret"] + let authDataWrong = ["id": "testing", + "screenName": "screenName", + "consumerKey": "consumerKey", + "consumerSecret": "consumerSecret", + "authToken": "authToken", + "hello": "authTokenSecret"] + XCTAssertTrue(ParseTwitter + .AuthenticationKeys.id.verifyMandatoryKeys(authData: authData)) + XCTAssertFalse(ParseTwitter + .AuthenticationKeys.id.verifyMandatoryKeys(authData: authDataWrong)) + } + + func testLogin() throws { + var serverResponse = LoginSignupResponse() + + let authData = ParseTwitter + .AuthenticationKeys.id.makeDictionary(userId: "testing", + screenName: "screenName", + consumerKey: "consumerKey", + consumerSecret: "consumerSecret", + authToken: "authToken", + authTokenSecret: "authTokenSecret") + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.twitter.__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.twitter.login(userId: "testing", screenName: "screenName", + authToken: "consumerKey", authTokenSecret: "consumerSecret", + consumerKey: "this", consumerSecret: "authTokenSecret") { 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.twitter.isLinked) + + //Test stripping + user.twitter.strip() + XCTAssertFalse(user.twitter.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testLoginAuthData() throws { + var serverResponse = LoginSignupResponse() + + let authData = ParseTwitter + .AuthenticationKeys.id.makeDictionary(userId: "testing", + screenName: "screenName", + consumerKey: "consumerKey", + consumerSecret: "consumerSecret", + authToken: "authToken", + authTokenSecret: "authTokenSecret") + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.twitter.__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.twitter.login(authData: authData) { 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.twitter.isLinked) + + //Test stripping + user.twitter.strip() + XCTAssertFalse(user.twitter.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testLoginWrongKeys() throws { + _ = try loginNormally() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Login") + + User.twitter.login(authData: ["hello": "world"]) { result in + + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("consisting of keys")) + } else { + XCTFail("Should have returned error") + } + 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 testReplaceAnonymousWithTwitter() throws { + try loginAnonymousUser() + MockURLProtocol.removeAll() + + let authData = ParseTwitter + .AuthenticationKeys.id.makeDictionary(userId: "testing", + screenName: "screenName", + consumerKey: "consumerSecret", + consumerSecret: "consumerSecret", + authToken: "this", + authTokenSecret: "authTokenSecret") + + var serverResponse = LoginSignupResponse() + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.twitter.__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.twitter.login(userId: "testing", screenName: "screenName", + authToken: "this", authTokenSecret: "authTokenSecret", + consumerKey: "consumerKey", consumerSecret: "consumerSecret") { 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.twitter.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testReplaceAnonymousWithLinkedTwitter() 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.twitter.link(userId: "testing", screenName: "screenName", + consumerKey: "consumerKey", consumerSecret: "consumerSecret", + authToken: "this", authTokenSecret: "authTokenSecret") { 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.twitter.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testLinkLoggedInUserWithTwitter() 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.twitter.link(userId: "testing", screenName: "screenName", + consumerKey: "consumerKey", consumerSecret: "consumerSecret", + authToken: "this", authTokenSecret: "authTokenSecret") { 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.twitter.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testLinkLoggedInAuthData() 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") + + let authData = ParseTwitter + .AuthenticationKeys.id.makeDictionary(userId: "testing", + screenName: "screenName", + consumerKey: "consumerKey", + consumerSecret: "consumerSecret", + authToken: "authToken", + authTokenSecret: "authTokenSecret") + User.twitter.link(authData: authData) { 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.twitter.isLinked) + XCTAssertFalse(user.anonymous.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testLinkWrongKeys() throws { + _ = try loginNormally() + MockURLProtocol.removeAll() + + let expectation1 = XCTestExpectation(description: "Login") + + User.twitter.link(authData: ["hello": "world"]) { result in + + if case let .failure(error) = result { + XCTAssertTrue(error.message.contains("consisting of keys")) + } else { + XCTFail("Should have returned error") + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testUnlink() throws { + _ = try loginNormally() + MockURLProtocol.removeAll() + + let authData = ParseTwitter + .AuthenticationKeys.id.makeDictionary(userId: "testing", + screenName: "screenNAme", + consumerKey: "consumerKey", + consumerSecret: "consumerSecret", + authToken: "this", + authTokenSecret: "authTokenSecret") + User.current?.authData = [User.twitter.__type: authData] + XCTAssertTrue(User.twitter.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.twitter.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.twitter.isLinked) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } +}