diff --git a/CHANGELOG.md b/CHANGELOG.md index 63a155025..32bbc0b7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.2.5...main) * _Contributing to this repo? Add info about your change here to be included in the next release_ +__New features__ +- Add OAuth 2.0 support ([#91](https://github.com/parse-community/Parse-Swift/pull/91)), thanks to [Corey Baker](https://github.com/cbaker6). + ### 1.2.5 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.2.4...1.2.5) diff --git a/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift index b1742bc1e..028918c37 100644 --- a/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift @@ -48,6 +48,12 @@ User.signup(username: "hello", password: "world") { results in assertionFailure("Error: these two objects should match") } else { print("Successfully signed up user \(user)") + print("The users' sessionToken is: \(currentUser.sessionToken)") + if let accessToken = currentUser.accessToken { + print("The users' accessToken is: \(accessToken)") + print("The users' refreshToken is: \(currentUser.refreshToken!)") + print("The users' token expires at: \(currentUser.expiresAt!)") + } } case .failure(let error): diff --git a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift index 210db700b..83bc9006f 100644 --- a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift @@ -53,6 +53,31 @@ struct GameScore: ParseObject { } } +//: If signed in using OAuth2.0, ask the server to refresh the token +if let currentUser = User.current, + let accessToken = currentUser.accessToken { + print("The current sessionToken: \(currentUser.expiresAt)") + print("The current accessToken is: \(accessToken)") + print("The current refreshToken is: \(currentUser.refreshToken)") + print("The current token expires at: \(currentUser.expiresAt)") + User.refresh { results in + + switch results { + case .success(let updatedUser): + print("Successfully refreshed users tokens") + if let updatedUser = User.current, + let accessToken = updatedUser.accessToken { + print("The new sessionToken: \(updatedUser.sessionToken)") + print("The new accessToken: \(updatedUser.accessToken)") + print("The new refreshToken is: \(updatedUser.refreshToken)") + print("The token expires at: \(updatedUser.expiresAt)") + } + case .failure(let error): + print("Failed to update user: \(error)") + } + } +} + //: Logging out - synchronously do { try User.logout() diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index c32f07f76..3907d2a3e 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -212,6 +212,9 @@ 707A3C2125B14BD0000D215C /* ParseApple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3C1F25B14BCF000D215C /* ParseApple.swift */; }; 707A3C2225B14BD0000D215C /* ParseApple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3C1F25B14BCF000D215C /* ParseApple.swift */; }; 707A3C2325B14BD0000D215C /* ParseApple.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707A3C1F25B14BCF000D215C /* ParseApple.swift */; }; + 707CCE9C25FBD4D0003C3B64 /* ParseUserOAuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707CCE9B25FBD4D0003C3B64 /* ParseUserOAuthTests.swift */; }; + 707CCE9D25FBD4D0003C3B64 /* ParseUserOAuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707CCE9B25FBD4D0003C3B64 /* ParseUserOAuthTests.swift */; }; + 707CCE9E25FBD4D0003C3B64 /* ParseUserOAuthTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 707CCE9B25FBD4D0003C3B64 /* ParseUserOAuthTests.swift */; }; 708D035225215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; }; 708D035325215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; }; 708D035425215F9B00646C70 /* Deletable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 708D035125215F9B00646C70 /* Deletable.swift */; }; @@ -606,6 +609,7 @@ 707A3BF025B0A4F0000D215C /* ParseAuthentication.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAuthentication.swift; sourceTree = ""; }; 707A3C1025B0A8E8000D215C /* ParseAnonymous.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseAnonymous.swift; sourceTree = ""; }; 707A3C1F25B14BCF000D215C /* ParseApple.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseApple.swift; sourceTree = ""; }; + 707CCE9B25FBD4D0003C3B64 /* ParseUserOAuthTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ParseUserOAuthTests.swift; sourceTree = ""; }; 708D035125215F9B00646C70 /* Deletable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Deletable.swift; sourceTree = ""; }; 709B98302556EC7400507778 /* ParseSwiftTeststvOS.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ParseSwiftTeststvOS.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 709B98342556EC7400507778 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; @@ -848,6 +852,7 @@ 89899CDC2603CE73002E2043 /* ParseTwitterTests.swift */, 7016ED3F25C4A25A00038648 /* ParseUserCombineTests.swift */, 70C7DC1D24D20E530050419B /* ParseUserTests.swift */, + 707CCE9B25FBD4D0003C3B64 /* ParseUserOAuthTests.swift */, 7FFF552A2217E729007C3B4E /* AnyCodableTests */, 911DB12A24C3F7260027F3C7 /* NetworkMocking */, ); @@ -1702,6 +1707,7 @@ 70A2D86B25B3ADB6001BEB7D /* ParseAnonymousTests.swift in Sources */, 7044C1EC25C5CC930011F6E7 /* ParseOperationCombineTests.swift in Sources */, 70C7DC1E24D20E530050419B /* ParseUserTests.swift in Sources */, + 707CCE9C25FBD4D0003C3B64 /* ParseUserOAuthTests.swift in Sources */, 70A2D81F25B36A7D001BEB7D /* ParseAuthenticationTests.swift in Sources */, 70D1BE4B25BB312700A42E7C /* ParseConfigTests.swift in Sources */, 911DB13324C494390027F3C7 /* MockURLProtocol.swift in Sources */, @@ -1853,6 +1859,7 @@ 70A2D86D25B3ADB6001BEB7D /* ParseAnonymousTests.swift in Sources */, 7044C1EE25C5CC930011F6E7 /* ParseOperationCombineTests.swift in Sources */, 709B98582556ECAA00507778 /* AnyEncodableTests.swift in Sources */, + 707CCE9E25FBD4D0003C3B64 /* ParseUserOAuthTests.swift in Sources */, 70A2D82125B36A7D001BEB7D /* ParseAuthenticationTests.swift in Sources */, 70D1BE5F25BB312A00A42E7C /* ParseConfigTests.swift in Sources */, 709B98542556ECAA00507778 /* ParseInstallationTests.swift in Sources */, @@ -1907,6 +1914,7 @@ 70A2D86C25B3ADB6001BEB7D /* ParseAnonymousTests.swift in Sources */, 7044C1ED25C5CC930011F6E7 /* ParseOperationCombineTests.swift in Sources */, 70F2E2BA254F283000B2EA5C /* ParseInstallationTests.swift in Sources */, + 707CCE9D25FBD4D0003C3B64 /* ParseUserOAuthTests.swift in Sources */, 70A2D82025B36A7D001BEB7D /* ParseAuthenticationTests.swift in Sources */, 70D1BE5525BB312900A42E7C /* ParseConfigTests.swift in Sources */, 70F2E2B9254F283000B2EA5C /* KeychainStoreTests.swift in Sources */, diff --git a/Sources/ParseSwift/API/API.swift b/Sources/ParseSwift/API/API.swift index 13f434362..d46b9e3b2 100644 --- a/Sources/ParseSwift/API/API.swift +++ b/Sources/ParseSwift/API/API.swift @@ -21,6 +21,7 @@ public struct API { case object(className: String, objectId: String) case users case user(objectId: String) + case refresh case installations case installation(objectId: String) case sessions @@ -29,6 +30,7 @@ public struct API { case role(objectId: String) case login case logout + case revoke case file(fileName: String) case passwordReset case verificationEmail @@ -50,6 +52,8 @@ public struct API { return "/users" case .user(let objectId): return "/users/\(objectId)" + case .refresh: + return "/users/refresh" case .installations: return "/installations" case .installation(let objectId): @@ -66,6 +70,8 @@ public struct API { return "/login" case .logout: return "/logout" + case .revoke: + return "/revoke" case .file(let fileName): return "/files/\(fileName)" case .passwordReset: @@ -117,6 +123,9 @@ public struct API { /// Specify tags. /// - note: This is typically used indirectly by `ParseFile`. case tags([String: String]) + /// Remove mimeType. + /// - note: This is typically used indirectly by `ParseUser` OAuth20. + case removeAccessToken public func hash(into hasher: inout Hasher) { switch self { @@ -136,6 +145,8 @@ public struct API { hasher.combine(7) case .tags: hasher.combine(8) + case .removeAccessToken: + hasher.combine(9) } } } @@ -148,8 +159,10 @@ public struct API { headers["X-Parse-Client-Key"] = clientKey } - if let token = BaseParseUser.currentUserContainer?.sessionToken { - headers["X-Parse-Session-Token"] = token + if let accessToken = BaseParseUser.currentUserContainer?.accessToken { + headers["X-Parse-Session-Token"] = accessToken + } else if let sessioinToken = BaseParseUser.currentUserContainer?.sessionToken { + headers["X-Parse-Session-Token"] = sessioinToken } if let installationId = BaseParseInstallation.currentInstallationContainer.installationId { @@ -180,6 +193,8 @@ public struct API { tags.forEach {(key, value) -> Void in headers[key] = value } + case .removeAccessToken: + headers.removeValue(forKey: "X-Parse-Session-Token") } } diff --git a/Sources/ParseSwift/API/Responses.swift b/Sources/ParseSwift/API/Responses.swift index d881a1a96..9ea606bd8 100644 --- a/Sources/ParseSwift/API/Responses.swift +++ b/Sources/ParseSwift/API/Responses.swift @@ -25,8 +25,11 @@ internal struct SaveResponse: Decodable { } internal struct UpdateSessionTokenResponse: Decodable { - var updatedAt: Date - let sessionToken: String + var updatedAt: Date? + let sessionToken: String? + let accessToken: String? + let refreshToken: String? + let expiresAt: Date? } internal struct UpdateResponse: Decodable { @@ -87,10 +90,13 @@ internal struct QueryResponse: Codable where T: ParseObject { // MARK: ParseUser internal struct LoginSignupResponse: Codable { let createdAt: Date - let objectId: String - let sessionToken: String var updatedAt: Date? + let objectId: String let username: String? + let sessionToken: String? + let accessToken: String? + let refreshToken: String? + let expiresAt: Date? } // MARK: ParseFile diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index 77b7e92a5..a0cd1fc10 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -81,12 +81,39 @@ public extension ParseUser { using *current*. - parameter sessionToken: The sessionToken of the user to login. + - parameter accessToken: The OAuth2.0 accessToken of the user to login. + - parameter refreshToken: The OAuth2.0 refreshToken of the user for refreshing. + - parameter The date the OAuth2.0 sessionToken expires. - parameter options: A set of header options sent to the server. Defaults to an empty set. - returns: A publisher that eventually produces a single value and then finishes or fails. */ - func becomePublisher(sessionToken: String, options: API.Options = []) -> Future { + func becomePublisher(sessionToken: String? = nil, + accessToken: String? = nil, + refreshToken: String? = nil, + expiresAt: Date? = nil, + options: API.Options = []) -> Future { Future { promise in - self.become(sessionToken: sessionToken, options: options, completion: promise) + self.become(sessionToken: sessionToken, + accessToken: accessToken, + refreshToken: refreshToken, + expiresAt: expiresAt, + options: options, + completion: promise) + } + } + + // MARK: Refreshing SessionToken - Combine + /** + Refreshes the OAuth2.0 tokens of the currently logged in + user *asynchronously*. Publishes when complete. + + This will also update the session in the Keychain. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: A publisher that eventually produces a single value and then finishes or fails. + */ + static func refreshPublisher(options: API.Options = []) -> Future { + Future { promise in + Self.refresh(options: options, completion: promise) } } diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index b0384f7c0..6b4695a4c 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -47,6 +47,11 @@ struct SignupLoginBody: Encodable { } } +// MARK: RefreshBody +struct RefreshBody: Encodable { + let refreshToken: String +} + // MARK: EmailBody struct EmailBody: Encodable { let email: String @@ -88,6 +93,9 @@ extension ParseUser { struct CurrentUserContainer: Codable { var currentUser: T? var sessionToken: String? + var accessToken: String? + var refreshToken: String? + var expiresAt: Date? } // MARK: Current User Support @@ -147,6 +155,33 @@ extension ParseUser { public var sessionToken: String? { Self.currentUserContainer?.sessionToken } + + /** + The access token for the `ParseUser`. + + This is set by the server upon successful authentication. + */ + public var accessToken: String? { + Self.currentUserContainer?.accessToken + } + + /** + The OAuth refresh token for the `ParseUser`. + + This is set by the server upon successful authentication. + */ + public var refreshToken: String? { + Self.currentUserContainer?.refreshToken + } + + /** + The OAuth token expiration date for the `ParseUser`. + + This is set by the server upon successful authentication. + */ + public var expiresAt: Date? { + Self.currentUserContainer?.expiresAt + } } // MARK: Logging In @@ -210,7 +245,10 @@ extension ParseUser { Self.currentUserContainer = .init( currentUser: user, - sessionToken: response.sessionToken + sessionToken: response.sessionToken, + accessToken: response.accessToken, + refreshToken: response.refreshToken, + expiresAt: response.expiresAt ) Self.saveCurrentContainerToKeychain() return user @@ -222,15 +260,29 @@ extension ParseUser { to the keychain, so you can retrieve the currently logged in user using *current*. - parameter sessionToken: The sessionToken of the user to login. + - parameter accessToken: The OAuth2.0 accessToken of the user to login. + - parameter refreshToken: The OAuth2.0 refreshToken of the user for refreshing. + - parameter expiresAt: The date the OAuth2.0 sessionToken expires. - parameter options: A set of header options sent to the server. Defaults to an empty set. - throws: An Error of `ParseError` type. */ - public func become(sessionToken: String, options: API.Options = []) throws -> Self { + public func become(sessionToken: String? = nil, + accessToken: String? = nil, + refreshToken: String? = nil, + expiresAt: Date? = nil, + options: API.Options = []) throws -> Self { var newUser = self newUser.objectId = "me" var options = options - options.insert(.sessionToken(sessionToken)) - return try newUser.meCommand(sessionToken: sessionToken) + if let accessToken = accessToken { + options.insert(.sessionToken(accessToken)) + } else if let sessionToken = sessionToken { + options.insert(.sessionToken(sessionToken)) + } + return try newUser.meCommand(sessionToken: sessionToken, + accessToken: accessToken, + refreshToken: refreshToken, + expiresAt: expiresAt) .execute(options: options, callbackQueue: .main) } @@ -240,22 +292,35 @@ extension ParseUser { to the keychain, so you can retrieve the currently logged in user using *current*. - parameter sessionToken: The sessionToken of the user to login. + - parameter accessToken: The OAuth2.0 accessToken of the user to login. + - parameter refreshToken: The OAuth2.0 refreshToken of the user for refreshing. + - parameter The date the OAuth2.0 sessionToken expires. - parameter options: A set of header options sent to the server. Defaults to an empty set. - parameter callbackQueue: The queue to return to after completion. Default value of .main. - parameter completion: The block to execute when completed. It should have the following argument signature: `(Result)`. */ - public func become(sessionToken: String, + public func become(sessionToken: String? = nil, + accessToken: String? = nil, + refreshToken: String? = nil, + expiresAt: Date? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { var newUser = self newUser.objectId = "me" var options = options - options.insert(.sessionToken(sessionToken)) + if let accessToken = accessToken { + options.insert(.sessionToken(accessToken)) + } else if let sessionToken = sessionToken { + options.insert(.sessionToken(sessionToken)) + } do { - try newUser.meCommand(sessionToken: sessionToken) + try newUser.meCommand(sessionToken: sessionToken, + accessToken: accessToken, + refreshToken: refreshToken, + expiresAt: expiresAt) .executeAsync(options: options, callbackQueue: callbackQueue) { result in if case .success(let foundResult) = result { @@ -279,7 +344,10 @@ extension ParseUser { } } - internal func meCommand(sessionToken: String) throws -> API.Command { + internal func meCommand(sessionToken: String? = nil, + accessToken: String? = nil, + refreshToken: String?, + expiresAt: Date?) throws -> API.Command { return API.Command(method: .GET, path: endpoint) { (data) -> Self in @@ -293,7 +361,10 @@ extension ParseUser { Self.currentUserContainer = .init( currentUser: user, - sessionToken: sessionToken + sessionToken: sessionToken, + accessToken: accessToken, + refreshToken: refreshToken, + expiresAt: expiresAt ) Self.saveCurrentContainerToKeychain() return user @@ -305,10 +376,17 @@ extension ParseUser { extension ParseUser { /** - Logs out the currently logged in user in Keychain *synchronously*. + Logs out the currently logged in user in Keychain *synchronously*. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - throws: An Error of `ParseError` type. */ public static func logout(options: API.Options = []) throws { - let error = try? logoutCommand().execute(options: options) + let error: ParseError? + if let refreshToken = BaseParseUser.currentUserContainer?.refreshToken { + error = try? revokeCommand(refreshToken: refreshToken).execute(options: options) + } else { + error = try? logoutCommand().execute(options: options) + } //Always let user logout locally, no matter the error. deleteCurrentKeychain() //Wait to throw error @@ -329,22 +407,44 @@ extension ParseUser { */ public static func logout(options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { - logoutCommand().executeAsync(options: options) { result in - callbackQueue.async { + if let refreshToken = BaseParseUser.currentUserContainer?.refreshToken { + revokeCommand(refreshToken: refreshToken).executeAsync(options: options) { result in + callbackQueue.async { - //Always let user logout locally, no matter the error. - deleteCurrentKeychain() + //Always let user logout locally, no matter the error. + deleteCurrentKeychain() - switch result { + switch result { - case .success(let error): - if let error = error { + case .success(let error): + if let error = error { + completion(.failure(error)) + } else { + completion(.success(())) + } + case .failure(let error): + completion(.failure(error)) + } + } + } + } else { + logoutCommand().executeAsync(options: options) { result in + callbackQueue.async { + + //Always let user logout locally, no matter the error. + deleteCurrentKeychain() + + switch result { + + case .success(let error): + if let error = error { + completion(.failure(error)) + } else { + completion(.success(())) + } + case .failure(let error): completion(.failure(error)) - } else { - completion(.success(())) } - case .failure(let error): - completion(.failure(error)) } } } @@ -352,13 +452,108 @@ extension ParseUser { internal static func logoutCommand() -> API.NonParseBodyCommand { return API.NonParseBodyCommand(method: .POST, path: .logout) { (data) -> ParseError? in + do { + return try ParseCoding.jsonDecoder().decode(ParseError.self, from: data) + } catch { + return nil + } + } + } +} + +// MARK: Refresh +extension ParseUser { + + /** + Refreshes the tokens of the currently logged in user in the Keychain *synchronously*. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - returns: The refreshed user. + - throws: An Error of `ParseError` type. + */ + public static func refresh(options: API.Options = []) throws -> Self { + var options = options + options.insert(.removeAccessToken) + return try refreshCommand().execute(options: options) + } + + /** + Refreshes the tokens of the currently logged in user in the Keychain *asynchronously*. + + This will update the session in the Keychain. This is preferable to using `refresh`, + unless your code is already running from a background thread. + - parameter options: A set of header options sent to the server. Defaults to an empty set. + - parameter callbackQueue: The queue to return to after completion. Default value of .main. + - parameter completion: A block that will be called when refreshing, completes or fails. + */ + public static func refresh(options: API.Options = [], callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + do { + var options = options + options.insert(.removeAccessToken) + try refreshCommand().executeAsync(options: options) { result in + callbackQueue.async { + completion(result) + } + } + } catch { + callbackQueue.async { + if let parseError = error as? ParseError { + completion(.failure(parseError)) + } else { + let parseError = ParseError(code: .unknownError, + message: "couldn't determine refresh error.") + completion(.failure(parseError)) + } + } + } + } + + internal static func refreshCommand() throws -> API.NonParseBodyCommand { + guard let refreshToken = BaseParseUser.currentUserContainer?.refreshToken else { + throw ParseError(code: .unknownError, message: "current user is missing refreshToken.") + } + let body = RefreshBody(refreshToken: refreshToken) + return API.NonParseBodyCommand(method: .POST, + path: .refresh, + body: body) { (data) -> Self in + + guard let user = try? ParseCoding.jsonDecoder().decode(LoginSignupResponse.self, from: data) else { + if let error = try? ParseCoding.jsonDecoder().decode(ParseError.self, from: data) { + throw error + } else { + throw ParseError(code: .unknownError, message: "couldn't decode refreshToken.") + } + } + + if Self.current != nil { + Self.currentUserContainer?.sessionToken = user.sessionToken + Self.currentUserContainer?.accessToken = user.accessToken + Self.currentUserContainer?.refreshToken = user.refreshToken + Self.currentUserContainer?.expiresAt = user.expiresAt + Self.saveCurrentContainerToKeychain() + return Self.current! + } else { + throw ParseError(code: .unknownError, + message: "This device doesn't have a current user in the Keychain") + } + } + } +} + +// MARK: Revoke +extension ParseUser { + internal static func revokeCommand(refreshToken: String) -> API.NonParseBodyCommand { + let body = RefreshBody(refreshToken: refreshToken) + return API.NonParseBodyCommand(method: .POST, + path: .revoke, + body: body) { (data) -> ParseError? in do { let parseError = try ParseCoding.jsonDecoder().decode(ParseError.self, from: data) return parseError } catch { return nil } - } + } } } @@ -611,8 +806,11 @@ extension ParseUser { API.NonParseBodyCommand(method: .POST, path: .users, body: body) { (data) -> Self in - let sessionToken = try ParseCoding.jsonDecoder() - .decode(LoginSignupResponse.self, from: data).sessionToken + let response = try ParseCoding.jsonDecoder().decode(LoginSignupResponse.self, from: data) + let sessionToken = response.sessionToken + let accessToken = response.accessToken + let refreshToken = response.refreshToken + let expiresAt = response.expiresAt var user = try ParseCoding.jsonDecoder().decode(Self.self, from: data) if user.username == nil { @@ -625,8 +823,14 @@ extension ParseUser { user.authData = authData } } - Self.currentUserContainer = .init(currentUser: user, - sessionToken: sessionToken) + + Self.currentUserContainer = .init( + currentUser: user, + sessionToken: sessionToken, + accessToken: accessToken, + refreshToken: refreshToken, + expiresAt: expiresAt + ) Self.saveCurrentContainerToKeychain() return user } @@ -638,14 +842,19 @@ extension ParseUser { path: endpoint, body: self) { (data) -> Self in - let sessionToken = try ParseCoding.jsonDecoder() - .decode(LoginSignupResponse.self, from: data).sessionToken + let response = try ParseCoding.jsonDecoder().decode(LoginSignupResponse.self, from: data) + let sessionToken = response.sessionToken + let accessToken = response.accessToken + let refreshToken = response.refreshToken + let expiresAt = response.expiresAt var user = try ParseCoding.jsonDecoder().decode(Self.self, from: data) user.username = self.username Self.currentUserContainer = .init( currentUser: user, - sessionToken: sessionToken - ) + sessionToken: sessionToken, + accessToken: accessToken, + refreshToken: refreshToken, + expiresAt: expiresAt) Self.saveCurrentContainerToKeychain() return user } diff --git a/Tests/ParseSwiftTests/ParseUserCombineTests.swift b/Tests/ParseSwiftTests/ParseUserCombineTests.swift index 9ea577e56..cf93c2e70 100644 --- a/Tests/ParseSwiftTests/ParseUserCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseUserCombineTests.swift @@ -64,6 +64,40 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le } } + struct LoginSignupResponseOAuth: ParseUser { + + var objectId: String? + var createdAt: Date? + var sessionToken: String? + var updatedAt: Date? + var ACL: ParseACL? + var accessToken: String? + var refreshToken: String? + var expiresAt: Date? + + // 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.accessToken = "myToken" + self.refreshToken = "yolo" + self.expiresAt = date + self.username = "hello10" + } + } + let loginUserName = "hello10" let loginPassword = "world" @@ -119,6 +153,9 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNil(signedUp.password) XCTAssertNotNil(signedUp.objectId) XCTAssertNotNil(signedUp.sessionToken) + XCTAssertNil(signedUp.accessToken) + XCTAssertNil(signedUp.refreshToken) + XCTAssertNil(signedUp.expiresAt) XCTAssertNotNil(signedUp.customKey) XCTAssertNil(signedUp.ACL) @@ -134,6 +171,9 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.accessToken) + XCTAssertNil(userFromKeychain.refreshToken) + XCTAssertNil(userFromKeychain.expiresAt) XCTAssertNil(userFromKeychain.ACL) }) publisher.store(in: &subscriptions) @@ -170,6 +210,9 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNil(signedUp.password) XCTAssertNotNil(signedUp.objectId) XCTAssertNotNil(signedUp.sessionToken) + XCTAssertNil(signedUp.accessToken) + XCTAssertNil(signedUp.refreshToken) + XCTAssertNil(signedUp.expiresAt) XCTAssertNotNil(signedUp.customKey) XCTAssertNil(signedUp.ACL) @@ -185,6 +228,9 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.accessToken) + XCTAssertNil(userFromKeychain.refreshToken) + XCTAssertNil(userFromKeychain.expiresAt) XCTAssertNil(userFromKeychain.ACL) }) publisher.store(in: &subscriptions) @@ -219,11 +265,11 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTFail("Should unwrap") return } - + let sessionToken = "newValue" var serverResponse = LoginSignupResponse() serverResponse.createdAt = User.current?.createdAt serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) - serverResponse.sessionToken = "newValue" + serverResponse.sessionToken = sessionToken serverResponse.username = "stop" var subscriptions = Set() @@ -237,7 +283,7 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le } let expectation1 = XCTestExpectation(description: "Become user1") - let publisher = user.becomePublisher(sessionToken: serverResponse.sessionToken) + let publisher = user.becomePublisher(sessionToken: sessionToken) .sink(receiveCompletion: { result in if case let .failure(error) = result { @@ -254,6 +300,9 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNil(signedUp.password) XCTAssertNotNil(signedUp.objectId) XCTAssertNotNil(signedUp.sessionToken) + XCTAssertNil(signedUp.accessToken) + XCTAssertNil(signedUp.refreshToken) + XCTAssertNil(signedUp.expiresAt) XCTAssertNotNil(signedUp.customKey) XCTAssertNil(signedUp.ACL) @@ -269,12 +318,169 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.accessToken) + XCTAssertNil(userFromKeychain.refreshToken) + XCTAssertNil(userFromKeychain.expiresAt) XCTAssertNil(userFromKeychain.ACL) }) publisher.store(in: &subscriptions) wait(for: [expectation1], timeout: 20.0) } + func testBecomeOAuth() { + login() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + guard let user = User.current else { + XCTFail("Should unwrap") + return + } + let accessToken = "newValue" + var serverResponse = LoginSignupResponseOAuth() + serverResponse.createdAt = User.current?.createdAt + serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) + serverResponse.accessToken = accessToken + serverResponse.username = "stop" + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Become user1") + let publisher = user.becomePublisher(accessToken: accessToken, + refreshToken: "yolo", + expiresAt: Date()) + .sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { signedUp in + XCTAssertNotNil(signedUp) + XCTAssertNotNil(signedUp.createdAt) + XCTAssertNotNil(signedUp.updatedAt) + XCTAssertNil(signedUp.email) + XCTAssertNotNil(signedUp.username) + XCTAssertNil(signedUp.password) + XCTAssertNotNil(signedUp.objectId) + XCTAssertNil(signedUp.sessionToken) + XCTAssertNotNil(signedUp.accessToken) + XCTAssertNotNil(signedUp.refreshToken) + XCTAssertNotNil(signedUp.expiresAt) + XCTAssertNotNil(signedUp.customKey) + XCTAssertNil(signedUp.ACL) + + guard let userFromKeychain = BaseParseUser.current else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + + XCTAssertNotNil(userFromKeychain.createdAt) + XCTAssertNotNil(userFromKeychain.updatedAt) + XCTAssertNil(userFromKeychain.email) + XCTAssertNotNil(userFromKeychain.username) + XCTAssertNil(userFromKeychain.password) + XCTAssertNotNil(userFromKeychain.objectId) + XCTAssertNil(userFromKeychain.sessionToken) + XCTAssertNotNil(userFromKeychain.accessToken) + XCTAssertNotNil(userFromKeychain.refreshToken) + XCTAssertNotNil(userFromKeychain.expiresAt) + XCTAssertNil(userFromKeychain.ACL) + }) + publisher.store(in: &subscriptions) + wait(for: [expectation1], timeout: 20.0) + } + + func loginOAuth() throws -> User { + let loginResponse = LoginSignupResponseOAuth() + + 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 testRefresh() throws { + _ = try loginOAuth() + MockURLProtocol.removeAll() + + var refreshResponse = LoginSignupResponseOAuth() + refreshResponse.accessToken = "hello" + refreshResponse.refreshToken = "world" + refreshResponse.expiresAt = Date() + var subscriptions = Set() + + let encoded = try ParseCoding.jsonEncoder().encode(refreshResponse) + //Get dates in correct format from ParseDecoding strategy + refreshResponse = try ParseCoding.jsonDecoder().decode(LoginSignupResponseOAuth.self, from: encoded) + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let expectation1 = XCTestExpectation(description: "Logout user1") + let publisher = User.refreshPublisher().sink(receiveCompletion: { result in + + if case let .failure(error) = result { + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + + }, receiveValue: { user in + guard let accessToken = user.accessToken, + let refreshToken = user.refreshToken, + let expiresAt = user.expiresAt else { + XCTFail("Should unwrap all values") + return + } + XCTAssertEqual(accessToken, refreshResponse.accessToken) + XCTAssertEqual(refreshToken, refreshResponse.refreshToken) + XCTAssertEqual(expiresAt, refreshResponse.expiresAt) + XCTAssertNotNil(user.createdAt) + XCTAssertNotNil(user.updatedAt) + XCTAssertNil(user.email) + XCTAssertNotNil(user.username) + XCTAssertNil(user.password) + XCTAssertNotNil(user.objectId) + XCTAssertNil(user.ACL) + + guard let userFromKeychain = BaseParseUser.current, + let accessTokenFromKeychain = user.accessToken, + let refreshTokenFromKeychain = user.refreshToken, + let expiresAtFromKeychain = user.expiresAt else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + XCTAssertEqual(accessTokenFromKeychain, refreshResponse.accessToken) + XCTAssertEqual(refreshTokenFromKeychain, refreshResponse.refreshToken) + XCTAssertEqual(expiresAtFromKeychain, refreshResponse.expiresAt) + XCTAssertNotNil(userFromKeychain.createdAt) + XCTAssertNotNil(userFromKeychain.updatedAt) + XCTAssertNil(userFromKeychain.email) + XCTAssertNotNil(userFromKeychain.username) + XCTAssertNil(userFromKeychain.password) + XCTAssertNotNil(userFromKeychain.objectId) + XCTAssertNil(userFromKeychain.ACL) + }) + publisher.store(in: &subscriptions) + + wait(for: [expectation1], timeout: 20.0) + } + func testLogout() { login() MockURLProtocol.removeAll() diff --git a/Tests/ParseSwiftTests/ParseUserOAuthTests.swift b/Tests/ParseSwiftTests/ParseUserOAuthTests.swift new file mode 100644 index 000000000..aa5f855d4 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseUserOAuthTests.swift @@ -0,0 +1,712 @@ +// +// ParseUserOAuthTests.swift +// ParseSwift +// +// Created by Corey Baker on 3/12/21. +// Copyright © 2021 Parse Community. All rights reserved. +// + +import Foundation +import XCTest +@testable import ParseSwift + +class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_length + + struct User: ParseUser { + + //: Those are required for Object + var objectId: String? + var createdAt: Date? + var updatedAt: Date? + var ACL: ParseACL? + + // provided by User + var username: String? + var email: String? + var password: String? + var authData: [String: [String: String]?]? + + // Your custom keys + var customKey: String? + } + + struct LoginSignupResponse: ParseUser { + + var objectId: String? + var createdAt: Date? + var sessionToken: String? + var updatedAt: Date? + var ACL: ParseACL? + var accessToken: String? + var refreshToken: String? + var expiresAt: Date? + + // 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.accessToken = "myToken" + self.refreshToken = "yolo" + self.expiresAt = date + self.username = "hello10" + } + } + let loginUserName = "hello10" + let loginPassword = "world" + + override func setUp() { + super.setUp() + guard let url = URL(string: "http://localhost:1337/1") else { + XCTFail("Should create valid URL") + return + } + ParseSwift.initialize(applicationId: "applicationId", + clientKey: "clientKey", + masterKey: "masterKey", + serverURL: url, + testing: true) + } + + override func tearDownWithError() throws { + super.tearDown() + MockURLProtocol.removeAll() + #if !os(Linux) && !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 testRefreshCommand() throws { + _ = try loginNormally() + let body = RefreshBody(refreshToken: "yolo") + do { + let command = try User.refreshCommand() + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/users/refresh") + XCTAssertEqual(command.method, API.Method.POST) + XCTAssertEqual(command.body?.refreshToken, body.refreshToken) + } catch { + XCTFail(error.localizedDescription) + } + } + + func testRevokeCommand() throws { + _ = try loginNormally() + let body = RefreshBody(refreshToken: "yolo") + let command = User.revokeCommand(refreshToken: "yolo") + XCTAssertNotNil(command) + XCTAssertEqual(command.path.urlComponent, "/revoke") + XCTAssertEqual(command.method, API.Method.POST) + XCTAssertEqual(command.body?.refreshToken, body.refreshToken) + } + + func testUserSignUp() { + let loginResponse = LoginSignupResponse() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + do { + let signedUp = try User.signup(username: loginUserName, password: loginPassword) + XCTAssertNotNil(signedUp) + XCTAssertNotNil(signedUp.createdAt) + XCTAssertNotNil(signedUp.updatedAt) + XCTAssertNotNil(signedUp.username) + XCTAssertNil(signedUp.password) + XCTAssertNil(signedUp.email) + XCTAssertNotNil(signedUp.objectId) + XCTAssertNil(signedUp.sessionToken) + XCTAssertNotNil(signedUp.accessToken) + XCTAssertNotNil(signedUp.refreshToken) + XCTAssertNotNil(signedUp.expiresAt) + XCTAssertNil(signedUp.ACL) + + guard let userFromKeychain = BaseParseUser.current else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + + XCTAssertNotNil(userFromKeychain.createdAt) + XCTAssertNotNil(userFromKeychain.updatedAt) + XCTAssertNil(userFromKeychain.email) + XCTAssertNotNil(userFromKeychain.username) + XCTAssertNil(userFromKeychain.password) + XCTAssertNotNil(userFromKeychain.objectId) + XCTAssertNil(userFromKeychain.sessionToken) + XCTAssertNotNil(userFromKeychain.accessToken) + XCTAssertNotNil(userFromKeychain.refreshToken) + XCTAssertNotNil(userFromKeychain.expiresAt) + XCTAssertNil(userFromKeychain.ACL) + + } catch { + XCTFail(error.localizedDescription) + } + } + + func signUpAsync(loginResponse: LoginSignupResponse, callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Signup user1") + User.signup(username: loginUserName, password: loginPassword, + callbackQueue: callbackQueue) { result in + switch result { + + case .success(let signedUp): + XCTAssertNotNil(signedUp.createdAt) + XCTAssertNotNil(signedUp.updatedAt) + XCTAssertNil(signedUp.email) + XCTAssertNotNil(signedUp.username) + XCTAssertNil(signedUp.password) + XCTAssertNotNil(signedUp.objectId) + XCTAssertNil(signedUp.sessionToken) + XCTAssertNotNil(signedUp.accessToken) + XCTAssertNotNil(signedUp.refreshToken) + XCTAssertNotNil(signedUp.expiresAt) + XCTAssertNil(signedUp.ACL) + + guard let userFromKeychain = BaseParseUser.current else { + XCTFail("Couldn't get CurrentUser from Keychain") + expectation1.fulfill() + return + } + + XCTAssertNotNil(userFromKeychain.createdAt) + XCTAssertNotNil(userFromKeychain.updatedAt) + XCTAssertNotNil(userFromKeychain.username) + XCTAssertNil(userFromKeychain.email) + XCTAssertNil(userFromKeychain.password) + XCTAssertNotNil(userFromKeychain.objectId) + XCTAssertNil(userFromKeychain.sessionToken) + XCTAssertNotNil(userFromKeychain.accessToken) + XCTAssertNotNil(userFromKeychain.refreshToken) + XCTAssertNotNil(userFromKeychain.expiresAt) + XCTAssertNil(userFromKeychain.ACL) + + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testSignUpAsyncMainQueue() { + let loginResponse = LoginSignupResponse() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + self.signUpAsync(loginResponse: loginResponse, callbackQueue: .main) + } + + func testLogin() { + let loginResponse = LoginSignupResponse() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + do { + let loggedIn = try User.login(username: loginUserName, password: loginPassword) + XCTAssertNotNil(loggedIn) + XCTAssertNotNil(loggedIn.createdAt) + XCTAssertNotNil(loggedIn.updatedAt) + XCTAssertNil(loggedIn.email) + XCTAssertNotNil(loggedIn.username) + XCTAssertNil(loggedIn.password) + XCTAssertNotNil(loggedIn.objectId) + XCTAssertNil(loggedIn.sessionToken) + XCTAssertNotNil(loggedIn.accessToken) + XCTAssertNotNil(loggedIn.refreshToken) + XCTAssertNotNil(loggedIn.expiresAt) + XCTAssertNil(loggedIn.ACL) + + guard let userFromKeychain = BaseParseUser.current else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + + XCTAssertNotNil(userFromKeychain.createdAt) + XCTAssertNotNil(userFromKeychain.updatedAt) + XCTAssertNotNil(userFromKeychain.username) + XCTAssertNil(userFromKeychain.email) + XCTAssertNil(userFromKeychain.password) + XCTAssertNotNil(userFromKeychain.objectId) + XCTAssertNil(userFromKeychain.sessionToken) + XCTAssertNotNil(userFromKeychain.accessToken) + XCTAssertNotNil(userFromKeychain.refreshToken) + XCTAssertNotNil(userFromKeychain.expiresAt) + XCTAssertNil(userFromKeychain.ACL) + + } catch { + XCTFail(error.localizedDescription) + } + } + + func loginAsync(loginResponse: LoginSignupResponse, callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Login user") + User.login(username: loginUserName, password: loginPassword, + callbackQueue: callbackQueue) { result in + + switch result { + + case .success(let loggedIn): + XCTAssertNotNil(loggedIn.createdAt) + XCTAssertNotNil(loggedIn.updatedAt) + XCTAssertNil(loggedIn.email) + XCTAssertNotNil(loggedIn.username) + XCTAssertNil(loggedIn.password) + XCTAssertNotNil(loggedIn.objectId) + XCTAssertNil(loggedIn.sessionToken) + XCTAssertNotNil(loggedIn.accessToken) + XCTAssertNotNil(loggedIn.refreshToken) + XCTAssertNotNil(loggedIn.expiresAt) + XCTAssertNil(loggedIn.ACL) + + guard let userFromKeychain = BaseParseUser.current else { + XCTFail("Couldn't get CurrentUser from Keychain") + expectation1.fulfill() + return + } + + XCTAssertNotNil(userFromKeychain.createdAt) + XCTAssertNotNil(userFromKeychain.updatedAt) + XCTAssertNil(userFromKeychain.email) + XCTAssertNotNil(userFromKeychain.username) + XCTAssertNil(userFromKeychain.password) + XCTAssertNotNil(userFromKeychain.objectId) + XCTAssertNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.ACL) + XCTAssertNotNil(userFromKeychain.accessToken) + XCTAssertNotNil(userFromKeychain.refreshToken) + XCTAssertNotNil(userFromKeychain.expiresAt) + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testLoginAsyncMainQueue() { + let loginResponse = LoginSignupResponse() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try loginResponse.getEncoder().encode(loginResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + self.loginAsync(loginResponse: loginResponse, callbackQueue: .main) + } + + func testRefresh() { + testLogin() + MockURLProtocol.removeAll() + + var refreshResponse = LoginSignupResponse() + refreshResponse.accessToken = "hello" + refreshResponse.refreshToken = "world" + refreshResponse.expiresAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(refreshResponse) + //Get dates in correct format from ParseDecoding strategy + refreshResponse = try ParseCoding.jsonDecoder().decode(LoginSignupResponse.self, from: encoded) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + do { + let user = try User.refresh() + guard let accessToken = user.accessToken, + let refreshToken = user.refreshToken, + let expiresAt = user.expiresAt else { + XCTFail("Should unwrap all values") + return + } + XCTAssertEqual(accessToken, refreshResponse.accessToken) + XCTAssertEqual(refreshToken, refreshResponse.refreshToken) + XCTAssertEqual(expiresAt, refreshResponse.expiresAt) + XCTAssertNotNil(user.createdAt) + XCTAssertNotNil(user.updatedAt) + XCTAssertNil(user.email) + XCTAssertNotNil(user.username) + XCTAssertNil(user.password) + XCTAssertNotNil(user.objectId) + XCTAssertNil(user.ACL) + + guard let userFromKeychain = BaseParseUser.current, + let accessTokenFromKeychain = user.accessToken, + let refreshTokenFromKeychain = user.refreshToken, + let expiresAtFromKeychain = user.expiresAt else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + XCTAssertEqual(accessTokenFromKeychain, refreshResponse.accessToken) + XCTAssertEqual(refreshTokenFromKeychain, refreshResponse.refreshToken) + XCTAssertEqual(expiresAtFromKeychain, refreshResponse.expiresAt) + XCTAssertNotNil(userFromKeychain.createdAt) + XCTAssertNotNil(userFromKeychain.updatedAt) + XCTAssertNil(userFromKeychain.email) + XCTAssertNotNil(userFromKeychain.username) + XCTAssertNil(userFromKeychain.password) + XCTAssertNotNil(userFromKeychain.objectId) + XCTAssertNil(userFromKeychain.ACL) + } catch { + XCTFail(error.localizedDescription) + } + } + + func refreshAsync(refreshResponse: LoginSignupResponse, callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Logout user1") + User.refresh(callbackQueue: callbackQueue) { result in + + switch result { + + case .success(let user): + guard let accessToken = user.accessToken, + let refreshToken = user.refreshToken, + let expiresAt = user.expiresAt else { + XCTFail("Should unwrap all values") + return + } + XCTAssertEqual(accessToken, refreshResponse.accessToken) + XCTAssertEqual(refreshToken, refreshResponse.refreshToken) + XCTAssertEqual(expiresAt, refreshResponse.expiresAt) + XCTAssertNotNil(user.createdAt) + XCTAssertNotNil(user.updatedAt) + XCTAssertNil(user.email) + XCTAssertNotNil(user.username) + XCTAssertNil(user.password) + XCTAssertNotNil(user.objectId) + XCTAssertNil(user.ACL) + + guard let userFromKeychain = BaseParseUser.current, + let accessTokenFromKeychain = user.accessToken, + let refreshTokenFromKeychain = user.refreshToken, + let expiresAtFromKeychain = user.expiresAt else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + XCTAssertEqual(accessTokenFromKeychain, refreshResponse.accessToken) + XCTAssertEqual(refreshTokenFromKeychain, refreshResponse.refreshToken) + XCTAssertEqual(expiresAtFromKeychain, refreshResponse.expiresAt) + XCTAssertNotNil(userFromKeychain.createdAt) + XCTAssertNotNil(userFromKeychain.updatedAt) + XCTAssertNil(userFromKeychain.email) + XCTAssertNotNil(userFromKeychain.username) + XCTAssertNil(userFromKeychain.password) + XCTAssertNotNil(userFromKeychain.objectId) + XCTAssertNil(userFromKeychain.ACL) + + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testRefreshAsyncMainQueue() throws { + testLogin() + MockURLProtocol.removeAll() + + var refreshResponse = LoginSignupResponse() + refreshResponse.accessToken = "hello" + refreshResponse.refreshToken = "world" + refreshResponse.expiresAt = Date() + + let encoded = try ParseCoding.jsonEncoder().encode(refreshResponse) + //Get dates in correct format from ParseDecoding strategy + refreshResponse = try ParseCoding.jsonDecoder().decode(LoginSignupResponse.self, from: encoded) + + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + self.refreshAsync(refreshResponse: refreshResponse, callbackQueue: .main) + } + + func testLogout() { + testLogin() + MockURLProtocol.removeAll() + + let logoutResponse = NoBody() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(logoutResponse) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + do { + try User.logout() + if let userFromKeychain = BaseParseUser.current { + XCTFail("\(userFromKeychain) wasn't deleted from Keychain during logout") + } + + if let installationFromKeychain = BaseParseInstallation.current { + XCTFail("\(installationFromKeychain) wasn't deleted from Keychain during logout") + } + } catch { + XCTFail(error.localizedDescription) + } + } + + func logoutAsync(callbackQueue: DispatchQueue) { + + let expectation1 = XCTestExpectation(description: "Logout user1") + User.logout(callbackQueue: callbackQueue) { result in + + switch result { + + case .success: + if let userFromKeychain = BaseParseUser.current { + XCTFail("\(userFromKeychain) wasn't deleted from Keychain during logout") + } + + if let installationFromMemory: CurrentInstallationContainer + = try? ParseStorage.shared.get(valueFor: ParseStorage.Keys.currentInstallation) { + XCTFail("\(installationFromMemory) wasn't deleted from memory during logout") + } + + #if !os(Linux) && !os(Android) + if let installationFromKeychain: CurrentInstallationContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentInstallation) { + XCTFail("\(installationFromKeychain) wasn't deleted from Keychain during logout") + } + #endif + + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } + + func testLogoutAsyncMainQueue() { + testLogin() + MockURLProtocol.removeAll() + + let logoutResponse = NoBody() + + MockURLProtocol.mockRequests { _ in + do { + let encoded = try ParseCoding.jsonEncoder().encode(logoutResponse) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + self.logoutAsync(callbackQueue: .main) + } + + func testBecome() { // swiftlint:disable:this function_body_length + testLogin() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + guard let user = User.current else { + XCTFail("Should unwrap") + return + } + + var serverResponse = LoginSignupResponse() + serverResponse.createdAt = User.current?.createdAt + serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) + serverResponse.accessToken = "newValue" + serverResponse.username = "stop" + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + do { + let become = try user.become(accessToken: "newValue", + refreshToken: "yolo", + expiresAt: Date()) + XCTAssert(become.hasSameObjectId(as: userOnServer)) + guard let becomeCreatedAt = become.createdAt, + let becomeUpdatedAt = become.updatedAt else { + XCTFail("Should unwrap dates") + return + } + guard let originalCreatedAt = user.createdAt, + let originalUpdatedAt = user.updatedAt else { + XCTFail("Should unwrap dates") + return + } + XCTAssertEqual(becomeCreatedAt, originalCreatedAt) + XCTAssertGreaterThan(becomeUpdatedAt, originalUpdatedAt) + XCTAssertNil(become.ACL) + XCTAssertNotNil(become.accessToken) + XCTAssertNotNil(become.refreshToken) + XCTAssertNotNil(become.expiresAt) + + //Should be updated in memory + XCTAssertEqual(User.current?.updatedAt, becomeUpdatedAt) + + //Should be updated in Keychain + #if !os(Linux) && !os(Android) + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) else { + XCTFail("Should get object from Keychain") + return + } + XCTAssertEqual(keychainUser.currentUser?.updatedAt, becomeUpdatedAt) + XCTAssertNotNil(keychainUser.accessToken) + XCTAssertNotNil(keychainUser.refreshToken) + XCTAssertNotNil(keychainUser.expiresAt) + #endif + + } catch { + XCTFail(error.localizedDescription) + } + } + + func testBecomeAsync() { // swiftlint:disable:this function_body_length + XCTAssertNil(User.current?.objectId) + testLogin() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + guard let user = User.current else { + XCTFail("Should unwrap") + return + } + + var serverResponse = LoginSignupResponse() + serverResponse.createdAt = User.current?.createdAt + serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) + serverResponse.accessToken = "newValue" + serverResponse.username = "stop" + serverResponse.password = "this" + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let expectation1 = XCTestExpectation(description: "Fetch user1") + user.become(accessToken: "newValue", + refreshToken: "yolo", + expiresAt: Date()) { result in + + switch result { + case .success(let become): + XCTAssert(become.hasSameObjectId(as: userOnServer)) + guard let becomeCreatedAt = become.createdAt, + let becomeUpdatedAt = become.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + guard let originalCreatedAt = user.createdAt, + let originalUpdatedAt = user.updatedAt else { + XCTFail("Should unwrap dates") + expectation1.fulfill() + return + } + XCTAssertEqual(becomeCreatedAt, originalCreatedAt) + XCTAssertGreaterThan(becomeUpdatedAt, originalUpdatedAt) + XCTAssertNil(become.ACL) + XCTAssertNotNil(become.accessToken) + XCTAssertNotNil(become.refreshToken) + XCTAssertNotNil(become.expiresAt) + + //Should be updated in memory + XCTAssertEqual(User.current?.updatedAt, becomeUpdatedAt) + + #if !os(Linux) && !os(Android) + //Should be updated in Keychain + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainUser.currentUser?.updatedAt, becomeUpdatedAt) + XCTAssertNotNil(keychainUser.accessToken) + XCTAssertNotNil(keychainUser.refreshToken) + XCTAssertNotNil(keychainUser.expiresAt) + #endif + case .failure(let error): + XCTFail(error.localizedDescription) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } +} diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index 638febb3b..c7d6acd03 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -785,6 +785,8 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNil(signedUp.password) XCTAssertNotNil(signedUp.objectId) XCTAssertNotNil(signedUp.sessionToken) + XCTAssertNil(signedUp.refreshToken) + XCTAssertNil(signedUp.expiresAt) XCTAssertNotNil(signedUp.customKey) XCTAssertNil(signedUp.ACL) @@ -800,6 +802,8 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.refreshToken) + XCTAssertNil(userFromKeychain.expiresAt) XCTAssertNil(userFromKeychain.ACL) } catch { @@ -868,6 +872,8 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNil(signedUp.password) XCTAssertNotNil(signedUp.objectId) XCTAssertNotNil(signedUp.sessionToken) + XCTAssertNil(signedUp.refreshToken) + XCTAssertNil(signedUp.expiresAt) XCTAssertNotNil(signedUp.customKey) XCTAssertNil(signedUp.ACL) @@ -884,6 +890,8 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.refreshToken) + XCTAssertNil(userFromKeychain.expiresAt) XCTAssertNil(userFromKeychain.ACL) case .failure(let error): XCTFail(error.localizedDescription) @@ -1032,6 +1040,8 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNil(loggedIn.password) XCTAssertNotNil(loggedIn.objectId) XCTAssertNotNil(loggedIn.sessionToken) + XCTAssertNil(loggedIn.refreshToken) + XCTAssertNil(loggedIn.expiresAt) XCTAssertNotNil(loggedIn.customKey) XCTAssertNil(loggedIn.ACL) @@ -1049,6 +1059,8 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length XCTAssertNotNil(userFromKeychain.objectId) XCTAssertNotNil(userFromKeychain.sessionToken) XCTAssertNil(userFromKeychain.ACL) + XCTAssertNil(userFromKeychain.refreshToken) + XCTAssertNil(userFromKeychain.expiresAt) case .failure(let error): XCTFail(error.localizedDescription) } @@ -2079,7 +2091,7 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length var user = User() user.objectId = "me" do { - let command = try user.meCommand(sessionToken: "yolo") + let command = try user.meCommand(sessionToken: "yolo", refreshToken: nil, expiresAt: nil) XCTAssertNotNil(command) XCTAssertEqual(command.path.urlComponent, "/users/me") XCTAssertEqual(command.method, API.Method.GET) @@ -2232,5 +2244,43 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } wait(for: [expectation1], timeout: 20.0) } + + func testCantRefreshRegularSesstionToken() throws { + XCTAssertNil(User.current?.objectId) + testLogin() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + guard User.current != nil else { + XCTFail("Should unwrap") + return + } + + XCTAssertThrowsError(try User.refresh()) + } + + func testCantRefreshRegularSesstionTokenAsync() throws { + XCTAssertNil(User.current?.objectId) + testLogin() + MockURLProtocol.removeAll() + XCTAssertNotNil(User.current?.objectId) + + guard User.current != nil else { + XCTFail("Should unwrap") + return + } + + let expectation1 = XCTestExpectation(description: "Refresh user1") + User.refresh { result in + switch result { + case .success: + XCTFail("Should not have succeeded") + case .failure(let error): + XCTAssertTrue(error.message.contains("missing refreshToken")) + } + expectation1.fulfill() + } + wait(for: [expectation1], timeout: 20.0) + } } // swiftlint:disable:this file_length