From 24dc08cc5255daf46b55de47943b67d128ec1007 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Fri, 12 Mar 2021 16:54:04 -0500 Subject: [PATCH 01/10] Add OAuth support --- ParseSwift.xcodeproj/project.pbxproj | 8 + Sources/ParseSwift/API/API.swift | 6 + Sources/ParseSwift/API/Responses.swift | 12 +- .../Objects/ParseUser+combine.swift | 28 +- Sources/ParseSwift/Objects/ParseUser.swift | 218 +++++- .../ParseUserCombineTests.swift | 202 +++++ .../ParseSwiftTests/ParseUserOAuthTests.swift | 711 ++++++++++++++++++ Tests/ParseSwiftTests/ParseUserTests.swift | 14 +- 8 files changed, 1170 insertions(+), 29 deletions(-) create mode 100644 Tests/ParseSwiftTests/ParseUserOAuthTests.swift diff --git a/ParseSwift.xcodeproj/project.pbxproj b/ParseSwift.xcodeproj/project.pbxproj index dd64a6cb7..124140fbf 100644 --- a/ParseSwift.xcodeproj/project.pbxproj +++ b/ParseSwift.xcodeproj/project.pbxproj @@ -209,6 +209,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 */; }; @@ -582,6 +585,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 = ""; }; @@ -813,6 +817,7 @@ 70C5504525B40D5200B5DBC2 /* ParseSessionTests.swift */, 7016ED3F25C4A25A00038648 /* ParseUserCombineTests.swift */, 70C7DC1D24D20E530050419B /* ParseUserTests.swift */, + 707CCE9B25FBD4D0003C3B64 /* ParseUserOAuthTests.swift */, 7FFF552A2217E729007C3B4E /* AnyCodableTests */, 911DB12A24C3F7260027F3C7 /* NetworkMocking */, ); @@ -1658,6 +1663,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 */, @@ -1802,6 +1808,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 */, @@ -1851,6 +1858,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 f6088e118..aec91b7df 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 "/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: diff --git a/Sources/ParseSwift/API/Responses.swift b/Sources/ParseSwift/API/Responses.swift index 9c70e603c..6692e713d 100644 --- a/Sources/ParseSwift/API/Responses.swift +++ b/Sources/ParseSwift/API/Responses.swift @@ -116,10 +116,18 @@ 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 refreshToken: String? + let expiresAt: Date? + + private enum CodingKeys: String, CodingKey { + case createdAt, objectId, updatedAt, username + case sessionToken, refreshToken + case expiresAt = "expires_in" + } } // MARK: ParseFile diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index 619ff287b..e2aeca95e 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -81,12 +81,36 @@ public extension ParseUser { using *current*. - parameter sessionToken: The sessionToken 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, + 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, + refreshToken: refreshToken, + expiresAt: expiresAt, + options: options, + completion: promise) + } + } + + // MARK: Refreshing SessionToken - Combine + /** + Refreshes the OAuth2.0 sessionToken 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 03cbbf414..d58fbd7f6 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 @@ -74,6 +79,9 @@ extension ParseUser { struct CurrentUserContainer: Codable { var currentUser: T? var sessionToken: String? + var oauth: Bool? + var refreshToken: String? + var expiresAt: Date? } // MARK: Current User Support @@ -137,6 +145,24 @@ extension ParseUser { public var sessionToken: String? { Self.currentUserContainer?.sessionToken } + + /** + 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 @@ -200,7 +226,9 @@ extension ParseUser { Self.currentUserContainer = .init( currentUser: user, - sessionToken: response.sessionToken + sessionToken: response.sessionToken, + refreshToken: response.refreshToken, + expiresAt: response.expiresAt ) Self.saveCurrentContainerToKeychain() return user @@ -212,15 +240,22 @@ 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 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, + 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) + return try newUser.meCommand(sessionToken: sessionToken, + refreshToken: refreshToken, + expiresAt: expiresAt) .execute(options: options, callbackQueue: .main) } @@ -230,6 +265,8 @@ 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 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. @@ -237,6 +274,8 @@ extension ParseUser { It should have the following argument signature: `(Result)`. */ public func become(sessionToken: String, + refreshToken: String? = nil, + expiresAt: Date? = nil, options: API.Options = [], callbackQueue: DispatchQueue = .main, completion: @escaping (Result) -> Void) { @@ -245,7 +284,9 @@ extension ParseUser { var options = options options.insert(.sessionToken(sessionToken)) do { - try newUser.meCommand(sessionToken: sessionToken) + try newUser.meCommand(sessionToken: sessionToken, + refreshToken: refreshToken, + expiresAt: expiresAt) .executeAsync(options: options, callbackQueue: callbackQueue) { result in if case .success(let foundResult) = result { @@ -269,7 +310,9 @@ extension ParseUser { } } - internal func meCommand(sessionToken: String) throws -> API.Command { + internal func meCommand(sessionToken: String, + refreshToken: String?, + expiresAt: Date?) throws -> API.Command { return API.Command(method: .GET, path: endpoint) { (data) -> Self in @@ -283,7 +326,9 @@ extension ParseUser { Self.currentUserContainer = .init( currentUser: user, - sessionToken: sessionToken + sessionToken: sessionToken, + refreshToken: refreshToken, + expiresAt: expiresAt ) Self.saveCurrentContainerToKeychain() return user @@ -295,10 +340,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. deleteCurrentContainerFromKeychain() BaseParseInstallation.deleteCurrentContainerFromKeychain() @@ -321,24 +373,48 @@ 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. - deleteCurrentContainerFromKeychain() - BaseParseInstallation.deleteCurrentContainerFromKeychain() - BaseConfig.deleteCurrentContainerFromKeychain() + //Always let user logout locally, no matter the error. + deleteCurrentContainerFromKeychain() + BaseParseInstallation.deleteCurrentContainerFromKeychain() + BaseConfig.deleteCurrentContainerFromKeychain() - 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. + deleteCurrentContainerFromKeychain() + BaseParseInstallation.deleteCurrentContainerFromKeychain() + BaseConfig.deleteCurrentContainerFromKeychain() + + 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)) } } } @@ -346,13 +422,103 @@ 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 sessionToken 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 { + try refreshCommand().execute(options: options) + } + + /** + Refreshes the sessionToken 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 { + 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?.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 } - } + } } } @@ -596,7 +762,9 @@ extension ParseUser { Self.currentUserContainer = .init( currentUser: user, - sessionToken: response.sessionToken + sessionToken: response.sessionToken, + refreshToken: response.refreshToken, + expiresAt: response.expiresAt ) Self.saveCurrentContainerToKeychain() return user @@ -612,7 +780,9 @@ extension ParseUser { Self.currentUserContainer = .init( currentUser: user, - sessionToken: response.sessionToken + sessionToken: response.sessionToken, + refreshToken: response.refreshToken, + expiresAt: response.expiresAt ) Self.saveCurrentContainerToKeychain() return user diff --git a/Tests/ParseSwiftTests/ParseUserCombineTests.swift b/Tests/ParseSwiftTests/ParseUserCombineTests.swift index 480316ddc..b15893d13 100644 --- a/Tests/ParseSwiftTests/ParseUserCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseUserCombineTests.swift @@ -64,6 +64,44 @@ 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 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.sessionToken = "myToken" + self.refreshToken = "yolo" + self.expiresAt = date + self.username = "hello10" + } + + private enum CodingKeys: String, CodingKey { // swiftlint:disable:this nesting + case createdAt, objectId, updatedAt, username + case sessionToken, refreshToken + case expiresAt = "expires_in" + } + } + let loginUserName = "hello10" let loginPassword = "world" @@ -119,6 +157,8 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNil(signedUp.password) XCTAssertNotNil(signedUp.objectId) XCTAssertNotNil(signedUp.sessionToken) + XCTAssertNil(signedUp.refreshToken) + XCTAssertNil(signedUp.expiresAt) XCTAssertNotNil(signedUp.customKey) XCTAssertNil(signedUp.ACL) @@ -134,6 +174,8 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.refreshToken) + XCTAssertNil(userFromKeychain.expiresAt) XCTAssertNil(userFromKeychain.ACL) }) publisher.store(in: &subscriptions) @@ -170,6 +212,8 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNil(signedUp.password) XCTAssertNotNil(signedUp.objectId) XCTAssertNotNil(signedUp.sessionToken) + XCTAssertNil(signedUp.refreshToken) + XCTAssertNil(signedUp.expiresAt) XCTAssertNotNil(signedUp.customKey) XCTAssertNil(signedUp.ACL) @@ -185,6 +229,8 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.refreshToken) + XCTAssertNil(userFromKeychain.expiresAt) XCTAssertNil(userFromKeychain.ACL) }) publisher.store(in: &subscriptions) @@ -254,6 +300,8 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNil(signedUp.password) XCTAssertNotNil(signedUp.objectId) XCTAssertNotNil(signedUp.sessionToken) + XCTAssertNil(signedUp.refreshToken) + XCTAssertNil(signedUp.expiresAt) XCTAssertNotNil(signedUp.customKey) XCTAssertNil(signedUp.ACL) @@ -269,12 +317,166 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) XCTAssertNotNil(userFromKeychain.sessionToken) + 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 + } + + var serverResponse = LoginSignupResponse() + serverResponse.createdAt = User.current?.createdAt + serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) + serverResponse.sessionToken = "newValue" + serverResponse.username = "stop" + + var subscriptions = Set() + MockURLProtocol.mockRequests { _ in + do { + let encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } catch { + return nil + } + } + + let expectation1 = XCTestExpectation(description: "Become user1") + let publisher = user.becomePublisher(sessionToken: serverResponse.sessionToken, + 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) + XCTAssertNotNil(signedUp.email) + XCTAssertNotNil(signedUp.username) + XCTAssertNil(signedUp.password) + XCTAssertNotNil(signedUp.objectId) + XCTAssertNotNil(signedUp.sessionToken) + 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) + XCTAssertNotNil(userFromKeychain.email) + XCTAssertNotNil(userFromKeychain.username) + XCTAssertNil(userFromKeychain.password) + XCTAssertNotNil(userFromKeychain.objectId) + XCTAssertNotNil(userFromKeychain.sessionToken) + 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.sessionToken = "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 sessionToken = user.sessionToken, + let refreshToken = user.refreshToken, + let expiresAt = user.expiresAt else { + XCTFail("Should unwrap all values") + return + } + XCTAssertEqual(sessionToken, refreshResponse.sessionToken) + 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 sessionTokenFromKeychain = user.sessionToken, + let refreshTokenFromKeychain = user.refreshToken, + let expiresAtFromKeychain = user.expiresAt else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + XCTAssertEqual(sessionTokenFromKeychain, refreshResponse.sessionToken) + 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..48f571645 --- /dev/null +++ b/Tests/ParseSwiftTests/ParseUserOAuthTests.swift @@ -0,0 +1,711 @@ +// +// 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 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.sessionToken = "myToken" + self.refreshToken = "yolo" + self.expiresAt = date + self.username = "hello10" + } + + private enum CodingKeys: String, CodingKey { // swiftlint:disable:this nesting + case createdAt, objectId, updatedAt, username + case sessionToken, refreshToken + case expiresAt = "expires_in" + } + } + 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) + 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, "/refresh") + XCTAssertEqual(command.method, API.Method.POST) + XCTAssertNil(command.params) + 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) + XCTAssertNil(command.params) + 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) + XCTAssertNotNil(signedUp.sessionToken) + 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) + XCTAssertNotNil(userFromKeychain.sessionToken) + 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) + XCTAssertNotNil(signedUp.sessionToken) + 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) + XCTAssertNotNil(userFromKeychain.sessionToken) + 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) + XCTAssertNotNil(loggedIn.sessionToken) + 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) + XCTAssertNotNil(userFromKeychain.sessionToken) + 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) + XCTAssertNotNil(loggedIn.sessionToken) + 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) + XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.ACL) + 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.sessionToken = "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 sessionToken = user.sessionToken, + let refreshToken = user.refreshToken, + let expiresAt = user.expiresAt else { + XCTFail("Should unwrap all values") + return + } + XCTAssertEqual(sessionToken, refreshResponse.sessionToken) + 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 sessionTokenFromKeychain = user.sessionToken, + let refreshTokenFromKeychain = user.refreshToken, + let expiresAtFromKeychain = user.expiresAt else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + XCTAssertEqual(sessionTokenFromKeychain, refreshResponse.sessionToken) + 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 sessionToken = user.sessionToken, + let refreshToken = user.refreshToken, + let expiresAt = user.expiresAt else { + XCTFail("Should unwrap all values") + return + } + XCTAssertEqual(sessionToken, refreshResponse.sessionToken) + 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 sessionTokenFromKeychain = user.sessionToken, + let refreshTokenFromKeychain = user.refreshToken, + let expiresAtFromKeychain = user.expiresAt else { + XCTFail("Couldn't get CurrentUser from Keychain") + return + } + XCTAssertEqual(sessionTokenFromKeychain, refreshResponse.sessionToken) + 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.sessionToken = "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) + 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.sessionToken = "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(sessionToken: "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.sessionToken) + XCTAssertNotNil(become.refreshToken) + XCTAssertNotNil(become.expiresAt) + + //Should be updated in memory + XCTAssertEqual(User.current?.updatedAt, becomeUpdatedAt) + + //Should be updated in Keychain + #if !os(Linux) + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) else { + XCTFail("Should get object from Keychain") + return + } + XCTAssertEqual(keychainUser.currentUser?.updatedAt, becomeUpdatedAt) + XCTAssertNotNil(keychainUser.sessionToken) + 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.sessionToken = "newValue" + serverResponse.username = "stop" + serverResponse.password = "this" + + var userOnServer: User! + + let encoded: Data! + do { + encoded = try serverResponse.getEncoder().encode(serverResponse, skipKeys: .none) + //Get dates in correct format from ParseDecoding strategy + userOnServer = try serverResponse.getDecoder().decode(User.self, from: encoded) + } catch { + XCTFail("Should encode/decode. Error \(error)") + return + } + MockURLProtocol.mockRequests { _ in + return MockURLResponse(data: encoded, statusCode: 200, delay: 0.0) + } + + let expectation1 = XCTestExpectation(description: "Fetch user1") + user.become(sessionToken: "newValue", + 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.sessionToken) + XCTAssertNotNil(become.refreshToken) + XCTAssertNotNil(become.expiresAt) + + //Should be updated in memory + XCTAssertEqual(User.current?.updatedAt, becomeUpdatedAt) + + #if !os(Linux) + //Should be updated in Keychain + guard let keychainUser: CurrentUserContainer + = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) else { + XCTFail("Should get object from Keychain") + expectation1.fulfill() + return + } + XCTAssertEqual(keychainUser.currentUser?.updatedAt, becomeUpdatedAt) + XCTAssertNotNil(keychainUser.sessionToken) + 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 aee6f831e..07cb222cd 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -786,6 +786,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) @@ -801,6 +803,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 { @@ -823,6 +827,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) @@ -839,6 +845,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) @@ -931,6 +939,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) @@ -948,6 +958,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) } @@ -1832,7 +1844,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) From 7af5f242f8cc4de693506d017533390c3bc03076 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sat, 13 Mar 2021 12:39:12 -0500 Subject: [PATCH 02/10] Remove expires_in coding key --- Sources/ParseSwift/API/Responses.swift | 6 ------ Tests/ParseSwiftTests/ParseUserCombineTests.swift | 6 ------ Tests/ParseSwiftTests/ParseUserOAuthTests.swift | 6 ------ 3 files changed, 18 deletions(-) diff --git a/Sources/ParseSwift/API/Responses.swift b/Sources/ParseSwift/API/Responses.swift index 6692e713d..604911094 100644 --- a/Sources/ParseSwift/API/Responses.swift +++ b/Sources/ParseSwift/API/Responses.swift @@ -122,12 +122,6 @@ internal struct LoginSignupResponse: Codable { let sessionToken: String let refreshToken: String? let expiresAt: Date? - - private enum CodingKeys: String, CodingKey { - case createdAt, objectId, updatedAt, username - case sessionToken, refreshToken - case expiresAt = "expires_in" - } } // MARK: ParseFile diff --git a/Tests/ParseSwiftTests/ParseUserCombineTests.swift b/Tests/ParseSwiftTests/ParseUserCombineTests.swift index b15893d13..f947a4796 100644 --- a/Tests/ParseSwiftTests/ParseUserCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseUserCombineTests.swift @@ -94,12 +94,6 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le self.expiresAt = date self.username = "hello10" } - - private enum CodingKeys: String, CodingKey { // swiftlint:disable:this nesting - case createdAt, objectId, updatedAt, username - case sessionToken, refreshToken - case expiresAt = "expires_in" - } } let loginUserName = "hello10" diff --git a/Tests/ParseSwiftTests/ParseUserOAuthTests.swift b/Tests/ParseSwiftTests/ParseUserOAuthTests.swift index 48f571645..a20fbe294 100644 --- a/Tests/ParseSwiftTests/ParseUserOAuthTests.swift +++ b/Tests/ParseSwiftTests/ParseUserOAuthTests.swift @@ -60,12 +60,6 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng self.expiresAt = date self.username = "hello10" } - - private enum CodingKeys: String, CodingKey { // swiftlint:disable:this nesting - case createdAt, objectId, updatedAt, username - case sessionToken, refreshToken - case expiresAt = "expires_in" - } } let loginUserName = "hello10" let loginPassword = "world" From 3d68ff5f0a70b09115d810fbfa3d3c765e7bb1f9 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sat, 13 Mar 2021 14:04:48 -0500 Subject: [PATCH 03/10] Add tests for refreshing non oauth token --- .../Contents.swift | 5 +++ .../Contents.swift | 23 +++++++++++ ParseSwift.playground/contents.xcplayground | 2 +- Tests/ParseSwiftTests/ParseUserTests.swift | 38 +++++++++++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) 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 367bb02d7..9e0935e4e 100644 --- a/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift @@ -42,6 +42,11 @@ 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 refreshToken = currentUser.refreshToken { + print("The users' refreshToken is: \(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 6cd3d0f98..8361410af 100644 --- a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift @@ -65,6 +65,29 @@ User.current?.save { results in } } +//: If signed in using OAuth2.0, ask the server to refresh the token +if let currentUser = User.current, + let refreshToken = currentUser.refreshToken { + print("The current sessionToken: \(currentUser.expiresAt)") + print("The current refreshToken is: \(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 refreshToken = updatedUser.refreshToken { + print("The new sessionToken: \(updatedUser.expiresAt)") + 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.playground/contents.xcplayground b/ParseSwift.playground/contents.xcplayground index b482d7b97..ab9557970 100644 --- a/ParseSwift.playground/contents.xcplayground +++ b/ParseSwift.playground/contents.xcplayground @@ -1,5 +1,5 @@ - + diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index 07cb222cd..25b091231 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -1997,5 +1997,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 From 664e19af74ae9692bbfa7572c68684980ebaca89 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sat, 13 Mar 2021 14:22:11 -0500 Subject: [PATCH 04/10] nits --- Sources/ParseSwift/Objects/ParseUser+combine.swift | 2 +- Sources/ParseSwift/Objects/ParseUser.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index e2aeca95e..99cbc4bf9 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -101,7 +101,7 @@ public extension ParseUser { // MARK: Refreshing SessionToken - Combine /** - Refreshes the OAuth2.0 sessionToken of the currently logged in + Refreshes the OAuth2.0 tokens of the currently logged in user *asynchronously*. Publishes when complete. This will also update the session in the Keychain. diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index d58fbd7f6..de9746f1c 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -435,7 +435,7 @@ extension ParseUser { extension ParseUser { /** - Refreshes the sessionToken of the currently logged in user in the Keychain *synchronously*. + 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. @@ -445,7 +445,7 @@ extension ParseUser { } /** - Refreshes the sessionToken of the currently logged in user in the Keychain *asynchronously*. + 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. From 47261114c4d70144b4093b02914fa50e7ff1a7d6 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sun, 14 Mar 2021 22:12:22 -0400 Subject: [PATCH 05/10] Update changelog --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1f17b7d8c..96ab52872 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.2.0...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.0 [Full Changelog](https://github.com/parse-community/Parse-Swift/compare/1.1.6...1.2.0) From 939a23ef195dd2ffa5b32cc45e0b5135a62cff91 Mon Sep 17 00:00:00 2001 From: Corey Date: Sun, 14 Mar 2021 22:19:33 -0400 Subject: [PATCH 06/10] Update .codecov.yml --- .codecov.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.codecov.yml b/.codecov.yml index 2517c8e20..5148b8498 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -5,7 +5,7 @@ coverage: status: patch: default: - target: 72 + target: auto changes: false project: default: From f178c8042d4a78a2f31f53db7b0e70ca4c806aa4 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Mon, 15 Mar 2021 22:00:41 -0400 Subject: [PATCH 07/10] Add access token. Note: expiresAt from server needs to be real date and not a number. --- Sources/ParseSwift/API/API.swift | 6 +- Sources/ParseSwift/API/Responses.swift | 3 +- .../Objects/ParseUser+combine.swift | 5 +- Sources/ParseSwift/Objects/ParseUser.swift | 41 ++++++++++-- .../ParseUserCombineTests.swift | 46 ++++++++----- .../ParseSwiftTests/ParseUserOAuthTests.swift | 65 +++++++++++-------- 6 files changed, 110 insertions(+), 56 deletions(-) diff --git a/Sources/ParseSwift/API/API.swift b/Sources/ParseSwift/API/API.swift index ead1af2f7..8ce877ff2 100644 --- a/Sources/ParseSwift/API/API.swift +++ b/Sources/ParseSwift/API/API.swift @@ -154,8 +154,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?.sessionToken { + 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 { diff --git a/Sources/ParseSwift/API/Responses.swift b/Sources/ParseSwift/API/Responses.swift index 0144e9fd0..d1d957039 100644 --- a/Sources/ParseSwift/API/Responses.swift +++ b/Sources/ParseSwift/API/Responses.swift @@ -119,7 +119,8 @@ internal struct LoginSignupResponse: Codable { var updatedAt: Date? let objectId: String let username: String? - let sessionToken: String + let sessionToken: String? + let accessToken: String? let refreshToken: String? let expiresAt: Date? } diff --git a/Sources/ParseSwift/Objects/ParseUser+combine.swift b/Sources/ParseSwift/Objects/ParseUser+combine.swift index e94e7b806..2f740757b 100644 --- a/Sources/ParseSwift/Objects/ParseUser+combine.swift +++ b/Sources/ParseSwift/Objects/ParseUser+combine.swift @@ -81,17 +81,20 @@ 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, + 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, + accessToken: accessToken, refreshToken: refreshToken, expiresAt: expiresAt, options: options, diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 51c342e83..cc869dcd4 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -79,7 +79,7 @@ extension ParseUser { struct CurrentUserContainer: Codable { var currentUser: T? var sessionToken: String? - var oauth: Bool? + var accessToken: String? var refreshToken: String? var expiresAt: Date? } @@ -146,6 +146,15 @@ extension ParseUser { 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`. @@ -227,6 +236,7 @@ extension ParseUser { Self.currentUserContainer = .init( currentUser: user, sessionToken: response.sessionToken, + accessToken: response.accessToken, refreshToken: response.refreshToken, expiresAt: response.expiresAt ) @@ -240,20 +250,27 @@ 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, + 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)) + 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, @@ -265,6 +282,7 @@ 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. @@ -273,7 +291,8 @@ extension ParseUser { - 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 = [], @@ -282,9 +301,14 @@ extension ParseUser { 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, + accessToken: accessToken, refreshToken: refreshToken, expiresAt: expiresAt) .executeAsync(options: options, @@ -310,7 +334,8 @@ extension ParseUser { } } - internal func meCommand(sessionToken: String, + internal func meCommand(sessionToken: String? = nil, + accessToken: String? = nil, refreshToken: String?, expiresAt: Date?) throws -> API.Command { @@ -327,6 +352,7 @@ extension ParseUser { Self.currentUserContainer = .init( currentUser: user, sessionToken: sessionToken, + accessToken: accessToken, refreshToken: refreshToken, expiresAt: expiresAt ) @@ -493,6 +519,7 @@ extension ParseUser { 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() @@ -763,6 +790,7 @@ extension ParseUser { Self.currentUserContainer = .init( currentUser: user, sessionToken: response.sessionToken, + accessToken: response.accessToken, refreshToken: response.refreshToken, expiresAt: response.expiresAt ) @@ -781,6 +809,7 @@ extension ParseUser { Self.currentUserContainer = .init( currentUser: user, sessionToken: response.sessionToken, + accessToken: response.accessToken, refreshToken: response.refreshToken, expiresAt: response.expiresAt ) diff --git a/Tests/ParseSwiftTests/ParseUserCombineTests.swift b/Tests/ParseSwiftTests/ParseUserCombineTests.swift index 2dadcd1cd..2ec4b2fd2 100644 --- a/Tests/ParseSwiftTests/ParseUserCombineTests.swift +++ b/Tests/ParseSwiftTests/ParseUserCombineTests.swift @@ -68,9 +68,10 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le var objectId: String? var createdAt: Date? - var sessionToken: String + var sessionToken: String? var updatedAt: Date? var ACL: ParseACL? + var accessToken: String? var refreshToken: String? var expiresAt: Date? @@ -89,7 +90,8 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le self.updatedAt = date self.objectId = "yarr" self.ACL = nil - self.sessionToken = "myToken" + self.customKey = "blah" + self.accessToken = "myToken" self.refreshToken = "yolo" self.expiresAt = date self.username = "hello10" @@ -151,6 +153,7 @@ 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) @@ -168,6 +171,7 @@ 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) @@ -206,6 +210,7 @@ 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) @@ -223,6 +228,7 @@ 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) @@ -259,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() @@ -277,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 { @@ -294,6 +300,7 @@ 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) @@ -311,6 +318,7 @@ 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) @@ -328,11 +336,11 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTFail("Should unwrap") return } - - var serverResponse = LoginSignupResponse() + let accessToken = "newValue" + var serverResponse = LoginSignupResponseOAuth() serverResponse.createdAt = User.current?.createdAt serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) - serverResponse.sessionToken = "newValue" + serverResponse.accessToken = accessToken serverResponse.username = "stop" var subscriptions = Set() @@ -346,7 +354,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(accessToken: accessToken, refreshToken: "yolo", expiresAt: Date()) .sink(receiveCompletion: { result in @@ -360,11 +368,12 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNotNil(signedUp) XCTAssertNotNil(signedUp.createdAt) XCTAssertNotNil(signedUp.updatedAt) - XCTAssertNotNil(signedUp.email) + XCTAssertNil(signedUp.email) XCTAssertNotNil(signedUp.username) XCTAssertNil(signedUp.password) XCTAssertNotNil(signedUp.objectId) - XCTAssertNotNil(signedUp.sessionToken) + XCTAssertNil(signedUp.sessionToken) + XCTAssertNotNil(signedUp.accessToken) XCTAssertNotNil(signedUp.refreshToken) XCTAssertNotNil(signedUp.expiresAt) XCTAssertNotNil(signedUp.customKey) @@ -377,11 +386,12 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNotNil(userFromKeychain.createdAt) XCTAssertNotNil(userFromKeychain.updatedAt) - XCTAssertNotNil(userFromKeychain.email) + XCTAssertNil(userFromKeychain.email) XCTAssertNotNil(userFromKeychain.username) XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) - XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.sessionToken) + XCTAssertNotNil(userFromKeychain.accessToken) XCTAssertNotNil(userFromKeychain.refreshToken) XCTAssertNotNil(userFromKeychain.expiresAt) XCTAssertNil(userFromKeychain.ACL) @@ -409,7 +419,7 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le MockURLProtocol.removeAll() var refreshResponse = LoginSignupResponseOAuth() - refreshResponse.sessionToken = "hello" + refreshResponse.accessToken = "hello" refreshResponse.refreshToken = "world" refreshResponse.expiresAt = Date() var subscriptions = Set() @@ -431,13 +441,13 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le expectation1.fulfill() }, receiveValue: { user in - guard let sessionToken = user.sessionToken, + guard let accessToken = user.accessToken, let refreshToken = user.refreshToken, let expiresAt = user.expiresAt else { XCTFail("Should unwrap all values") return } - XCTAssertEqual(sessionToken, refreshResponse.sessionToken) + XCTAssertEqual(accessToken, refreshResponse.accessToken) XCTAssertEqual(refreshToken, refreshResponse.refreshToken) XCTAssertEqual(expiresAt, refreshResponse.expiresAt) XCTAssertNotNil(user.createdAt) @@ -449,13 +459,13 @@ class ParseUserCombineTests: XCTestCase { // swiftlint:disable:this type_body_le XCTAssertNil(user.ACL) guard let userFromKeychain = BaseParseUser.current, - let sessionTokenFromKeychain = user.sessionToken, + let accessTokenFromKeychain = user.accessToken, let refreshTokenFromKeychain = user.refreshToken, let expiresAtFromKeychain = user.expiresAt else { XCTFail("Couldn't get CurrentUser from Keychain") return } - XCTAssertEqual(sessionTokenFromKeychain, refreshResponse.sessionToken) + XCTAssertEqual(accessTokenFromKeychain, refreshResponse.accessToken) XCTAssertEqual(refreshTokenFromKeychain, refreshResponse.refreshToken) XCTAssertEqual(expiresAtFromKeychain, refreshResponse.expiresAt) XCTAssertNotNil(userFromKeychain.createdAt) diff --git a/Tests/ParseSwiftTests/ParseUserOAuthTests.swift b/Tests/ParseSwiftTests/ParseUserOAuthTests.swift index a20fbe294..6730c9e56 100644 --- a/Tests/ParseSwiftTests/ParseUserOAuthTests.swift +++ b/Tests/ParseSwiftTests/ParseUserOAuthTests.swift @@ -34,9 +34,10 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng var objectId: String? var createdAt: Date? - var sessionToken: String + var sessionToken: String? var updatedAt: Date? var ACL: ParseACL? + var accessToken: String? var refreshToken: String? var expiresAt: Date? @@ -55,7 +56,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng self.updatedAt = date self.objectId = "yarr" self.ACL = nil - self.sessionToken = "myToken" + self.accessToken = "myToken" self.refreshToken = "yolo" self.expiresAt = date self.username = "hello10" @@ -146,7 +147,8 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertNil(signedUp.password) XCTAssertNil(signedUp.email) XCTAssertNotNil(signedUp.objectId) - XCTAssertNotNil(signedUp.sessionToken) + XCTAssertNil(signedUp.sessionToken) + XCTAssertNotNil(signedUp.accessToken) XCTAssertNotNil(signedUp.refreshToken) XCTAssertNotNil(signedUp.expiresAt) XCTAssertNil(signedUp.ACL) @@ -162,7 +164,8 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertNotNil(userFromKeychain.username) XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) - XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.sessionToken) + XCTAssertNotNil(userFromKeychain.accessToken) XCTAssertNotNil(userFromKeychain.refreshToken) XCTAssertNotNil(userFromKeychain.expiresAt) XCTAssertNil(userFromKeychain.ACL) @@ -186,7 +189,8 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertNotNil(signedUp.username) XCTAssertNil(signedUp.password) XCTAssertNotNil(signedUp.objectId) - XCTAssertNotNil(signedUp.sessionToken) + XCTAssertNil(signedUp.sessionToken) + XCTAssertNotNil(signedUp.accessToken) XCTAssertNotNil(signedUp.refreshToken) XCTAssertNotNil(signedUp.expiresAt) XCTAssertNil(signedUp.ACL) @@ -203,7 +207,8 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertNil(userFromKeychain.email) XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) - XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.sessionToken) + XCTAssertNotNil(userFromKeychain.accessToken) XCTAssertNotNil(userFromKeychain.refreshToken) XCTAssertNotNil(userFromKeychain.expiresAt) XCTAssertNil(userFromKeychain.ACL) @@ -251,7 +256,8 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertNotNil(loggedIn.username) XCTAssertNil(loggedIn.password) XCTAssertNotNil(loggedIn.objectId) - XCTAssertNotNil(loggedIn.sessionToken) + XCTAssertNil(loggedIn.sessionToken) + XCTAssertNotNil(loggedIn.accessToken) XCTAssertNotNil(loggedIn.refreshToken) XCTAssertNotNil(loggedIn.expiresAt) XCTAssertNil(loggedIn.ACL) @@ -267,7 +273,8 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertNil(userFromKeychain.email) XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) - XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.sessionToken) + XCTAssertNotNil(userFromKeychain.accessToken) XCTAssertNotNil(userFromKeychain.refreshToken) XCTAssertNotNil(userFromKeychain.expiresAt) XCTAssertNil(userFromKeychain.ACL) @@ -292,7 +299,8 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertNotNil(loggedIn.username) XCTAssertNil(loggedIn.password) XCTAssertNotNil(loggedIn.objectId) - XCTAssertNotNil(loggedIn.sessionToken) + XCTAssertNil(loggedIn.sessionToken) + XCTAssertNotNil(loggedIn.accessToken) XCTAssertNotNil(loggedIn.refreshToken) XCTAssertNotNil(loggedIn.expiresAt) XCTAssertNil(loggedIn.ACL) @@ -309,8 +317,9 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertNotNil(userFromKeychain.username) XCTAssertNil(userFromKeychain.password) XCTAssertNotNil(userFromKeychain.objectId) - XCTAssertNotNil(userFromKeychain.sessionToken) + XCTAssertNil(userFromKeychain.sessionToken) XCTAssertNil(userFromKeychain.ACL) + XCTAssertNotNil(userFromKeychain.accessToken) XCTAssertNotNil(userFromKeychain.refreshToken) XCTAssertNotNil(userFromKeychain.expiresAt) case .failure(let error): @@ -341,7 +350,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng MockURLProtocol.removeAll() var refreshResponse = LoginSignupResponse() - refreshResponse.sessionToken = "hello" + refreshResponse.accessToken = "hello" refreshResponse.refreshToken = "world" refreshResponse.expiresAt = Calendar.current.date(byAdding: .init(day: 1), to: Date()) @@ -357,13 +366,13 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng } do { let user = try User.refresh() - guard let sessionToken = user.sessionToken, + guard let accessToken = user.accessToken, let refreshToken = user.refreshToken, let expiresAt = user.expiresAt else { XCTFail("Should unwrap all values") return } - XCTAssertEqual(sessionToken, refreshResponse.sessionToken) + XCTAssertEqual(accessToken, refreshResponse.accessToken) XCTAssertEqual(refreshToken, refreshResponse.refreshToken) XCTAssertEqual(expiresAt, refreshResponse.expiresAt) XCTAssertNotNil(user.createdAt) @@ -375,13 +384,13 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertNil(user.ACL) guard let userFromKeychain = BaseParseUser.current, - let sessionTokenFromKeychain = user.sessionToken, + let accessTokenFromKeychain = user.accessToken, let refreshTokenFromKeychain = user.refreshToken, let expiresAtFromKeychain = user.expiresAt else { XCTFail("Couldn't get CurrentUser from Keychain") return } - XCTAssertEqual(sessionTokenFromKeychain, refreshResponse.sessionToken) + XCTAssertEqual(accessTokenFromKeychain, refreshResponse.accessToken) XCTAssertEqual(refreshTokenFromKeychain, refreshResponse.refreshToken) XCTAssertEqual(expiresAtFromKeychain, refreshResponse.expiresAt) XCTAssertNotNil(userFromKeychain.createdAt) @@ -404,13 +413,13 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng switch result { case .success(let user): - guard let sessionToken = user.sessionToken, + guard let accessToken = user.accessToken, let refreshToken = user.refreshToken, let expiresAt = user.expiresAt else { XCTFail("Should unwrap all values") return } - XCTAssertEqual(sessionToken, refreshResponse.sessionToken) + XCTAssertEqual(accessToken, refreshResponse.accessToken) XCTAssertEqual(refreshToken, refreshResponse.refreshToken) XCTAssertEqual(expiresAt, refreshResponse.expiresAt) XCTAssertNotNil(user.createdAt) @@ -422,13 +431,13 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertNil(user.ACL) guard let userFromKeychain = BaseParseUser.current, - let sessionTokenFromKeychain = user.sessionToken, + let accessTokenFromKeychain = user.accessToken, let refreshTokenFromKeychain = user.refreshToken, let expiresAtFromKeychain = user.expiresAt else { XCTFail("Couldn't get CurrentUser from Keychain") return } - XCTAssertEqual(sessionTokenFromKeychain, refreshResponse.sessionToken) + XCTAssertEqual(accessTokenFromKeychain, refreshResponse.accessToken) XCTAssertEqual(refreshTokenFromKeychain, refreshResponse.refreshToken) XCTAssertEqual(expiresAtFromKeychain, refreshResponse.expiresAt) XCTAssertNotNil(userFromKeychain.createdAt) @@ -452,7 +461,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng MockURLProtocol.removeAll() var refreshResponse = LoginSignupResponse() - refreshResponse.sessionToken = "hello" + refreshResponse.accessToken = "hello" refreshResponse.refreshToken = "world" refreshResponse.expiresAt = Date() @@ -558,7 +567,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng var serverResponse = LoginSignupResponse() serverResponse.createdAt = User.current?.createdAt serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) - serverResponse.sessionToken = "newValue" + serverResponse.accessToken = "newValue" serverResponse.username = "stop" var userOnServer: User! @@ -577,7 +586,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng } do { - let become = try user.become(sessionToken: "newValue", + let become = try user.become(accessToken: "newValue", refreshToken: "yolo", expiresAt: Date()) XCTAssert(become.hasSameObjectId(as: userOnServer)) @@ -594,7 +603,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertEqual(becomeCreatedAt, originalCreatedAt) XCTAssertGreaterThan(becomeUpdatedAt, originalUpdatedAt) XCTAssertNil(become.ACL) - XCTAssertNotNil(become.sessionToken) + XCTAssertNotNil(become.accessToken) XCTAssertNotNil(become.refreshToken) XCTAssertNotNil(become.expiresAt) @@ -609,7 +618,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng return } XCTAssertEqual(keychainUser.currentUser?.updatedAt, becomeUpdatedAt) - XCTAssertNotNil(keychainUser.sessionToken) + XCTAssertNotNil(keychainUser.accessToken) XCTAssertNotNil(keychainUser.refreshToken) XCTAssertNotNil(keychainUser.expiresAt) #endif @@ -633,7 +642,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng var serverResponse = LoginSignupResponse() serverResponse.createdAt = User.current?.createdAt serverResponse.updatedAt = User.current?.updatedAt?.addingTimeInterval(+300) - serverResponse.sessionToken = "newValue" + serverResponse.accessToken = "newValue" serverResponse.username = "stop" serverResponse.password = "this" @@ -653,7 +662,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng } let expectation1 = XCTestExpectation(description: "Fetch user1") - user.become(sessionToken: "newValue", + user.become(accessToken: "newValue", refreshToken: "yolo", expiresAt: Date()) { result in @@ -675,7 +684,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertEqual(becomeCreatedAt, originalCreatedAt) XCTAssertGreaterThan(becomeUpdatedAt, originalUpdatedAt) XCTAssertNil(become.ACL) - XCTAssertNotNil(become.sessionToken) + XCTAssertNotNil(become.accessToken) XCTAssertNotNil(become.refreshToken) XCTAssertNotNil(become.expiresAt) @@ -691,7 +700,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng return } XCTAssertEqual(keychainUser.currentUser?.updatedAt, becomeUpdatedAt) - XCTAssertNotNil(keychainUser.sessionToken) + XCTAssertNotNil(keychainUser.accessToken) XCTAssertNotNil(keychainUser.refreshToken) XCTAssertNotNil(keychainUser.expiresAt) #endif From 34e68cea1534c6dcf888dc4ad1bc74e93840e084 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Tue, 16 Mar 2021 17:29:35 -0400 Subject: [PATCH 08/10] Updates --- .../3 - User - Sign Up.xcplaygroundpage/Contents.swift | 5 +++-- .../Contents.swift | 10 ++++++---- Sources/ParseSwift/API/API.swift | 2 +- Tests/ParseSwiftTests/ParseUserOAuthTests.swift | 4 ++-- 4 files changed, 12 insertions(+), 9 deletions(-) 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 cdfbe3bf3..028918c37 100644 --- a/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/3 - User - Sign Up.xcplaygroundpage/Contents.swift @@ -49,8 +49,9 @@ User.signup(username: "hello", password: "world") { results in } else { print("Successfully signed up user \(user)") print("The users' sessionToken is: \(currentUser.sessionToken)") - if let refreshToken = currentUser.refreshToken { - print("The users' refreshToken is: \(refreshToken)") + 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!)") } } diff --git a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift index 8c5883a0b..54f9fe492 100644 --- a/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift +++ b/ParseSwift.playground/Pages/4 - User - Continued.xcplaygroundpage/Contents.swift @@ -55,9 +55,10 @@ struct GameScore: ParseObject { //: If signed in using OAuth2.0, ask the server to refresh the token if let currentUser = User.current, - let refreshToken = currentUser.refreshToken { + let accessToken = currentUser.accessToken { print("The current sessionToken: \(currentUser.expiresAt)") - print("The current refreshToken is: \(refreshToken)") + 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 @@ -65,8 +66,9 @@ if let currentUser = User.current, case .success(let updatedUser): print("Successfully refreshed users tokens") if let updatedUser = User.current, - let refreshToken = updatedUser.refreshToken { - print("The new sessionToken: \(updatedUser.expiresAt)") + 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)") } diff --git a/Sources/ParseSwift/API/API.swift b/Sources/ParseSwift/API/API.swift index 8ce877ff2..fef2e9c53 100644 --- a/Sources/ParseSwift/API/API.swift +++ b/Sources/ParseSwift/API/API.swift @@ -53,7 +53,7 @@ public struct API { case .user(let objectId): return "/users/\(objectId)" case .refresh: - return "/refresh" + return "/users/refresh" case .installations: return "/installations" case .installation(let objectId): diff --git a/Tests/ParseSwiftTests/ParseUserOAuthTests.swift b/Tests/ParseSwiftTests/ParseUserOAuthTests.swift index 6730c9e56..704a527f6 100644 --- a/Tests/ParseSwiftTests/ParseUserOAuthTests.swift +++ b/Tests/ParseSwiftTests/ParseUserOAuthTests.swift @@ -353,7 +353,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng 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) @@ -363,7 +363,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng } catch { return nil } - } + }*/ do { let user = try User.refresh() guard let accessToken = user.accessToken, From a5c31e16188a6feac88ff9e119834e33aebcf4a6 Mon Sep 17 00:00:00 2001 From: Corey Date: Thu, 18 Mar 2021 06:09:21 -0400 Subject: [PATCH 09/10] Fix accessToken in header --- Sources/ParseSwift/API/API.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/ParseSwift/API/API.swift b/Sources/ParseSwift/API/API.swift index fef2e9c53..19cd9c844 100644 --- a/Sources/ParseSwift/API/API.swift +++ b/Sources/ParseSwift/API/API.swift @@ -154,7 +154,7 @@ public struct API { headers["X-Parse-Client-Key"] = clientKey } - if let accessToken = BaseParseUser.currentUserContainer?.sessionToken { + 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 From 9de2d51f10cabb56f27ed03c54b11c00be33d541 Mon Sep 17 00:00:00 2001 From: Corey's iMac Date: Sun, 21 Mar 2021 21:15:48 -0400 Subject: [PATCH 10/10] Add missing Android builds in test cases --- Tests/ParseSwiftTests/ParseUserOAuthTests.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Tests/ParseSwiftTests/ParseUserOAuthTests.swift b/Tests/ParseSwiftTests/ParseUserOAuthTests.swift index b01115827..aa5f855d4 100644 --- a/Tests/ParseSwiftTests/ParseUserOAuthTests.swift +++ b/Tests/ParseSwiftTests/ParseUserOAuthTests.swift @@ -81,7 +81,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng override func tearDownWithError() throws { super.tearDown() MockURLProtocol.removeAll() - #if !os(Linux) + #if !os(Linux) && !os(Android) try KeychainStore.shared.deleteAll() #endif try ParseStorage.shared.deleteAll() @@ -519,7 +519,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTFail("\(installationFromMemory) wasn't deleted from memory during logout") } - #if !os(Linux) + #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") @@ -609,7 +609,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng XCTAssertEqual(User.current?.updatedAt, becomeUpdatedAt) //Should be updated in Keychain - #if !os(Linux) + #if !os(Linux) && !os(Android) guard let keychainUser: CurrentUserContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) else { XCTFail("Should get object from Keychain") @@ -689,7 +689,7 @@ class ParseUserOAuthTests: XCTestCase { // swiftlint:disable:this type_body_leng //Should be updated in memory XCTAssertEqual(User.current?.updatedAt, becomeUpdatedAt) - #if !os(Linux) + #if !os(Linux) && !os(Android) //Should be updated in Keychain guard let keychainUser: CurrentUserContainer = try? KeychainStore.shared.get(valueFor: ParseStorage.Keys.currentUser) else {