From 7a0540c9a1f7a2d60f52c50a09b1e8911a7d9ad5 Mon Sep 17 00:00:00 2001 From: Corey Date: Tue, 9 May 2023 13:56:54 -0400 Subject: [PATCH] feat: Add the ability to login users automatically (#98) * feat: Add the ability to create users automatically * fix automatic login * doc nits * increase codecov * Update ParseVersion.swift --- CHANGELOG.md | 1 + .../Protocols/ParseAuthentication+async.swift | 11 ++++ .../Protocols/ParseAuthentication.swift | 36 ++++++++---- Sources/ParseSwift/Objects/ParseUser.swift | 38 ++++++++++++- Sources/ParseSwift/Parse.swift | 7 +++ .../ParseSwift/Types/ParseConfiguration.swift | 11 ++++ Sources/ParseSwift/Types/ParseVersion.swift | 2 +- .../ParseSwiftTests/ParseAnonymousTests.swift | 55 +++++++++++++++++++ Tests/ParseSwiftTests/ParseAppleTests.swift | 31 +++++++++++ .../ParseAuthenticationAsyncTests.swift | 1 + Tests/ParseSwiftTests/ParseUserTests.swift | 11 ++++ 11 files changed, 190 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c615bca7e..2b2e40629 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ [Full Changelog](https://github.com/netreconlab/Parse-Swift/compare/5.4.3...5.5.0), [Documentation](https://swiftpackageindex.com/netreconlab/Parse-Swift/5.5.0/documentation/parseswift) __New features__ +* Adds a setting to enable automatic user login by calling User.current(). The setting can be enabled/disabled when initializing the SDK by setting "usingAutomaticLogin" or at anytime after initialization using User.enableAutomaticLogin() ([#98](https://github.com/netreconlab/Parse-Swift/pull/98)), thanks to [Corey Baker](https://github.com/cbaker6). * Add ParseServer.information() to retrieve version and info from a Parse Server. Depracates ParseHealth and check() in favor of ParseServer and health() respectively ([#97](https://github.com/netreconlab/Parse-Swift/pull/97)), thanks to [Corey Baker](https://github.com/cbaker6). ### 5.4.3 diff --git a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+async.swift b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+async.swift index 02c927532..78e4fc950 100644 --- a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+async.swift +++ b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication+async.swift @@ -89,4 +89,15 @@ public extension ParseUser { } } + internal static func signupWithAuthData(_ type: String, + authData: [String: String], + options: API.Options = []) async throws -> Self { + try await withCheckedThrowingContinuation { continuation in + Self.signupWithAuthData(type, + authData: authData, + options: options, + completion: continuation.resume) + } + } + } diff --git a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift index 7d14f96bb..7c22c2794 100644 --- a/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift +++ b/Sources/ParseSwift/Authentication/Protocols/ParseAuthentication.swift @@ -255,17 +255,31 @@ public extension ParseUser { callbackQueue: callbackQueue, completion: completion) } catch { - let body = SignupLoginBody(authData: [type: authData]) - do { - try await signupCommand(body: body) - .execute(options: options, - callbackQueue: callbackQueue, - completion: completion) - } catch { - let parseError = error as? ParseError ?? ParseError(swift: error) - callbackQueue.async { - completion(.failure(parseError)) - } + signupWithAuthData(type, + authData: authData, + options: options, + callbackQueue: callbackQueue, + completion: completion) + } + } + } + + internal static func signupWithAuthData(_ type: String, + authData: [String: String], + options: API.Options = [], + callbackQueue: DispatchQueue = .main, + completion: @escaping (Result) -> Void) { + let body = SignupLoginBody(authData: [type: authData]) + Task { + do { + try await signupCommand(body: body) + .execute(options: options, + callbackQueue: callbackQueue, + completion: completion) + } catch { + let parseError = error as? ParseError ?? ParseError(swift: error) + callbackQueue.async { + completion(.failure(parseError)) } } } diff --git a/Sources/ParseSwift/Objects/ParseUser.swift b/Sources/ParseSwift/Objects/ParseUser.swift index 961fc5cb9..daa825a13 100644 --- a/Sources/ParseSwift/Objects/ParseUser.swift +++ b/Sources/ParseSwift/Objects/ParseUser.swift @@ -168,8 +168,14 @@ public extension ParseUser { try await yieldIfNotInitialized() guard let container = await Self.currentContainer(), let user = container.currentUser else { - throw ParseError(code: .otherCause, - message: "There is no current user logged in") + // User automatic login if configured + guard Parse.configuration.isUsingAutomaticLogin else { + throw ParseError(code: .otherCause, + message: "There is no current user logged in") + } + let authData = ParseAnonymous.AuthenticationKeys.id.makeDictionary() + return try await Self.loginLazy(ParseAnonymous().__type, + authData: authData) } return user } @@ -186,6 +192,12 @@ public extension ParseUser { currentContainer?.currentUser = newValue await Self.setCurrentContainer(currentContainer) } + + internal static func loginLazy(_ type: String, authData: [String: String]) async throws -> Self { + try await Self.signupWithAuthData(type, + authData: authData) + } + } // MARK: SignupLoginBody @@ -1573,4 +1585,26 @@ public extension Sequence where Element: ParseUser { } } } +} + +// MARK: Automatic User +public extension ParseObject { + + /** + Enables/disables automatic creation of anonymous users. After calling this method, + `Self.current()` will always have a value or throw an error from the server. + When enabled, the user will only be created on the server once. + + - parameter enable: **true** allows automatic user logins, **false** + disables automatic user logins. Defaults to **true**. + - throws: An error of `ParseError` type. + */ + static func enableAutomaticLogin(_ enable: Bool = true) async throws { + try await yieldIfNotInitialized() + guard Parse.configuration.isUsingAutomaticLogin != enable else { + return + } + Parse.configuration.isUsingAutomaticLogin = enable + } + } // swiftlint:disable:this file_length diff --git a/Sources/ParseSwift/Parse.swift b/Sources/ParseSwift/Parse.swift index 1926cc8a0..c78be58cf 100644 --- a/Sources/ParseSwift/Parse.swift +++ b/Sources/ParseSwift/Parse.swift @@ -29,6 +29,7 @@ internal func initialize(applicationId: String, usingDataProtectionKeychain: Bool = false, deletingKeychainIfNeeded: Bool = false, httpAdditionalHeaders: [AnyHashable: Any]? = nil, + usingAutomaticLogin: Bool = false, maxConnectionAttempts: Int = 5, liveQueryMaxConnectionAttempts: Int = 20, testing: Bool = false, @@ -52,6 +53,7 @@ internal func initialize(applicationId: String, usingDataProtectionKeychain: usingDataProtectionKeychain, deletingKeychainIfNeeded: deletingKeychainIfNeeded, httpAdditionalHeaders: httpAdditionalHeaders, + usingAutomaticLogin: usingAutomaticLogin, maxConnectionAttempts: maxConnectionAttempts, liveQueryMaxConnectionAttempts: liveQueryMaxConnectionAttempts, authentication: authentication) @@ -235,6 +237,9 @@ public func initialize(configuration: ParseConfiguration) async throws { // swif - parameter httpAdditionalHeaders: A dictionary of additional headers to send with requests. See Apple's [documentation](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1411532-httpadditionalheaders) for more info. + - parameter usingAutomaticLogin: If **true**, automatic creation of anonymous users is enabled. + When enabled, `User.current()` will always have a value or throw an error from the server. The user will only be created on + the server once. - parameter maxConnectionAttempts: Maximum number of times to try to connect to Parse Server. Defaults to 5. - parameter liveQueryMaxConnectionAttempts: Maximum number of times to try to connect to a Parse @@ -270,6 +275,7 @@ public func initialize( usingDataProtectionKeychain: Bool = false, deletingKeychainIfNeeded: Bool = false, httpAdditionalHeaders: [AnyHashable: Any]? = nil, + usingAutomaticLogin: Bool = false, maxConnectionAttempts: Int = 5, liveQueryMaxConnectionAttempts: Int = 20, parseFileTransfer: ParseFileTransferable? = nil, @@ -293,6 +299,7 @@ public func initialize( usingDataProtectionKeychain: usingDataProtectionKeychain, deletingKeychainIfNeeded: deletingKeychainIfNeeded, httpAdditionalHeaders: httpAdditionalHeaders, + usingAutomaticLogin: usingAutomaticLogin, maxConnectionAttempts: maxConnectionAttempts, liveQueryMaxConnectionAttempts: liveQueryMaxConnectionAttempts, parseFileTransfer: parseFileTransfer, diff --git a/Sources/ParseSwift/Types/ParseConfiguration.swift b/Sources/ParseSwift/Types/ParseConfiguration.swift index a2d9fa542..3ec4b625c 100644 --- a/Sources/ParseSwift/Types/ParseConfiguration.swift +++ b/Sources/ParseSwift/Types/ParseConfiguration.swift @@ -91,6 +91,12 @@ public struct ParseConfiguration { /// apps do not have credentials to setup a Keychain. public internal(set) var isUsingDataProtectionKeychain: Bool = false + /// If **true**, automatic creation of anonymous users is enabled. + /// When enabled, `User.current()` will always have a value or throw an error from the server. + /// The user will only be created on the server once. + /// Defaults to **false**. + public internal(set) var isUsingAutomaticLogin: Bool = false + /// Maximum number of times to try to connect to a Parse Server. /// Defaults to 5. public internal(set) var maxConnectionAttempts: Int = 5 @@ -147,6 +153,9 @@ public struct ParseConfiguration { - parameter httpAdditionalHeaders: A dictionary of additional headers to send with requests. See Apple's [documentation](https://developer.apple.com/documentation/foundation/urlsessionconfiguration/1411532-httpadditionalheaders) for more info. + - parameter usingAutomaticLogin: If **true**, automatic creation of anonymous users is enabled. + When enabled, `User.current()` will always have a value or throw an error from the server. The user will only be created on + the server once. - parameter maxConnectionAttempts: Maximum number of times to try to connect to a Parse Server. Defaults to 5. - parameter liveQueryMaxConnectionAttempts: Maximum number of times to try to connect to a Parse @@ -181,6 +190,7 @@ public struct ParseConfiguration { usingDataProtectionKeychain: Bool = false, deletingKeychainIfNeeded: Bool = false, httpAdditionalHeaders: [AnyHashable: Any]? = nil, + usingAutomaticLogin: Bool = false, maxConnectionAttempts: Int = 5, liveQueryMaxConnectionAttempts: Int = 20, parseFileTransfer: ParseFileTransferable? = nil, @@ -206,6 +216,7 @@ public struct ParseConfiguration { self.isUsingDataProtectionKeychain = usingDataProtectionKeychain self.isDeletingKeychainIfNeeded = deletingKeychainIfNeeded self.httpAdditionalHeaders = httpAdditionalHeaders + self.isUsingAutomaticLogin = usingAutomaticLogin self.maxConnectionAttempts = maxConnectionAttempts self.liveQueryMaxConnectionAttempts = liveQueryMaxConnectionAttempts self.parseFileTransfer = parseFileTransfer ?? ParseFileDefaultTransfer() diff --git a/Sources/ParseSwift/Types/ParseVersion.swift b/Sources/ParseSwift/Types/ParseVersion.swift index 0d07d60a3..60bf94916 100644 --- a/Sources/ParseSwift/Types/ParseVersion.swift +++ b/Sources/ParseSwift/Types/ParseVersion.swift @@ -172,7 +172,7 @@ public struct ParseVersion: ParseTypeable, Hashable { } // MARK: Default Implementation -extension ParseVersion { +public extension ParseVersion { init(string: String) throws { self = try Self.convertVersionString(string) diff --git a/Tests/ParseSwiftTests/ParseAnonymousTests.swift b/Tests/ParseSwiftTests/ParseAnonymousTests.swift index 89f50b29c..2344ff7b6 100644 --- a/Tests/ParseSwiftTests/ParseAnonymousTests.swift +++ b/Tests/ParseSwiftTests/ParseAnonymousTests.swift @@ -124,6 +124,7 @@ class ParseAnonymousTests: XCTestCase { XCTAssertNotEqual(authData["id"], "12345") } + @MainActor func testLogin() async throws { var serverResponse = LoginSignupResponse() let authData = ParseAnonymous.AuthenticationKeys.id.makeDictionary() @@ -160,6 +161,60 @@ class ParseAnonymousTests: XCTestCase { XCTAssertTrue(isLinked) } + @MainActor + func testLoginAutomaticLogin() async throws { + try await User.enableAutomaticLogin() + XCTAssertTrue(Parse.configuration.isUsingAutomaticLogin) + + var serverResponse = LoginSignupResponse() + let authData = ParseAnonymous.AuthenticationKeys.id.makeDictionary() + serverResponse.username = "hello" + serverResponse.password = "world" + serverResponse.objectId = "yarr" + serverResponse.sessionToken = "myToken" + serverResponse.authData = [serverResponse.anonymous.__type: authData] + serverResponse.createdAt = Date() + serverResponse.updatedAt = serverResponse.createdAt?.addingTimeInterval(+300) + + var userOnServer: User + + var 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) + } + + let currentUser = try await User.current() + XCTAssertEqual(currentUser, userOnServer) + XCTAssertEqual(currentUser.username, "hello") + XCTAssertEqual(currentUser.password, "world") + let isLinked = await currentUser.anonymous.isLinked() + XCTAssertTrue(isLinked) + + // User stays the same and does not access server when logged in already + serverResponse.objectId = "peace" + 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) + } + let currentUser2 = try await User.current() + XCTAssertEqual(currentUser, currentUser2) + } + @MainActor func testLoginAuthData() async throws { var serverResponse = LoginSignupResponse() diff --git a/Tests/ParseSwiftTests/ParseAppleTests.swift b/Tests/ParseSwiftTests/ParseAppleTests.swift index 83306f6ac..580880123 100644 --- a/Tests/ParseSwiftTests/ParseAppleTests.swift +++ b/Tests/ParseSwiftTests/ParseAppleTests.swift @@ -520,4 +520,35 @@ class ParseAppleTests: XCTestCase { XCTAssertNil(updatedUser.password) XCTAssertFalse(ParseApple.isLinked(with: updatedUser)) } + + @MainActor + func testUnlinkNotLoggedIn() async throws { + do { + _ = try await User.apple.unlink() + XCTFail("Should have thrown error") + } catch { + XCTAssertTrue(error.containedIn([.invalidLinkedSession])) + let isLinked = await User.apple.isLinked() + XCTAssertFalse(isLinked) + } + } + + func testUnlinkNotLoggedInCompletion() async throws { + let expectation1 = XCTestExpectation(description: "Wait 1") + User.apple.unlink { result in + switch result { + case .success: + XCTFail("Should have produced error") + case .failure(let error): + XCTAssertEqual(error.code, .invalidLinkedSession) + } + expectation1.fulfill() + } + #if compiler(>=5.8.0) && !os(Linux) && !os(Android) && !os(Windows) + await fulfillment(of: [expectation1], timeout: 20.0) + #elseif compiler(<5.8.0) && !os(iOS) && !os(tvOS) + wait(for: [expectation1], timeout: 20.0) + #endif + } + } diff --git a/Tests/ParseSwiftTests/ParseAuthenticationAsyncTests.swift b/Tests/ParseSwiftTests/ParseAuthenticationAsyncTests.swift index 8fd7021b6..e57d8fe20 100644 --- a/Tests/ParseSwiftTests/ParseAuthenticationAsyncTests.swift +++ b/Tests/ParseSwiftTests/ParseAuthenticationAsyncTests.swift @@ -223,4 +223,5 @@ class ParseAuthenticationAsyncTests: XCTestCase { XCTAssertEqual(user.username, "hello10") XCTAssertNil(user.password) } + } diff --git a/Tests/ParseSwiftTests/ParseUserTests.swift b/Tests/ParseSwiftTests/ParseUserTests.swift index b300d7b19..7995837ac 100644 --- a/Tests/ParseSwiftTests/ParseUserTests.swift +++ b/Tests/ParseSwiftTests/ParseUserTests.swift @@ -2543,5 +2543,16 @@ class ParseUserTests: XCTestCase { // swiftlint:disable:this type_body_length } } } + + func testEnableAutomaticLogin() async throws { + XCTAssertFalse(Parse.configuration.isUsingAutomaticLogin) + try await User.enableAutomaticLogin(false) + XCTAssertFalse(Parse.configuration.isUsingAutomaticLogin) + try await User.enableAutomaticLogin() + XCTAssertTrue(Parse.configuration.isUsingAutomaticLogin) + try await User.enableAutomaticLogin(false) + XCTAssertFalse(Parse.configuration.isUsingAutomaticLogin) + } + } // swiftlint:disable:this file_length